在Unity中,如何正确实现单例模式?


36

我看过一些视频和教程,这些视频和教程主要用于Unity中创建单例对象GameManager,它们似乎使用了不同的方法来实例化和验证单例。

是否有一种正确的或更确切的首选方法?

我遇到的两个主要示例是:

第一

public class GameManager
{
    private static GameManager _instance;

    public static GameManager Instance
    {
        get
        {
            if(_instance == null)
            {
                _instance = GameObject.FindObjectOfType<GameManager>();
            }

            return _instance;
        }
    }

    void Awake()
    {
        DontDestroyOnLoad(gameObject);
    }
}

第二

public class GameManager
{
    private static GameManager _instance;

    public static GameManager Instance
    {
        get
        {
            if(_instance == null)
            {
                instance = new GameObject("Game Manager");
                instance.AddComponent<GameManager>();
            }

            return _instance;
        }
    }

    void Awake()
    {
        _instance = this;
    }
}

我可以看到两者之间的主要区别是:

第一种方法将尝试导航游戏对象堆栈,以找到其实例GameManager,即使这种情况仅发生(或应该只发生)一次,因为随着开发过程中场景尺寸的增加,这种情况似乎可能非常不优化。

同样,第一种方法将在应用程序更改场景时将对象标记为不删除,从而确保在场景之间保留对象。第二种方法似乎不遵守这一点。

第二种方法似乎很奇怪,因为在getter中实例为null的情况下,它将创建一个新的GameObject并为其分配一个GameManger组件。但是,如果不先将GameManager组件已经附加到场景中的对象上,就无法运行此操作,这使我有些困惑。

是否还有其他建议的方法,或者上述两种方法的混合?有很多有关单例的视频和教程,但是它们都相差很大,因此很难对两者进行比较,因此可以得出结论,哪种方法是最佳/首选方法。


GameManager应该做什么?它一定是GameObject吗?
bummzack '16

1
这实际上不是问题GameManager应该做什么,而是如何确保对象只有一个实例以及实施该实例的最佳方法的问题。
CaptainRedmuff

这个教程很好地解释了如何实现单例unitygeek.com/unity_c_singleton,我希望它是有用的
Rahul Lalit

Answers:


30

这取决于,但通常我使用第三种方法。您所使用的方法的问题在于,如果从一开始就包含了该对象,则不会将其从树中删除,并且仍然可以通过实例化太多的调用来创建它们,这可能会使事情真正混乱。

public class SomeClass : MonoBehaviour {
    private static SomeClass _instance;

    public static SomeClass Instance { get { return _instance; } }


    private void Awake()
    {
        if (_instance != null && _instance != this)
        {
            Destroy(this.gameObject);
        } else {
            _instance = this;
        }
    }
}

这两个实现的问题在于它们不会破坏以后创建的对象。它可能会起作用,但可能会给工作人员带来麻烦,这可能会导致非常难以调试错误。确保检入“唤醒”中是否已有实例,如果存在,则销毁新实例。


2
OnDestroy() { if (this == _instance) { _instance = null; } }如果要在每个场景中使用不同的实例,则可能还需要。
Dietrich Epp

而不是销毁()游戏对象,您应该引发一个错误。
Doodlemeat

2
可能吧 您可能想要记录它,但是我不认为您应该提出错误,除非您尝试执行非常具体的操作。我可以想象有很多实例,引发错误实际上会导致更多问题,然后会解决。
PearsonArtPhoto '17

您可能要注意,MonoBehaviour是Unity的英式拼写(“ MonoBehavior”不会编译-我一直都这样做);否则,这是一些不错的代码。
迈克尔·埃里克·奥伯林

我知道我来晚了,但只想指出一点,这个答案的单例无法在编辑器重新加载后幸存下来,因为static Instance属性被擦除了。在下面的答案之一中都找不到一个示例,或wiki.unity3d.com/index.php/Singleton(虽然可能已过时,但似乎在我的实验中起作用)
Jakub Arnold

24

快速摘要:

                 Create object   Removes scene   Global    Keep across
               if not in scene?   duplicates?    access?   Scene loads?

Method 1              No              No           Yes        Yes

Method 2              Yes             No           Yes        No

PearsonArtPhoto       No              Yes          Yes        No
Method 3

因此,如果您只关心全局访问,那么这三者都能满足您的需求。使用Singleton模式在我们是否需要延迟实例化,强制唯一性或全局访问方面可能有点模棱两可,因此请务必仔细考虑为什么要使用Singleton,并选择能够正确实现这些功能的实现。而不是只需要一个就使用全部三个标准。

(例如,如果我的游戏将始终具有GameManager,也许我不在乎延迟实例化-也许这只是我关心的具有保证的存在性和唯一性的全局访问-在这种情况下,静态类非常简洁地为我提供了这些功能,无需考虑场景加载)

...但是绝对不要使用书面的方法1。使用Method2 / 3的Awake()方法可以更轻松地跳过“查找”,并且如果我们要让经理跨场景使用,那么很可能需要重复杀死,以防万一我们在两个场景之间加载了已经有经理的场景。


1
注意:应该可以将所有三种方法结合起来以创建具有所有四种功能的第四种方法。
Draco18s '02

3
这个答案的重点不是“您应该寻找能够完成所有任务的Singleton实现”,而是“您应该从该Singleton中确定您真正想要的功能,并选择提供这些功能的实现-即使该实现是一点也不单身”
DMGregory

DMGregory很好。我并不是要提出“将它们全部粉碎”的意图,而是“这些功能都无法阻止它们在一个班级中一起工作”。即“此答案的重点不是建议选择一个。
Draco18s '18

17

Singleton我所知道的Unity 通用模式的最佳实现(当然)是我自己的。

它可以做所有事情,而且它整洁高效

Create object        Removes scene        Global access?               Keep across
if not in scene?     duplicates?                                       Scene loads?

     Yes                  Yes                  Yes                     Yes (optional)

其他优点:

  • 这是线程安全的
  • 通过确保在之后不能创建单例,它避免了与退出应用程序时获取(创建)单例实例有关的错误OnApplicationQuit()。(它使用单个全局标志来执行此操作,而不是每个单例类型都有自己的标志)
  • 它使用Unity 2017的Mono Update(大致等效于C#6)。(但可以很容易地将其改编为古代版本)
  • 它带有一些免费的糖果!

由于共享很重要,所以这里是:

public abstract class Singleton<T> : Singleton where T : MonoBehaviour
{
    #region  Fields
    [CanBeNull]
    private static T _instance;

    [NotNull]
    // ReSharper disable once StaticMemberInGenericType
    private static readonly object Lock = new object();

    [SerializeField]
    private bool _persistent = true;
    #endregion

    #region  Properties
    [NotNull]
    public static T Instance
    {
        get
        {
            if (Quitting)
            {
                Debug.LogWarning($"[{nameof(Singleton)}<{typeof(T)}>] Instance will not be returned because the application is quitting.");
                // ReSharper disable once AssignNullToNotNullAttribute
                return null;
            }
            lock (Lock)
            {
                if (_instance != null)
                    return _instance;
                var instances = FindObjectsOfType<T>();
                var count = instances.Length;
                if (count > 0)
                {
                    if (count == 1)
                        return _instance = instances[0];
                    Debug.LogWarning($"[{nameof(Singleton)}<{typeof(T)}>] There should never be more than one {nameof(Singleton)} of type {typeof(T)} in the scene, but {count} were found. The first instance found will be used, and all others will be destroyed.");
                    for (var i = 1; i < instances.Length; i++)
                        Destroy(instances[i]);
                    return _instance = instances[0];
                }

                Debug.Log($"[{nameof(Singleton)}<{typeof(T)}>] An instance is needed in the scene and no existing instances were found, so a new instance will be created.");
                return _instance = new GameObject($"({nameof(Singleton)}){typeof(T)}")
                           .AddComponent<T>();
            }
        }
    }
    #endregion

    #region  Methods
    private void Awake()
    {
        if (_persistent)
            DontDestroyOnLoad(gameObject);
        OnAwake();
    }

    protected virtual void OnAwake() { }
    #endregion
}

public abstract class Singleton : MonoBehaviour
{
    #region  Properties
    public static bool Quitting { get; private set; }
    #endregion

    #region  Methods
    private void OnApplicationQuit()
    {
        Quitting = true;
    }
    #endregion
}
//Free candy!

这很扎实。从编程背景和非Unity背景出发,您能解释一下为什么不在构造函数中而不是在Awake方法中管理单例吗?您可能可以想象,对于那里的任何开发人员来说,看到在构造函数之外强制执行的Singleton都是
令人毛骨悚然的事

1
@netpoetica简单。Unity不支持构造函数。这就是为什么您看不到在继承的MonoBehaviour任何类中使用构造函数的原因,并且我相信大体上直接由Unity使用的任何类。
XenoRo

我不确定我该如何利用它。这是否意味着只是成为该类的家长?声明后SampleSingletonClass : SingletonSampleSingletonClass.Instance返回SampleSingletonClass does not contain a definition for Instance
Ben I.

@BenI。您必须使用通用Singleton<>类。这就是为什么泛型是基Singleton类的子代的原因。
XenoRo

哦,当然!很明显 我不确定为什么没有看到。= /
Ben I.

6

我只想补充一点,DontDestroyOnLoad如果您希望单身人士在场景中持续存在,致电可能会很有用。

public class Singleton : MonoBehaviour
{ 
    private static Singleton _instance;

    public static Singleton Instance 
    { 
        get { return _instance; } 
    } 

    private void Awake() 
    { 
        if (_instance != null && _instance != this) 
        { 
            Destroy(this.gameObject);
            return;
        }

        _instance = this;
        DontDestroyOnLoad(this.gameObject);
    } 
}

非常方便。我正要对@PearsonArtPhoto的回复发表评论,以问这个确切的问题:]
CaptainRedmuff

5

另一个选择可能是将类分为两部分:Singleton组件的常规静态类,以及充当Singleton实例的控制器的MonoBehaviour。这样,您可以完全控制单例的构造,并且该构造将在整个场景中保持不变。这也使您可以将控制器添加到可能需要单例数据的任何对象,而不必在整个场景中查找特定组件。

public class Singleton{
    private Singleton(){
        //Class initialization goes here.
    }

    public void someSingletonMethod(){
        //Some method that acts on the Singleton.
    }

    private static Singleton _instance;
    public static Singleton Instance 
    { 
        get { 
            if (_instance == null)
                _instance = new Singleton();
            return _instance; 
        }
    } 
}

public class SingletonController: MonoBehaviour{
   //Create a local reference so that the editor can read it.
   public Singleton instance;
   void Awake(){
       instance = Singleton.Instance;
   }
   //You can reference the singleton instance directly, but it might be better to just reflect its methods in the controller.
   public void someMethod(){
       instance.someSingletonMethod();
   }
} 

太好了!
CaptainRedmuff

1
我在理解此方法时遇到麻烦,您能否在这个问题上再扩展一点。谢谢。
hex

3

这是我下面的单例抽象类的实现。这是它如何根据4个标准进行叠加

             Create object   Removes scene   Global    Keep across
           if not in scene?   duplicates?    access?   Scene loads?

             No (but why         Yes           Yes        Yes
             should it?)

与此处的其他一些方法相比,它还有其他一些优点:

  • 它不使用FindObjectsOfType哪个是性能杀手
  • 它的灵活性在于无需在游戏过程中创建新的空游戏对象。您只需在编辑器中(或在游戏过程中)将其添加到您选择的游戏对象中即可。
  • 它是线程安全的

    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;
    
    public abstract class Singleton<T> : MonoBehaviour where T : Singleton<T>
    {
        #region  Variables
        protected static bool Quitting { get; private set; }
    
        private static readonly object Lock = new object();
        private static Dictionary<System.Type, Singleton<T>> _instances;
    
        public static T Instance
        {
            get
            {
                if (Quitting)
                {
                    return null;
                }
                lock (Lock)
                {
                    if (_instances == null)
                        _instances = new Dictionary<System.Type, Singleton<T>>();
    
                    if (_instances.ContainsKey(typeof(T)))
                        return (T)_instances[typeof(T)];
                    else
                        return null;
                }
            }
        }
    
        #endregion
    
        #region  Methods
        private void OnEnable()
        {
            if (!Quitting)
            {
                bool iAmSingleton = false;
    
                lock (Lock)
                {
                    if (_instances == null)
                        _instances = new Dictionary<System.Type, Singleton<T>>();
    
                    if (_instances.ContainsKey(this.GetType()))
                        Destroy(this.gameObject);
                    else
                    {
                        iAmSingleton = true;
    
                        _instances.Add(this.GetType(), this);
    
                        DontDestroyOnLoad(gameObject);
                    }
                }
    
                if(iAmSingleton)
                    OnEnableCallback();
            }
        }
    
        private void OnApplicationQuit()
        {
            Quitting = true;
    
            OnApplicationQuitCallback();
        }
    
        protected abstract void OnApplicationQuitCallback();
    
        protected abstract void OnEnableCallback();
        #endregion
    }

可能是一个愚蠢的问题,但你为什么做OnApplicationQuitCallbackOnEnableCallback作为abstract,而不只是空洞的virtual方法呢?至少在我的情况下,我没有任何退出/启用逻辑,并且有一个空的覆盖会很脏。但是我可能会缺少一些东西。
雅库布·阿诺德

@JakubArnold我已经有一段时间没有看这个了,但是乍一看似乎你是对的,作为虚拟方法会更好
aBertrand

其实@JakubArnold我想我还记得我的思维从当年:我希望让那些知道谁用这个作为一个组成部分,他们可以利用OnApplicationQuitCallbackOnEnableCallback:具有它作为虚拟方法那种使得它不太明显。也许有点奇怪,但据我所知,那是我的理性。
aBertrand

2

在Unity中使用Singleton实际上是一种伪官方方法。这里是解释,基本上是创建一个Singleton类,并使您的脚本从该类继承。


请避免在仅包含答案的情况下回答问题,方法是在答案中至少包含您希望读者从链接中收集的信息摘要。这样,如果链接变得不可用,答案仍然有用。
DMGregory

2

我也将为后代制定实施方案。

void Awake()
    {
        if (instance == null)
            instance = this;
        else if (instance != this)
            Destroy(gameObject.GetComponent(instance.GetType()));
        DontDestroyOnLoad(gameObject);
    }

对我而言,这一行Destroy(gameObject.GetComponent(instance.GetType()));非常重要,因为一旦我在场景中的另一个gameObject上留下了单例脚本,并且整个游戏对象都被删除了。如果该组件已经存在,则只会销毁该组件。


1

我写了一个单例类,使创建单例对象变得容易。它是一个MonoBehaviour脚本,因此您可以使用协同程序。它基于Unity Wiki的这篇文章,稍后我将添加从Prefab创建它的选项。

因此,您无需编写Singleton代码。只需下载此Singleton.cs基类,将其添加到您的项目中,然后创建扩展它的单例:

public class MySingleton : Singleton<MySingleton> {
  protected MySingleton () {} // Protect the constructor!

  public string globalVar;

  void Awake () {
      Debug.Log("Awoke Singleton Instance: " + gameObject.GetInstanceID());
  }
}

现在,您的MySingleton类是单例,您可以通过Instance调用它:

MySingleton.Instance.globalVar = "A";
Debug.Log ("globalVar: " + MySingleton.Instance.globalVar);

这是完整的教程:http : //www.bivis.com.br/2016/05/04/unity-reusable-singleton-tutorial/

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.