【Unity记录】编写一个超实用的单例模式模板

本文内容

阅读须知:

  • 阅读本文建议提前了解Unity中的单例模式

本文将介绍:

  • 简单介绍单例模式
  • 编写在Unity中使用的单例模式,它将满足以下需求:
泛型实现 全局访问 删除重复 场景切换保留 不存在时创建 线程安全
✅(可选) ✅(虽然不推荐)

单例模式

单例模式提供了一种可由全局访问并取得唯一对象的操作。在Unity中,常用作某些数据共享的情景,如:需要被各种对象广泛访问的“唯一管理对象”。
单例模式与静态类在用途上相似,但更为强大,它最大的优势是遵循面向对象程序设计的理念:

  • 它可以实现接口
  • 它可以作为接口参数传入函数
  • 它可以实现继承
  • ⭐继承MonoBehaviour,意味着它可以作为预制体承载预定义物体!(这是Unity中使用单例模式的主要原因)

单例模式虽然强大,但不应该滥用。因为它与全局变量类似,有着污染变量的风险,滥用单例模式往往会造成程序耦合,降低可维护性(成为屎山)。因此在使用单例模式前应考虑是否能通过一般OOP的思想实现。

使用方法

在查看脚本之前,先关注一下这个脚本的使用。

  1. 继承单例脚本Singleton,并实现你的逻辑。
    ⚠️注意:如果需要实现MonoBehaviour.Awake方法,需要重写并调用父类方法。
    比如下面这个例子:
class SceneLoader : Singleton<SceneLoader>
{
	//自定义变量
    [SerializeField]
    Image transitionScreen;
    [SerializeField]
    GameObject loadingIndicator;
    [SerializeField]
    Slider loadingBar;

	//注意:实现Awake需要重写
    protected override void Awake()
    {
    	//调用父类方法
        base.Awake();
    	//......你的逻辑
    }

    private void Start()
    {
    	//......你的逻辑
    }
    
    //其它逻辑
    //......
}
  1. 将脚本挂在至游戏物体中,在Inspector中进行对变量进行配置:
    红色部分:勾选Daemon可使单例跨场景存在。
    蓝色部分:你的自定义变量。
    【Unity记录】编写一个超实用的单例模式模板_第1张图片

  2. 将该游戏物体加入任意需要使用的场景中。
    无须担心在场景切换时会产生重复,因为该脚本会自动清除重复单例。
    这意味着可在任意场景都添加同一个单例物体,这在场景测试时会非常方便。

  3. 在其它脚本中通过Instance变量访问单例。

SceneLoader.Instance.DoSomething(1);

单例脚本

实现思路

通过控制受保护的静态对象Instance的赋值,从而实现唯一性。

需要在第一次被访问前初始化,因此一共有两种初始化的可能:

  • 在Awake()调用前就需要访问Instance

    • 先查找启用的Singleton物体
      0个:创建空物体,加入Singleton脚本组件,并赋值至Instance
      1个:设为Instance
      2+个:移除其余Destroy(singletonList[i])
    • 返回Instance
  • Awake()中初始化

    • 当前脚本属于第几个出现物体?
      不存在:单例将在Instance被第一次调用时创建
      第1个:将自己设为Instance
      第2+个:销毁自己Destroy(gameObject)

如此一来,可以保证在场景初始化后,把任何重复的Singleton对象都移除,只保留唯一一个实例;而对于未部署至场景的单例,会在Instance被第一次访问时生成(⚠️但不推荐这么做,因为这样生成的单例物体没有初始化数据!)

此外,在访问Instance时可加入lock()语句,保证线程安全。

具体代码

  • 为了防止退出时创建单例引起错误,使用变量quitting阻止退出时的创建
  • 每个操作都加入了Debug.LogWarning()帮助追踪问题,实际使用时若无问题可删除
using UnityEngine;

public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    [SerializeField]
    private bool daemon = true;

    private static bool quitting;
    private static T instance;

    private static readonly object _lock = new object();

    public static T Instance
    {
        get
        {
            lock (_lock)
            {
                if (instance == null)
                {
                    if (quitting)
                    {
                        return null;
                    }
                    var instances = FindObjectsOfType<T>();
                    if (instances.Length > 0)
                    {
                        //只要第一个
                        instance = instances[0];
                        Debug.LogWarning($"[{typeof(T).Name} get]: found 1 Singleton({typeof(T).Name}). assigning to instance...");

                        //只允许场景出现一个T类物体,其余是没用的。
                        //这需要主动Destroy多余的物体
                        //否则多余物体的Update仍会被Unity调用,可能造成错误
                        for (var i = 1; i < instances.Length; i++)
                        {
                            Debug.LogWarning($"[{typeof(T).Name} get]: more than 1 Singleton({typeof(T).Name}) exist, destroying No.{i} from the scene...");
                            Destroy(instances[i]);
                        }
                    }
                    else
                    {
                        Debug.LogWarning($"[{typeof(T).Name} get]: Singleton({typeof(T).Name}) not existing, will create one on the scene...");
                        //创建一个
                        new GameObject($"[{typeof(T).Name} get]: Singleton({typeof(T).Name})").AddComponent<T>();
                    }
                }
                return instance;
            }
        }
    }

    protected virtual void Awake()
    {
        lock (_lock)
        {
            if (instance == null)
            {
                instance = this as T;
                Debug.LogWarning($"[{typeof(T).Name} Awake]: no {typeof(T).Name} exists, assigning self...");
                if (daemon)
                {
                    DontDestroyOnLoad(gameObject);
                }
            }
            else if (instance != this)
            {
                Debug.LogWarning($"[{typeof(T).Name} Awake]: another {typeof(T).Name} already exists, destorying self...");
                Destroy(gameObject);
            }
        }
    }

    private void OnApplicationQuit()
    {
        quitting = true;
    }
}

注意事项

  • FindObjectsOfType() 以及 new GameObject()在非主线程调用时会报错,如果存在多线程情形,请确保Instance为null时在主线程调用这些方法。
  • 子类实现Unity消息Awake()时需要重写override该方法并调用base.Awake(),否则父类Awake()逻辑不会被调用。
  • quitting在Unity新的Enter Play Mode Options勾选✅时不会生效。

参考:

  1. In Unity, how do I correctly implement the singleton pattern?

你可能感兴趣的:(Unity记录,单例模式,unity,游戏引擎)