Unity的C#編程教程_63_單一實例 Singleton 詳解及應用練習
目錄:
Singleton UI Manager
Challenge: Singleton Spawn Manager
Singleton: Lazy Instantiation
1. Lazy Instantiation
2. Downfall of Lazy Instantiation
MonoSingleton
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 的兩大優勢:
- global access conmunication
- 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()
{
}
}