[Unity]建構函式與單例模式
從BUG說起
在實現一個小功能時,遇到了一個bug,程式碼如下:
public class EnemySpawner : MonoBehaviour {
#region singleton
private EnemySpawner() {}
private static EnemySpawner _instance = null;
public static EnemySpawner Instance {
get {
if(_instance == null)
_instance = new EnemySpawner();
return _instance;
}
}
#endregion
//remianing
void Update(){
//spwan enemies
}
}
實現了一個敵人孵化器的類,由於它應該是存在於全域性,且僅有一份,所以將其寫作單例模式。
上述程式碼是不考慮多執行緒問題的單例實現。
將其掛載到一個空物體上,執行,但是出現了一系列的問題。
在除錯過程中發現,在Instance的get中,即便new EnemySpawner(),__instance仍然為null??!!
我百思不得解,而且神奇的是,當我寫下如下程式碼測試的時候:
public class AudioManager : MonoBehaviour {
public AudioManager(){
Debug.Log("You call me in AudioManager");
}
}
當我開始運行遊戲時,Log輸出了兩次”You call me in AudioManager”.
只好藉助引擎了..
永遠不要在繼承MonoBehaviour的類中預設建構函式的呼叫
其實,在Unity的文件中有提到上述問題:
“避免使用建構函式 不要在建構函式中初始化任何變數,使用Awake或Start實現這個目的。即使是在編輯模式中Unity也自動呼叫建構函式,這通常發生在一個指令碼被編譯之後,因為需要呼叫建構函式來取向一個指令碼的預設值。建構函式不僅會在無法預料的時刻被呼叫,它也會為預設或未啟用的遊戲物體呼叫。”
實際上,MonoBehaviour有兩個生命週期,一個是作為C#物件的週期,一個是作為Component的週期。建構函式代表第一個,Awake代表第二個。Editor環境下Editor的程式碼和指令碼程式碼在同一個AppDomain裡,物件的生命週期會表現的跟Player環境下不一樣。比如Editor中建構函式被呼叫的次數和時機跟build出來的遊戲不一樣,這樣就不容易保證正確性。
而且,在編輯器模式下,指令碼類就會被構造,在你進入遊戲之後,也會被構造,你無法預期類的建構函式何時被呼叫,因此,凡是繼承自MonoBehaviour的類,把它的Awake或者Update當做建構函式來初始化變數。
那麼,如何改進上述程式碼呢?
public class EnemySpawner : MonoBehaviour {
#region singleton
private EnemySpawner() {}
private static EnemySpawner _instance = null;
public static EnemySpawner Instance {
get {
return _instance;
}
}
#endregion
private void Awake() {
_instance = this;
}
}
不考慮多執行緒等問題,這樣改進應當是足夠的。
有人可能會指出,不是不可以使用建構函式嗎,你怎麼還private EnemySpawner() {}
這樣寫呢?實際上,這只是對它的可訪問性進行了限制,並沒有使用new EnemySpawner()
。
但是,這種單例實現有個缺陷,就是你必須這種將指令碼手動賦給一個GameObject(缺陷)。
更好的單例實現方式
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour{
private static T instance;
public static T Instance {
get {
if(instance == null){
GameObject obj = new GameObject();
instance = obj.AddComponent<T>();
obj.name = typeof(T).Name;
//切換場景之後不銷燬單例物件
DontDestroyOnLoad(obj);
}
return instance;
}
}
public class AudioManager : Singleton<AudioMananger>{
//
}