天天看點

Unity3d之設計模式(一)單例模式

為什麼要使用單例模式

在我們的整個遊戲生命周期當中,有很多對象從始至終有且隻有一個。這個唯一的執行個體隻需要生成一次,并且直到遊戲結束才需要銷毀。 

單例模式一般應用于管理器類,或者是一些需要持久化存在的對象。

Unity3d中單例模式的實作方式

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

因為單例本身的寫法不是重點,是以這裡就略過,直接上代碼。 

以下代碼來自于MSDN。

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

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

(二)如果是MonoBeheviour呢?

MonoBeheviour和一般的類有幾個重要差別,展現在單例模式上有兩點。 

第一,MonoBehaviour不能使用構造函數進行執行個體化,隻能挂載在GameObject上。 

第二,當切換場景時,目前場景中的GameObject都會被銷毀(LoadLevel帶有additional參數時除外),這種情況下,我們的單例對象也會被銷毀。 

為了使之不被銷毀,我們需要進行DontDestroyOnLoad的處理。同時,為了保持場景當中隻有一個執行個體,我們要對目前場景中的單例進行判斷,如果存在其他的執行個體,則應該将其全部删除。 

是以,建構單例的方式會變成這樣。

public sealed class SingletonMoBehaviour: MonoBehaviour
{ 
    private static volatile SingletonBehaviour instance; 
    private static object syncRoot = new Object(); 
    public static SingletonBehaviour Instance 
    { 
        get  
        { 
            if (instance == null)  
            { 
                lock (syncRoot)  
                { 
                    if (instance == null)  {
                        SingletonBehaviour[] instances = FindObjectsOfType<SingletonBehaviour>();
                        if (instances != null){
                            for (var i = 0; i < instances.Length; i++) {
                                Destroy(instances[i].gameObject);
                            }
                        }
                        GameObject go = new GameObject("_SingletonBehaviour");
                        instance = go.AddComponent<SingletonBehaviour>();
                        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 = FindObjectsOfType<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
public static T Instance
    {
        get
        {
            if (instance == null)
            {
                T[] instances = FindObjectsOfType<T>();
                if (instances != null)
                {
                    instance = instances[0];
                    for (var i = 1; i < instances.Length; i++)
                    {
                        Destroy(instances[i].gameObject);
                    }
                }
            }
            return instance;
        }
    }
           

單例與靜态的差別

我們都知道,靜态的成員或者方法,在整個Runtime當中也隻有一份。是以一直存在着靜态與單例模式之争。 

事實上這兩種方式都有其适用範圍,不能片面的說某種好或某種不好。具體的争論實在是太多了,資料也多,這裡也不深入講,僅僅簡單的說明一下兩者使用上的差別。 

* 單例的方法可以繼承,靜态的不可以。 

* 單例存在着建立執行個體的過程,生命周期并不是整個運作時,靜态方法在編譯時就存在,整個過程中是一直有效的。 

雖然兩者的差別其實非常多,但在這裡隻說一個最核心的問題,如何進行選擇?

其實很簡單,從面向對象的角度來說—— 

* 如果方法中需要用到執行個體本身的狀态,也就是說需要用到執行個體的成員時,這個方法一定是執行個體方法,請使用單例調用。 

* 如果方法中完全不涉及到執行個體,而是類共享的一些狀态的話,或者甚至不需要任何狀态,這個方法一定是靜态方法。 

從應用的角度來說,我覺得以上就足夠了,至于說記憶體占用的不同啊,GC以及效率上的差別啊這些我覺得更多是理論,不夠貼近實際使用。

單例雖好,請勿濫用

濫用設計模式是很多人都會遇到的問題,尤其是對新手來說。設計模式應該隻在合适的場景當中使用,而不是随處都使用單例。 

事實上,單例的濫用會造成以下一些問題: 

* 代碼的耦合性可能會增加。如一個子產品當中調用MusicController.instance.Play,可能導緻這個子產品無法獨立複用。 

* 單個類的職責可能會過大,違背單一職責原則。 

* 某些情況下會造成一些性能問題。因為單例的對象永遠不銷毀,過多的單例會造成性能問題。 

可以使用一些别的方法來代替單例模式,這裡暫時不再擴充。

單例的單例

在某些情況下我會使用這種方法來建構唯一執行個體。 即在總單例類中聲明了初始化其他的子單例類,友善了單例的統一擷取和初始化。

擷取某個子單例的執行個體,可以用GameRoot.Instance.dbManager或DBManager.Instance。 

作為更高一級的控制器的單例成員或者類變量,同樣可以使該執行個體在整個遊戲中僅存在一份。 

其優勢在于擴充性更好,因為我們可以随時添加單例的Controller類,等等。這裡就不再擴充了。 

using UnityEngine;

public class GameRoot : MonoBehaviour {

    //資料讀取管理類
    [HideInInspector]
    public DBManager       dbManager;

    //頁面管理器
    [HideInInspector]
    public PageManager      pageManager;

    private static object _lock = new object();
    private static GameRoot _instance;
    public static GameRoot Instance
    {
        get
        {
            lock (_lock)
            {
                if (_instance == null)
                {
                    GameObject go = new GameObject("GameRoot");
                    _instance = go.AddComponent<GameRoot>();
                }
            }
            return _instance;
        }
    }
    private void Awake()
    {
        if (_instance == null)
        {
            _instance = this;
            _instance.Initialize();
        }
        else
        {
            Destroy(this);
            _instance = null;
        }
        DontDestroyOnLoad(this);
    }

	void Initialize()
    {
        dbManager = gameObject.AddComponent<SqlManager>();
        dbManager.Init();

        pageManager = gameObject.AddComponent<PageManager>();
        pageManager.Init();
    }
}
           

DBManager單例類:

public class DBManager : MonoBehaviour {

    private static DBManager _instance = null;
    public static DBManager Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = GameRoot.Instance.dbManager;
            }
            return _instance;
        }
    }
}
           

繼續閱讀