💡
一句话理解:单例模式保证某个类型在约定范围内只有一个实例,并提供统一访问入口。它适合真正全局且唯一的服务,但不应该成为所有管理器的默认写法。

它解决什么问题

当音频、存档或游戏会话服务需要被多个系统共享时,重复实例会造成状态冲突。单例把“唯一性”和“访问入口”集中管理。
在 Unity 中要先明确实例的生命周期:
  • 场景级:切换场景后销毁,不使用 DontDestroyOnLoad
  • 应用级:跨场景存在,显式使用 DontDestroyOnLoad
  • 纯 C# 服务:不依赖 MonoBehaviour,可由启动流程或依赖注入容器创建

Unity 实现

调用方可以使用 GameSession.Instance.AddScore(10)。如果对象的初始化顺序不确定,应由启动场景负责创建,或在访问前显式检查 Instance,不要依赖脚本执行顺序碰运气。

关键细节

  • DontDestroyOnLoad 不是单例模式的一部分,只用于跨场景生命周期。
  • Unity API 通常只能在主线程调用;普通 MonoBehaviour 单例无需套用桌面应用中的“双重检查锁”。
  • 关闭 Domain Reload 时,静态字段可能跨 Play 保留。可以用 RuntimeInitializeOnLoadMethod 重置静态状态。
  • 泛型基类能减少重复代码,但也会把生命周期策略强加给所有子类,使用前应确认这些服务真的共享同一策略。

什么时候不该用

  • 需要多个存档槽、玩家或会话实例
  • 单元测试需要轻松替换依赖
  • 服务之间依赖关系复杂
  • 只是为了避免在 Inspector 中传引用
这些场景更适合构造函数注入、VContainer 等依赖注入方案,或用 ScriptableObject 保存共享配置。

优缺点

优点:访问简单、唯一性明确、适合少量应用级服务。
代价:隐藏依赖、全局可变状态、初始化顺序问题,以及测试和复用困难。

小结

先确定生命周期,再决定是否使用单例。真正的判断标准不是“它是不是 Manager”,而是“它是否必须唯一、全局共享,并且值得承担全局状态的成本”。