我要加入

UNITY3D聖殿-自由的遊戲設計天堂

會長:stormcorn / stormcorn開設日:2013-03-21 18:05:22

  • EXP

  • 資金24183  
  • 招募制度:自由加入制
  • 成員:1740 人
  • 昨日人氣:0

單例Singleton

推上精選編輯

近期編輯:ADSL1234567B ...看更多

目的
保證class只有一個object instance,並且提供單一存取點。

時機
  • 類別只能有一個實體,且需要提供方便的單一窗口給外界使用。
  • 類別也能以繼承的方式來擴充介面,不須修改原來的類別。

遊戲中通常有許多管理類型的class,並且絕大多數的狀況下只需要一個,這些管理器需要提供單一存取點給其他人來用。這時候就適合使用Singleton來解決此問題。不過在遊戲程式中,管理類型的class大都不會被繼承,尤其是愈高層次的遊戲邏輯,愈難有被繼承的情況,此時就得思考是否真的該設計為singleton。

但在Unity中,有些狀況會是繼承Component、Behaviour或MonoBehaviour之類的Unity class,這類class基於Unity的機制,禁止定義constructor,因此實作手法和一般有所差異。

一般範例,方法一
public class Manager
{
    private static Manager s_Instance;
    
    public static Manager Instance
    {
        get
        {
            if (s_Instance == null)
                s_Instance = new Manager();
            return s_Instance;
        }
    }
    
    private Manager() { }
}

一般範例,方法二
public class Manager
{
    private static readonly Manager s_Instance = new Manager();
    
    public static Manager Instance
    {
        get
        {
            return s_Instance;
        }
    }
    
    private Manager() { }
}

Unity Component範例
public class Manager : MonoBehaviour
{
    private static Manager s_Instance;
    
    public static Manager Instance
    {
        get
        {
            if (s_Instance == null)
            {
                s_Instance = FindObjectOfType(typeof(Manager)) as Manager;
                if (s_Instance == null)
                {
                    GameObject go = new GameObject("Manager");
                    s_Instance = go.AddComponent<Manager>();
                }
            }
            return s_Instance;
        }
    }

    void Start()
    {
        if (Instance != this) Destroy(this);
    }
}

由於與Unity核心有關的程式都是在main thread,而那些管理器若要在遊戲中被使用,也只能自main thread產生,在這情況下沒有多執行緒會遇到的問題。
WWW及Rendering則真的是多執行緒執行,WWW會有背景執行緒在執行下載任務。

進階Singleton
一種作法是採用註冊表的方式,不使用Instance窮舉所有Singleton class,而是讓這些class自行向註冊表登記,但有三個缺點,一是取得Instance還得轉型才能使用。二是在註冊時必須窮舉所有Singleton,使用起來有些彆扭。三是這麼會喪失延遲初始化(Lazy initialization)的效果,但遊戲中設計的singleton通常都一定會用到,因此這缺點不是很重要。
(尚未驗證)
public abstract class Singleton
{
    private static Singleton s_Instance;
    private static Dictionary<Type, Singleton> s_Registry =
        new Dictionary<Type, Singleton>();
        
    protected static Singleton Lookup(Type type)
    {
        if (type == null) return null;
        if (s_Registry.ContainsKey(type)) return s_Registry[type];
        return null;
    }

    public static void Register(Singleton singleton)
    {
        if (singleton == null) return;
        if (s_Registry.ContainsKey(singleton.GetType())) return;
        s_Registry.Add(singleton.GetType(), singleton);
    }

    public static Singleton GetInstance(Type type)
    {
        if (type == null) return null;
        if (s_Instance == null)
            s_Instance = Lookup(type);
        return s_Instance;
    }
}

public class MySingleton : Singleton
{
    private static MySingleton s_MySingleton = new MySingleton();
    
    private MySingleton()
    {
        Singleton.Register(this);
    }
}


改進版(待修正)
省去轉型與傳遞Type的麻煩。
public class Singleton<T> where T : Singleton<T>
{
    private static T s_Instance;
    private static Dictionary<Type, object> s_Registry =
        new Dictionary<Type, object>();
        
    protected static T Lookup(Type type)
    {
        if (type == null) return null;
        if (s_Registry.ContainsKey(type))
            return s_Registry[type] as T;
        return null;
    }

    public static void Register(T singleton)
    {
        if (singleton == null) return;
        if (s_Registry.ContainsKey(singleton.GetType())) return;
        s_Registry.Add(singleton.GetType(), singleton);
    }

    public static T GetInstance()
    {
        if (s_Instance == null)
            s_Instance = Lookup(typeof(T));
        return s_Instance;
    }
}

public class MySingleton : Singleton<MySingleton>
{
    private static MySingleton s_MySingleton = new MySingleton();

    private MySingleton()
    {
        Singleton<MySingleton>.Register(this);
    }
}

但若遇到多執行緒問題,該怎麼辦呢?
C#的做法很簡單,加個sync root鎖住,並指示編譯器不要對s_Instance最佳化即可。
public class Manager
{
    private static volatile Manager s_Instance;
    private static object s_SyncRoot = new object();
    
    public static Manager Instance
    {
        get
        {
            if (s_Instance == null)
            {
                lock (s_SyncRoot)
                {
                    if (s_Instance)
                        s_Instance = new Manager();
                }
            }
            return s_Instance;
        }
    }
    
    private Manager() { }
}

Unity Component Singleton可能會遇到的其他狀況
有一種可能是會遇到當遊戲程式結束時,某一singleton已經被Destroy了,但在其他地方卻又呼叫了該singleton的Instance,於是又產生了該singleton的Instance,這時候該怎麼辦呢?
  • 不得在所有OnDestroy()、finalizer (destructor)使用任何singleton的Instance,此方法需要規範程式人員。
  • 清除Singleton刪除時,應該要Destroy field,而不是調用Instance,也不可以在finalizer才清除。
  • 給予遊戲程式狀態,當遊戲程式進入結束狀態時,所有Instance均會無效,但這麼做會導致singleton與遊戲邏輯產生相依性問題。

其他 (static class)
若有一種功能確定不應該被繼承擴充,也確定在遊戲中是唯一,也確定功能是經常被用到的,那麼這個情況就不應該考慮使用Singleton,而應該是直接設計成static class。例如Unity的Resources, AssetDatabase等class。此狀況就不符合使用singleton的時機。

參考文獻

公會首頁

主選單
關聯資料