• 【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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    1. 将脚本挂在至游戏物体中,在Inspector中进行对变量进行配置:
      🟥红色部分:勾选Daemon可使单例跨场景存在。
      🟦蓝色部分:你的自定义变量。
      在这里插入图片描述

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

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

    SceneLoader.Instance.DoSomething(1);
    
    • 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;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79

    注意事项

    • 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?
  • 相关阅读:
    Stable Diffusion WebUI扩展a1111-sd-webui-tagcomplete之Booru风格Tag自动补全功能详细介绍
    termux安装docker
    uniapp H5预览PDF支持手势缩放、分页、添加水印、懒加载、PDF下载
    程序员福音,关于如何使用Markdown写出一份漂亮的简历 —— 程序员简历 | md文档简历制作教程
    SSM项目【Spring MVC、MyBatis、Spring】传智健康项目,关于检查项页面问题
    电子科技大学820笔记【2011年】
    emacs配置教程
    【鸿蒙学习笔记】鸿蒙ArkTS学习笔记
    线程池相关总结
    从crc32到linux内核实现
  • 原文地址:https://blog.csdn.net/mkr67n/article/details/126320876