1. 程式人生 > >[Unity]建構函式與單例模式

[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>{
    //
}