找回密码
 立即注册
查看: 194|回复: 5

Unity3D开发中如何用好单例模式?

[复制链接]
发表于 2023-2-23 14:44 | 显示全部楼层 |阅读模式
我本人觉得单例在客户端开发中还是有许多好处的,就我过去的经验来看,只要控制好创建和销毁顺序,单例模式在存储一些全局数据的时候是非常有用的,但是今天看到puzzy3d写的文章:永航科技-技术交流。产生了一些疑惑,求诸位解答
发表于 2023-2-23 14:48 | 显示全部楼层
我给的简单的说法吧。
单例是用来取代以前的全局函数变量的。
相比全局函数,单例不会重名,应用域明确,可以管理生命周期,可封装,可以通过继承扩展(重要!)
和全局函数比除了要多写几个结构体外没有任何缺点。可以完全取代。
所以要用全局函数的情景,就应该用单例。否则就不应该。
顺便说句,@王远易你这非得用MonoBehaviour的想法是极端错误的。你知道这玩意有多慢么?携程为何一定要有?单例本身是个简单高效的东西,你搞成这样……
在开发经验到达一定程度前切勿自以为是进行架构,这样的架构基本都是过度架构,甚至功能本身就是弊大于利的。
unity并不是什么特殊的东西,单例这些用标准写法是没有问题的。至于延迟实际化也没问题,耗时长的实际化可以使用前手动执行和写法无矛盾。
延迟化是一门美妙的技术。比如说你反序列化读取一个表,数据量很大,进游戏时会卡很长一段时间。你把它做成读取特定数据时反序列化那部分内容,这样进游戏就不卡了,读取时只用一小部分内容花的时间也感觉不到。唉,好像我举的这个例子就是云风的代码。
你要明白那些人说话都是有特定条件和语境的。
另外我不认为会有任何一个项目禁用单例,只会出现主程禁止其手下程序员使用单例,因为他判断他们的代码并不应该使用单例。有一点,因为单例通常是全生命周期永不销毁的,所以承担的也是全局功能。如果你做的不是全局功能,你本来就不该用单例。局部功能必定有入口和出口以及自己的生命周期,你就该把需要的东西放自己身上随自己销毁而不是单例出去。硬生生搞个单例管理类也是错误的做法。你管理了又如何?放进入不删不也一样?就为了容易发现错误?那还不如简单的禁用单例。
发表于 2023-2-23 14:51 | 显示全部楼层
利益相关: 曾经是puzzy3d 手下的程序员
puzzy3d 文章里提到的 “禁止” 单例模式的使用,是有上下文的
“开发维护一个持续10-15年,百万量级C++代码”的情况下
项目各模块如果大量使用单例模式,最终必将导致灾难
不使用单例,而是精确维护生命期,遵照项目设计准则,进行设计
是可以避免使用单例,也并不会增加开发工作量
至于题主问的 Unity3D 下单例如何
个人意见:99%的unity3d项目连下一年怎样都不知道...
先怎样快怎样来吧:D
发表于 2023-2-23 14:55 | 显示全部楼层
我觉得要想用好单例,最重要的是想怎么样用少单例。越少越好。
游戏软件处理的对象,一般分“资源对象”与“实例对象”。
        “资源对象”:在程序运行时,储存一种游戏资源的数据。这个对象内的数据用于游戏程序中的渲染,声音播放等。一般而言,资源对象内的资源数据在程序运行时很少变动。比如贴图,3D网格,音频数据等。资源数据往往比较占内存空间。 资源对象可以被多个“实例对象”所共享。以节约内存。
        “实例对象”:代表游戏世界中的任意类型的实体。这个实体有自己的一些属性。并且在游戏时经常会变动。实例对象内部含有一个其共享的“资源对象”的索引(指针)。
这篇文章的这部分我觉得总结的很对,而我对用不用单例的情景划分也是以这个为主要依据的。
有些情景是不得不用单例的:

  • 比如各种无状态Utils,各种辅助函数。本来就是无状态的,还实例化的话简直就不可理喻了。
  • 比如配表数据,当然前提你的配表转数据方案是直接转成代码,这种是静态数据,无并发问题,最好单例。
有些情景是用单例更方便的:

  • 像bundle管理器、声音/音效管理器这种,按文章里的划分,是属于资源对象,是一定在渲染线程中调用的,而且并不会影响gameplay,单例用起来代码字数少很多,何乐不为。
  • 像一些全局的状态管理,跟具体scene无关的,单例也可。
  • 还有就是unity帮你确保线程安全的一些东西,比如log这些的。
那么什么情景下不建议用单例呢?
最简单的,需要跨调用上下文的实例最好不要做成单例。
一般情况用unity写的游戏是无需考虑多线程问题的,基本都是跑在渲染线程的,大家都在直接接触各种GameObject,但是如果你的游戏要支持一些比较特殊的功能,比如重放、比如边玩边更新、比如windows上要锁屏断线重连,那你的逻辑主循环放在渲染线程中是很要命的,写的时候蛋疼不说还会出各种意想不到的bug。
这个时候你相当于要通过一个逻辑线程来驱动整个游戏逻辑,渲染线程只是拿到数据并画出来。这样也更符合游戏开发的模式,摘掉了用unity写游戏=小作坊的帽子。
还有一种情况,就是很多单例之间总会产生依赖的,有依赖那就需要你有人肉维护的创建/销毁顺序严格的一坨逻辑,这种肯定没有非单例的对象依赖树看的那么直白。比如我有一套运行时推翻重来的逻辑,后来人新加一个单例,顺序没加好,动不动就能让运行时推翻重来这类逻辑爆掉,开发期没爆掉那就更可怕了。
单例的优势究竟是什么?
同一个调用上下文内,只有这么一份的某个东西S,我有很多实例需要S,但是这些实例不需要都存对S的一份引用,可以通过一个约定好的入口直接拿到S。
其他的优势我真的想不到了,有想到的朋友请补充。
举个实际例子
还是以之前说的逻辑线程+渲染线程模式为基础:

  • 为了从根本上避免逻辑线程访问到渲染对象,我们的gameplay全放在一个assembly里。
  • 机制来确保这个assembly的gameplay一定是跑在同一个调用上下文中的。
  • 这个assembly内部,不会出现race condition。
那对于gameplay部分来说,assembly internal的单例是可以接受的,也基本上是只要约定好初始化顺序(销毁不考虑),典型如网络模块和gameplay实体管理模块创建时间就是差很多的。public的单例肯定是不能接受的,只要你public了,团队里一定会有人去用你的这个单例的。
对于Assembly-CSharp这个assembly里的逻辑来说,单例用的最多的应该还是我之前提到过的不得不用的情况和用了更方便的情况。
发表于 2023-2-23 15:00 | 显示全部楼层
算是自问自答吧,我从事Unity3D开发有一段时间了,见了不少代码,先讲出自己的看法,算是抛砖引玉吧:
该博客中的代码均出自我的开源项目 : 迷你微信

为什么需要单例模式
游戏中需要单例有以下几个原因:

  • 我们需要在游戏开始前和结束前做一些操作,比如网络的链接和断开,资源的加载和卸载,我们一般会把这部分逻辑放在单例里。
  • 单例可以控制初始化和销毁顺序,而静态变量和场景中的GameObject都无法控制自己的创建和销毁顺序,这样就会造成很多潜在的问题。
  • Unity3D的GameObject需要动态创建。而不是固定在场景里,我们需要使用单例来创建GameObject。
  • Unity3D的场景中的各个GameObject需要从单例中存取数据。
单例的设计原则
在设计单例的时候,我并不建议采取延迟初始化的方案,正如云风所说:
对于单件的处理,采用静态对象和惰性初始化的方案,简直就是 C++ 程序员的陋习。Double Checked Locking is broken,相信很多人都读过了。过于依赖语法糖,通常就会造成这种结果。其实让程序有明显的初始化和退出阶段,是很容易被规划出来的。把单件(singleton) 的处理放在正确的时机,以正确的次序来处理并非难事。
我们应该在程序某处明确定义单例是否被初始化,在初始化执行完毕后再执行正常的游戏逻辑

  • 尽量避免多线程创建单例带来的复杂性
  • 在某处定义了一定的初始化顺序后,可以在游戏结束的时候按照相反的顺序销毁这些单例
设计单例的基类
在Unity中,我们需要一个基类来为所有单例的操作提供统一的接口,同时,我们还要让所有单例继承MonoBehaviour,只有这样才能让单例自由使用协程这一特性。
基类设计如下,代码链接

using System;
using UnityEngine;

namespace MiniWeChat
{
    [RequireComponent(typeof(GameRoot))]
    public class Singleton<T> : MonoBehaviour where T : Singleton<T>
    {
        private static T _instance;

        public static T GetInstance()
        {
            return _instance;
        }

        public void SetInstance(T t)
        {
            if (_instance == null)
            {
                _instance = t;
            }
        }

        public virtual void Init()
        {
            return;
        }

        public virtual void Release()
        {
            return;
        }
    }
}
设计单例的管理类
除了设计基类之外, 还需要设计一个让所有基类初始化和销毁的类,我们把这个类叫做GameRoot,并且把它绑定在一个名为GameRoot的GameObject上,并且把这个GameObject放在游戏进入的Main场景中。

GameRoot类设计如下,代码链接

namespace MiniWeChat
{
    public class GameRoot : MonoBehaviour
    {
        private static GameObject _rootObj;

        private static List<Action> _singletonReleaseList = new List<Action>();

        public void Awake()
        {
            _rootObj = gameObject;
            GameObject.DontDestroyOnLoad(_rootObj);

            StartCoroutine(InitSingletons());
        }

        /// <summary>
        /// 在这里进行所有单例的销毁
        /// </summary>
        public void OnApplicationQuit()
        {
            for (int i = _singletonReleaseList.Count - 1; i >= 0; i--)
            {
                _singletonReleaseList();
            }
        }

        /// <summary>
        /// 在这里进行所有单例的初始化
        /// </summary>
        /// <returns></returns>
        private IEnumerator InitSingletons()
        {
            yield return null;
            // Init Singletons
        }

        private static void AddSingleton<T>() where T : Singleton<T>
        {
            if (_rootObj.GetComponent<T>() == null)
            {
                T t = _rootObj.AddComponent<T>();
                t.SetInstance(t);
                t.Init();

                _singletonReleaseList.Add(delegate()
                {
                    t.Release();
                });
            }
        }

        public static T GetSingleton<T>() where T : Singleton<T>
        {
            T t = _rootObj.GetComponent<T>();

            if (t == null)
            {
                AddSingleton<T>();
            }

            return t;
        }
    }
}

我的单例的缺陷
- 单例的参数没有做成可以在UnityEditor状态下可配置的
- 单例创建销毁完成没有发出消息通知所有对象
这两点大家应该稍作拓展就可解决
如何拓展新的单例
有了以上两个类之后,当我们需要新创建一个类的时候,就可以继承Singleton<T>来创建新的单例,重写Init和Release方法,同时在GameRoot的InitSingleton方法的适当顺序执行AddSingleton<T>方法即可。具体的使用可以参考该类代码链接
发表于 2023-2-23 15:07 | 显示全部楼层
不大会用U3D,偶然点进来的,但我觉得他说的明显有问题,比如第二段:
"对于大型复杂软件开发,软件中对象的初始化顺序和销毁顺序非常重要。单键对象的初始化,有时是在第一次使用此单键类对象的时候才进行。...............造成销毁后又重新初始化。........每个人自行使用Singleton模式,造成这些对象的初始化顺序与销毁不可控。.........没有一个地方统一控制它们的初始化顺序和销毁顺序..............."
这些问题不都是一个合格单例必须解决的问题吗? 连这些都不能避免还算什么单例?
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-6-22 05:08 , Processed in 0.227809 second(s), 25 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表