1. 程式人生 > >Unity3d與設計模式(二)單例模式

Unity3d與設計模式(二)單例模式

為什麼要使用單例模式

在我們的整個遊戲生命週期當中,有很多物件從始至終有且只有一個。這個唯一的例項只需要生成一次,並且直到遊戲結束才需要銷燬。
單例模式一般應用於管理器類,或者是一些需要持久化存在的物件。

Unity3d中單例模式的實現方式

(一)c#當中實現單例模式的方法

因為單例本身的寫法不是重點,所以這裡就略過,直接上程式碼。
以下程式碼來自於MSDN。

public sealed class Singleton 
{ 
   private static volatile Singleton instance; 
   private static object
syncRoot = new Object(); private Singleton() {} public static Singleton Instance { get { if (instance == null) { lock (syncRoot) { if (instance == null) instance = new Singleton(); } } return
instance; } } }

以上程式碼是比較完整版本的c#單例。在unity當中,如果不需要使用到monobeheviour的話,可以使用這種方式來構建單例。

(二)如果是monobeheviour呢?

monobeheviour和一般的類有幾個重要區別,體現在單例模式上有兩點。
第一,monohehaviour不能使用建構函式進行例項化,只能掛載在GameObject上。
第二,當切換場景時,當前場景中的GameObject都會被銷燬(LoadLevel帶有additional引數時除外),這種情況下,我們的單例物件也會被銷燬。
為了使之不被銷燬,我們需要進行DontDestroyOnLoad的處理。同時,為了保持場景當中只有一個例項,我們要對當前場景中的單例進行判斷,如果存在其他的例項,則應該將其全部刪除。
因此,構建單例的方式會變成這樣。

public sealed class SingletonMonoBehaviour: MonoBehaviour
{ 
    private static volatile SingletonMonoBehaviour instance; 
    private static object syncRoot = new Object(); 
    public static SingletonMonoBehaviour Instance 
    { 
        get  
        { 
            if (instance == null)  
            { 
                lock (syncRoot)  
                { 
                    if (instance == null)  {
                        SingletonMonoBehaviour[] instances = (SingletonMonoBehaviour[])FindObjectsOfType(typeof(SingletonMonoBehaviour));
                        if (instances != null){
                            for (var i = 0; i < instances.Length; i++) {
                                Destroy(instances[i].gameObject);
                            }
                        }
                        GameObject go = new GameObject("__SingletonMonoBehaviour");
                        instance = go.AddComponent<SingletonMonoBehaviour>();
                        DontDestroyOnLoad(go); 
                    }

                } 
            } 
            return instance; 
        } 
    } 
} 

這種方式並非完美。其缺陷至少有:
* 如果有許多的單例類,會需要複製貼上這些程式碼
* 有些時候我們也許會希望使用當前存在的所有例項,而不是刪除全部新建一個例項。(這個未必是缺陷,只是設計的不同)
在本文後面將會附上這種單例模式的程式碼以及測試。

(三)使用模板類實現單例

為了避免重複程式碼,我們可以使用模板類的方式來生成單例。非MonoBehaviour的實現方式這裡就不贅述,只說monoBehaviour的。
程式碼

public sealed class SingletonTemplate<T>: MonoBehaviour where T : MonoBehaviour {
    private static volatile T instance; 
    private static object syncRoot = new Object(); 
    public static T Instance 
    { 
        get  
        { 
            if (instance == null)  
            { 
                lock (syncRoot)  
                { 
                    if (instance == null)  {
                        T[] instances = (T[])FindObjectsOfType(typeof(T));
                        if (instances != null){
                            for (var i = 0; i < instances.Length; i++) {
                                Destroy(instances[i].gameObject);
                            }
                        }
                        GameObject go = new GameObject();
                        go.name = typeof(T).Name;
                        instance = go.AddComponent<T>();
                        DontDestroyOnLoad(go); 
                    }

                } 
            } 
            return instance; 
        } 
    } 
}

以上程式碼解決了每個單例類都需要重複寫同樣程式碼的問題,基本上算一個比較好的解決方案。

單例當中的一些坑

  • 最大的坑是單例的monobehaviour,其生命週期並非我們程式設計師可以控制的。MonoBehaviour本身的Destroy,將會決定單例類的例項在何時銷燬。因此,一定不要在OnDestroy函式中呼叫單例物件,這可能導致該物件在遊戲結束後依然存在(原本的單例類已經銷燬了,你又建立了一個新的,當然就不會再銷燬一次了)。舉例來說,以下的程式碼是需要注意的的。
void Start(){
    Singleton.Instance.OnSomeTime += DoSth;
}

void OnDestroy(){
    Singleton.Instance.OnSomeTime -= DoSth;
}
  • 此外,建議不要在場景或者預置當中放置擁有單例類元件的Gameobject。很多網上的專案有這樣的寫法。但我的觀點是這種寫法不夠靈活。如果使用這種方法,注意在獲取instance時,將找到的第一個物件賦給instance
if (instance == null)  {
                        T[] instances = (T[])FindObjectsOfType(typeof(T));
                        if (instances != null){
                        instance = instances[0];
                            for (var i = 1; i < instances.Length; i++) {
                                Destroy(instances[i].gameObject);
                            }
                        }
    }

單例與靜態的區別

我們都知道,靜態的成員或者方法,在整個Runtime當中也只有一份。所以一直存在著靜態與單例模式之爭。
事實上這兩種方式都有其適用範圍,不能片面的說某種好或某種不好。具體的爭論實在是太多了,資料也多,這裡也不深入講,僅僅簡單的說明一下兩者使用上的區別。
* 單例的方法可以繼承,靜態的不可以。
* 單例存在著建立例項的過程,生命週期並不是整個執行時,靜態方法在編譯時就存在,整個過程中是一直有效的。
雖然兩者的區別其實非常多,但在這裡只說一個最核心的問題,如何進行選擇?

其實很簡單,從面向物件的角度來說——
* 如果方法中需要用到例項本身的狀態,也就是說需要用到例項的成員時,這個方法一定是例項方法,請使用單例呼叫。
* 如果方法中完全不涉及到例項,而是類共享的一些狀態的話,或者甚至不需要任何狀態,這個方法一定是靜態方法。
從應用的角度來說,我覺得以上就足夠了,至於說記憶體佔用的不同啊,GC以及效率上的區別啊這些我覺得更多是理論,不夠貼近實際使用。

單例雖好,請勿濫用

濫用設計模式是很多人都會遇到的問題,尤其是對新手來說。設計模式應該只在合適的場景當中使用,而不是隨處都使用單例。
事實上,單例的濫用會造成以下一些問題:
* 程式碼的耦合性可能會增加。如一個模組當中呼叫MusicController.instance.Play,可能導致這個模組無法獨立複用。
* 單個類的職責可能會過大,違背單一職責原則。
* 某些情況下會造成一些效能問題。
可以使用一些別的方法來代替單例模式,這裡暫時不再擴充套件。

不使用單例的單例

在某些情況下我會使用這種方法來構建唯一例項。
Game.Instance.MusicController或Game.MusicController。
作為更高一級的控制器的單例成員或者類變數,同樣可以使該例項在整個遊戲中僅存在一份。
其優勢在於擴充套件性更好,因為我們可以隨時新增Game.Instance.ReleaseMusicController,等等。這裡就不再擴充套件了。
本文的程式碼如下
singleton