Unity的C#編程教程_63_單一實例 Singleton 詳解及應用練習

目錄:

Singleton Design Pattern

  • 遊戲設計模式之一:單一實例模式
    • 全局都可以訪問的 class,該 class 僅存在一個
    • 比如我們的 Manager class:Game Manager,Item Manager,Player Manager,UI Manager,Spawn Manager等
    • 我們一般不用通過 GetComponent 訪問,而是直接進行訪問
    • 使用 singleton,確保這個 class 僅有一個

創建 Game Manager 腳本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

}

創建一個 Player,掛載同名腳本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{

    private GameManager _gm;

    // Start is called before the first frame update
    void Start()
    {
        _gm = GameObject.Find("Game Manager").GetComponent<GameManager>();
        // 傳統方法找到對應的 GameManager 腳本,需要先找到對應的遊戲對象,然後獲取遊戲對象下面掛載的腳本組件
    }

    // Update is called once per frame
    void Update()
    {

    }
}

改成設計成 singleton:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    private static GameManager _instance;
    // 生成一個靜態實例,確保了僅有一個實例

    public static GameManager Instance
    {
        get
        {
            if (_instance == null)
            {
                Debug.Log("Game Manager is null");
            }

            return _instance;
        }
    }
    // 用於別的腳本與 GameManager 交互
    // 這個 property 僅有一個 get 方法,確保了別的腳本只能對其只讀
    // 讀取的就是 _instance 實例對象

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    private void Awake() // 載入腳本實例的時候進行調用
    {
        _instance = this; // 賦值為該遊戲對象
    }

    public void Test()
    {
        Debug.Log("Testing");
    }

}

在 Player 中調用:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{

    // private GameManager _gm;

    // Start is called before the first frame update
    void Start()
    {
        // _gm = GameObject.Find("Game Manager").GetComponent<GameManager>();
        // 傳統方法找到對應的 GameManager 腳本,需要先找到對應的遊戲對象,然後獲取遊戲對象下面掛載的腳本組件

        GameManager.Instance.Test();
        // 使用了 singleton 後就可以直接調用了
    }

    // Update is called once per frame
    void Update()
    {

    }
}

使用 singleton 的時候,我們可以讓所有的遊戲對象(非靜態)訪問 Manager 的腳本進行交互,這也是僅有的和 Manager class 交互的方式,Manager classes 之間也可以交互,但是 Manager 腳本不會訪問其他的遊戲對象進行交互,所以這種交互是單向的。

Singleton UI Manager

  • 使用 singleton 方式創建 UI Manager

創建一個空的遊戲對象,命名為 UI Manager,掛載同名腳本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // 需要引入 UI 的庫

public class UIManager : MonoBehaviour
{
    private static UIManager _instance;
    // static 確保只有一個,所有遊戲對象都訪問這個

    public static UIManager Instance // 這裡的 property 也是 static
    {
        get
        {
            if (_instance == null) // 檢測腳本有沒有掛載到遊戲對象上
            {
                Debug.Log("The UIManager instance is null");
            }

            return _instance;
        }
        // 這設定 property,外部腳本只讀訪問 _instance
    }

    private void Awake()
    {
        _instance = this; // 對實例進行賦值為該遊戲對象
    }

    public void ShowScore(int score)
    {
        Debug.Log("The score is: " + score);
        GameManager.Instance.Test();
        // Manager classes 之間也可以相互訪問
        // 但是 Manager class 不訪問別的遊戲對象
        // 僅由別的遊戲對象訪問 Manager class
    }
}

UI Manager 腳本負責管理遊戲中的所有 UI 遊戲對象,整個遊戲中只會有一個 UI Manager,所以可以使用 singleton 使得訪問更簡便。

還可以在 Manager classes 之間進行交互:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    private static GameManager _instance;
    // 生成一個靜態實例,確保了僅有一個實例

    public static GameManager Instance
    {
        get
        {
            if (_instance == null)
            {
                Debug.Log("Game Manager is null");
            }
            return _instance;
        }
    }

    private void Awake() // 載入腳本實例的時候進行調用
    {
        _instance = this; // 賦值為該遊戲對象
    }

    public void Test()
    {
        Debug.Log("Testing");
    }

}

Player 可以簡便地訪問 Manager class:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{

    // Start is called before the first frame update
    void Start()
    {
        UIManager.Instance.ShowScore(77);
        // 使用了 singleton 後就可以直接調用了
    }

    // Update is called once per frame
    void Update()
    {

    }
}

Challenge: Singleton Spawn Manager

  • 任務說明:
    • 創建一個 Spawn Manager
    • 將其設計為 singleton
    • 創建一個 SpawnEnemy 方法,然後在 Player 腳本中進行嘗試調用

創建遊戲對象 Spawn Manager 並掛載同名腳本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpawnManager : MonoBehaviour
{
    private static SpawnManager _instance;

    public static SpawnManager Instance
    {
        get
        {
            if (_instance == null)
            {
                Debug.Log("The Spawn Manager instance is null");
            }

            return _instance;
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    private void Awake()
    {
        _instance = this;
    }

    public void SpawnEnemy()
    {
        Debug.Log("Spawn one Enemy");
    }
}

在 Player 中調用:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{

    // Start is called before the first frame update
    void Start()
    {
        SpawnManager.Instance.SpawnEnemy();
        // 使用了 singleton 後就可以直接調用了
    }

    // Update is called once per frame
    void Update()
    {

    }
}

Singleton: Lazy Instantiation

1. Lazy Instantiation

  • 最優的做法是,在運行應用之前聲明 Manager
    • 比如我們應該建立對應的 class,還有對應的遊戲對象,掛載對應腳本
    • 然後還需要檢驗 instance 是不是存在

但是我們也可以先不創建遊戲對象,自然創建的腳本也不用掛載了:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // 需要引入 UI 的庫

public class UIManager : MonoBehaviour
{
    private static UIManager _instance;
    // static 確保只有一個,所有遊戲對象都訪問這個

    public static UIManager Instance // 這裡的 property 也是 static
    {
        get
        {
            if (_instance == null) // 檢測腳本有沒有掛載到遊戲對象上
            {
                GameObject obj = new GameObject("UI Manager");
                // 新建一個遊戲對象叫做 UI Manager
                obj.AddComponent<UIManager>();
                // 為該遊戲對象增加組件:該腳本
            }

            return _instance;
        }
        // 這設定 property,外部腳本只讀訪問 _instance
    }

    private void Awake()
    {
        _instance = this; // 對實例進行賦值為該遊戲對象
    }

    public void ShowScore(int score)
    {
        Debug.Log("The score is: " + score);
    }
}

我們可以把原有的 UI Manager 刪除,然後 Player 中照樣可以調用:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            UIManager.Instance.ShowScore(88);
        }
    }
}

運行以後會自動創建遊戲對象 UI Manager,停止運行後該遊戲對象會被刪除。

使用 singleton 的兩大優勢:

  1. global access conmunication
  2. lazily instantiate an object

2. Downfall of Lazy Instantiation

  • 使用 lazy instantiation 的缺點
    • 比如我們使用 Spawn Manager
    • 我們會通過這個腳本去生成 Enemy,這需要我們首先由 Enemy 的 prefab,然後在 inspector 中拖拽賦值
    • 如果一開始不建立 Spawn Manager 遊戲對象,那就沒法拖拽賦值了
    • 需要我們知道 prefab 存在的文件夾,然後通過程式碼進行賦值,比較麻煩

MonoSingleton

  • 把任何 manager class 簡單轉化為 singleton

之前我們製作 singleton 的時候,每一個 manager class 都需要進行初始化設定以及實例賦值,假設我們有 50 個,那就要進行 50 次類似的操作。

即然需要重複做,那我們就可以想辦法進行模組化操作。

建立一個 singleton 的模版,腳本命名為 MonoSingleton:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public  abstract class MonoSingleton<T> : MonoBehaviour where T: MonoSingleton<T>
    // 設定為抽象類,這樣就不能掛載到遊戲對象上,僅作為模版使用
    // <T> 設定了一個 generic type,即不同的類型都可以使用該模版,多態性
    // T 繼承的應用模版的時候所輸入的類型
    // 這裡這麼做的原因在於,我們不但要繼承 MonoSingleton,還需要知道具體的類型

{
    private static T _instance;

    public static T Instance
    {
        get
        {
            if (_instance == null)
            {
                Debug.LogError(typeof(T).ToString()+ " has no instance");
                // 顯示該類沒有實例化,即沒有賦值遊戲對象(遊戲對象掛載腳本)
            }

            return _instance;
        }
    }

    private void Awake()
    {
        _instance = this as T;
        // 或者 _instance = (T)this;
        Int();
        // 一旦載入實例,即進指定的行初始化
        // 如果沒有被重寫,那不執行任何操作
    }

    public virtual void Int() // 設定一個初始化方法
    {
        // 虛方法,可以選擇重寫,也可以不重寫
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

我們嘗試把 Player 的腳本來使用一下這個模版:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoSingleton<Player> // 繼承自模版,類型為 Player
{

    public override void Int()
    {
        base.Int(); // 調用默認的初始化方法,可去除
        Debug.Log("Player initialized!");

    }

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }
}

另外嘗試創建一個 Level Manager,掛載同名腳本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LevelManager : MonoSingleton<LevelManager>
{
    public override void Int()
    {
        Debug.Log("Level Manager initialized");
    }

    public void SetLevel()
    {
        Debug.Log("Set a level");
    }
}

通過 Player 嘗試直接調用:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoSingleton<Player>
{

    public override void Int()
    {
        Debug.Log("Player initialized!");
        LevelManager.Instance.SetLevel(); // 可以直接調用
    }

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }
}

Tags:
Exit mobile version