8.1 单例模式 (Singleton) 8.1 单例模式 (Singleton) 单例模式是软件设计模式中最广为人知和常用的模式之一。它属于创建型模式,旨在确保一个类只有一个实例,并提供一个全局访问点来访问该实例。在Unity3D游戏开发中,单例模式尤其有用,它可以帮助我们管理全局资源、配置信息、游戏管理器等,从而简化代码结构并提高效率。 8.1.1 定义与意图 定义: 单例模式(Singleton Pattern)确保一个类只有一个实例,并提供一个全局访问点来访问该实例。 意图: 控制实例数量: 确保一个类在整个应用程序生命周期中只有一个实例存在。 提供全局访问点: 提供一个统一的入口点,方便全局访问该唯一实例。
单例模式是软件设计模式中最广为人知和常用的模式之一。它属于创建型模式,旨在确保一个类只有一个实例,并提供一个全局访问点来访问该实例。在Unity3D游戏开发中,单例模式尤其有用,它可以帮助我们管理全局资源、配置信息、游戏管理器等,从而简化代码结构并提高效率。
定义:
单例模式(Singleton Pattern)确保一个类只有一个实例,并提供一个全局访问点来访问该实例。
意图:
控制实例数量: 确保一个类在整个应用程序生命周期中只有一个实例存在。
提供全局访问点: 提供一个统一的入口点,方便全局访问该唯一实例。
节约系统资源: 对于某些只需要一个实例的对象,例如配置管理器、日志管理器等,单例模式可以避免重复创建对象,从而节约系统资源。
单例模式的核心结构主要包含以下几个关键部分:
私有构造函数 (Private Constructor): 防止外部通过 new 关键字直接创建类的实例。
静态私有实例变量 (Static Private Instance Variable): 用于保存类的唯一实例。
公共静态访问方法 (Public Static Access Method): 提供全局访问该唯一实例的入口,通常命名为 Instance 或 GetInstance。
我们可以使用 Mermaid 的 graph TD 图来可视化单例模式的结构:
图 8.1.2 单例模式结构图
上图中,SingletonExample 类代表单例类,_instance 是私有静态实例变量,Instance 是公共静态访问方法,SingletonExample() 代表私有构造函数。通过这种结构,我们确保了外部只能通过 Instance 属性来获取单例类的唯一实例。
在 Unity3D 中,实现单例模式有多种方式,常见的包括基于 MonoBehaviour 的单例和普通的 C# 类单例。
基于 MonoBehaviour 的单例适用于需要在 Unity 场景中存在的单例对象,例如游戏管理器、音频管理器等。这种单例可以利用 Unity 的生命周期函数,例如 Awake 和 OnDestroy 来进行初始化和清理操作。
代码示例 8.1.3.1-1:基于 MonoBehaviour 的基本单例
using UnityEngine; public class SingletonMonoBehaviour : MonoBehaviour { private static SingletonMonoBehaviour _instance; // 静态私有实例变量 public static SingletonMonoBehaviour Instance // 公共静态访问属性 { get { if (_instance == null) { _instance = FindObjectOfType<SingletonMonoBehaviour>(); // 查找场景中已存在的单例 if (_instance == null) { GameObject singletonObject = new GameObject("Singleton"); // 创建一个新的 GameObject _instance = singletonObject.AddComponent<SingletonMonoBehaviour>(); // 添加单例组件 } } return _instance; } } protected virtual void Awake() // 使用 protected virtual 允许子类重写 { if (_instance != null && _instance != this) // 确保只有一个实例 { Destroy(gameObject); // 如果已存在实例,销毁自身 return; } _instance = this; // 设置当前实例为单例实例 // DontDestroyOnLoad(gameObject); // 可选:在场景切换时不销毁 } protected virtual void OnDestroy() // 使用 protected virtual 允许子类重写 { if (_instance == this) { _instance = null; // 清理单例实例 } } // 其他单例类的方法和属性 public void DoSomething() { Debug.Log("Singleton MonoBehaviour is doing something!"); } }
代码详解:
_instance (private static): 静态私有变量,用于存储单例的唯一实例。
Instance (public static property): 公共静态属性,作为全局访问单例实例的入口。
get 访问器中,首先检查 _instance 是否为空。
如果为空,则使用 FindObjectOfType<SingletonMonoBehaviour>() 查找场景中是否已存在 SingletonMonoBehaviour 类型的对象。
如果场景中不存在,则创建一个新的 GameObject 并添加 SingletonMonoBehaviour 组件,并将新创建的实例赋值给 _instance。
最后返回 _instance。
Awake() (protected virtual): Unity 的生命周期函数,在脚本实例被启用时调用。
首先检查 _instance 是否已存在且不是当前实例自身。如果是,说明场景中已经存在另一个单例实例,为了保证唯一性,销毁当前 GameObject。
如果 _instance 为空或者当前实例就是 _instance,则将当前实例赋值给 _instance,确保当前实例成为单例实例。
DontDestroyOnLoad(gameObject) (可选):如果希望单例对象在场景切换时不被销毁,可以使用 DontDestroyOnLoad 方法。
OnDestroy() (protected virtual): Unity 的生命周期函数,在脚本实例被销毁时调用。
_instance 置为 null,清理单例实例的引用。DoSomething() (public method): 单例类的其他方法,用于实现具体的功能。
使用示例:
public class SomeOtherScript : MonoBehaviour { void Start() { SingletonMonoBehaviour.Instance.DoSomething(); // 通过 Instance 访问单例方法 } }
优点:
利用 Unity 生命周期: 可以方便地使用 Awake 和 OnDestroy 进行初始化和清理操作。
场景感知: 可以通过 FindObjectOfType 在场景中查找已存在的单例实例。
可扩展性: Awake 和 OnDestroy 方法使用 protected virtual 修饰,允许子类继承并重写,方便扩展单例的功能。
缺点:
依赖于 MonoBehaviour: 必须挂载在 GameObject 上才能使用,不适用于纯粹的数据管理或逻辑处理单例。
查找开销: FindObjectOfType 在大型场景中可能会有一定的性能开销,尤其是在频繁访问单例的情况下。
对于不需要挂载在 GameObject 上,只需要在 C# 代码中使用的单例,可以使用静态类的方式实现。这种方式更加简洁高效,适用于配置管理器、数据管理器等场景。
代码示例 8.1.3.2-1:静态类的基本单例
public class SingletonStaticClass { private static SingletonStaticClass _instance; // 静态私有实例变量 private static readonly object _lock = new object(); // 线程锁,用于线程安全 public static SingletonStaticClass Instance // 公共静态访问属性 { get { if (_instance == null) // 第一次访问时创建实例 { lock (_lock) // 线程锁,保证线程安全 { if (_instance == null) // 双重检查锁定,避免多线程竞争创建多个实例 { _instance = new SingletonStaticClass(); // 创建单例实例 } } } return _instance; } } // 私有构造函数,防止外部直接创建实例 private SingletonStaticClass() { // 初始化操作 Debug.Log("SingletonStaticClass instance created."); } // 其他单例类的方法和属性 public void DoSomething() { Debug.Log("Singleton Static Class is doing something!"); } }
代码详解:
_instance (private static): 静态私有变量,用于存储单例的唯一实例。
_lock (private static readonly object): 静态只读对象,作为线程锁,用于在多线程环境下保证线程安全。
Instance (public static property): 公共静态属性,作为全局访问单例实例的入口。
get 访问器中,首先检查 _instance 是否为空。
如果为空,则使用 lock (_lock) 锁定代码块,保证在多线程环境下只有一个线程可以进入临界区创建实例。
在 lock 代码块内部,进行双重检查锁定 (if (_instance == null)),再次检查 _instance 是否为空,避免多线程竞争导致创建多个实例。
如果 _instance 仍然为空,则创建新的 SingletonStaticClass 实例并赋值给 _instance。
最后返回 _instance。
SingletonStaticClass() (private constructor): 私有构造函数,防止外部通过 new 关键字直接创建 SingletonStaticClass 的实例。
DoSomething() (public method): 单例类的其他方法,用于实现具体的功能。
使用示例:
public class AnotherScript { void Start() { SingletonStaticClass.Instance.DoSomething(); // 通过 Instance 访问单例方法 } }
优点:
简洁高效: 实现方式简单,代码量少,执行效率高。
线程安全: 使用线程锁保证在多线程环境下也能正确创建单例实例。
不依赖 MonoBehaviour: 可以在任何 C# 代码中使用,不局限于 Unity 场景。
缺点:
初始化时机: 静态类的单例实例在第一次访问 Instance 属性时才会被创建,属于懒加载模式。如果初始化过程比较耗时,可能会在第一次访问时造成一定的延迟。
生命周期管理: 静态类的单例生命周期与应用程序的生命周期一致,无法像 MonoBehaviour 单例那样利用 Unity 的生命周期函数进行精细化的管理。
为了提高代码复用性和可维护性,我们可以创建一个泛型单例基类,让其他需要实现单例模式的类继承该基类。
代码示例 8.1.3.3-1:泛型 MonoBehaviour 单例基类
using UnityEngine; public class SingletonMonoBehaviour<T> : MonoBehaviour where T : MonoBehaviour { private static T _instance; public static T Instance { get { if (_instance == null) { _instance = FindObjectOfType<T>(); if (_instance == null) { GameObject singletonObject = new GameObject(typeof(T).Name + " (Singleton)"); _instance = singletonObject.AddComponent<T>(); } } return _instance; } } protected virtual void Awake() { if (_instance != null && _instance != this) { Destroy(gameObject); return; } _instance = this as T; // 将当前实例转换为泛型类型 T // DontDestroyOnLoad(gameObject); } protected virtual void OnDestroy() { if (_instance == this) { _instance = null; } } }
代码详解:
SingletonMonoBehaviour<T> : MonoBehaviour where T : MonoBehaviour: 定义一个泛型类 SingletonMonoBehaviour<T>,继承自 MonoBehaviour,并使用泛型约束 where T : MonoBehaviour,限制泛型类型 T 必须是 MonoBehaviour 或其子类。
_instance (private static T): 静态私有变量,类型为泛型类型 T,用于存储单例的唯一实例。
Instance (public static T property): 公共静态属性,类型为泛型类型 T,作为全局访问单例实例的入口。
Awake() 和 OnDestroy() (protected virtual): 与基本 MonoBehaviour 单例类似,但 Awake() 方法中将当前实例强制转换为泛型类型 T (_instance = this as T;)。
使用示例:
public class GameManager : SingletonMonoBehaviour<GameManager> // 继承泛型单例基类 { // GameManager 特有的方法和属性 public void StartGame() { Debug.Log("Game Started!"); } } public class AudioManager : SingletonMonoBehaviour<AudioManager> // 继承泛型单例基类 { // AudioManager 特有的方法和属性 public void PlaySound(string soundName) { Debug.Log("Playing sound: " + soundName); } } public class UsageScript : MonoBehaviour { void Start() { GameManager.Instance.StartGame(); // 访问 GameManager 单例 AudioManager.Instance.PlaySound("ButtonClick"); // 访问 AudioManager 单例 } }
优点:
代码复用性高: 只需编写一次单例基类,即可让多个类轻松实现单例模式。
类型安全: 使用泛型约束保证继承单例基类的类必须是 MonoBehaviour 或其子类,避免类型错误。
易于维护: 统一的单例逻辑集中在基类中,方便维护和修改。
缺点:
MonoBehaviour 单例基类仍然依赖于 MonoBehaviour,不适用于纯 C# 类单例。类似地,我们也可以创建泛型静态类单例基类,用于实现纯 C# 类单例的复用。
代码示例 8.1.3.4-1:泛型静态类单例基类
public class SingletonStaticClass<T> where T : class, new() { private static T _instance; private static readonly object _lock = new object(); public static T Instance { get { if (_instance == null) { lock (_lock) { if (_instance == null) { _instance = new T(); // 创建泛型类型 T 的实例 } } } return _instance; } } protected SingletonStaticClass() // 受保护的构造函数,防止外部直接创建基类实例 { // 基类初始化操作 } }
代码详解:
SingletonStaticClass<T> where T : class, new(): 定义一个泛型类 SingletonStaticClass<T>,并使用泛型约束 where T : class, new(),限制泛型类型 T 必须是类 (class) 且具有无参构造函数 (new())。
_instance (private static T): 静态私有变量,类型为泛型类型 T,用于存储单例的唯一实例。
Instance (public static T property): 公共静态属性,类型为泛型类型 T,作为全局访问单例实例的入口。
SingletonStaticClass() (protected constructor): 受保护的构造函数,防止外部直接创建 SingletonStaticClass 基类的实例,只能由子类调用。
使用示例:
public class ConfigManager : SingletonStaticClass<ConfigManager> // 继承泛型静态类单例基类 { // ConfigManager 特有的方法和属性 public void LoadConfig() { Debug.Log("Loading Configuration..."); } // 必须提供 public 或 internal 的无参构造函数才能被泛型基类创建实例 public ConfigManager() { // ConfigManager 初始化操作 } } public class DataManager : SingletonStaticClass<DataManager> // 继承泛型静态类单例基类 { // DataManager 特有的方法和属性 public void SaveData() { Debug.Log("Saving Data..."); } public DataManager() { // DataManager 初始化操作 } } public class UsageScript { void Start() { ConfigManager.Instance.LoadConfig(); // 访问 ConfigManager 单例 DataManager.Instance.SaveData(); // 访问 DataManager 单例 } }
优点:
代码复用性高: 与泛型 MonoBehaviour 单例基类类似,提高代码复用性。
类型安全: 使用泛型约束保证继承单例基类的类必须是类且具有无参构造函数。
适用于纯 C# 类: 不依赖 MonoBehaviour,适用于纯粹的数据管理或逻辑处理单例。
缺点:
public 或 internal 的无参构造函数,以便泛型基类可以创建实例。在 Unity3D 游戏开发中,单例模式的应用非常广泛,以下是一些常见的应用场景:
游戏管理器 (GameManager): 管理游戏全局状态、游戏流程、玩家信息、得分、生命值等。通常一个游戏中只需要一个 GameManager 实例来协调整个游戏的运行。
音频管理器 (AudioManager): 负责播放游戏中的音效和背景音乐,控制音量、暂停、停止等。一个游戏中通常只需要一个 AudioManager 实例来统一管理音频播放。
输入管理器 (InputManager): 处理玩家的输入事件,例如键盘、鼠标、触摸等。使用单例 InputManager 可以方便地在任何地方获取和处理玩家的输入。
资源管理器 (ResourceManager): 加载和管理游戏中的资源,例如纹理、模型、音频、预制体等。单例 ResourceManager 可以缓存已加载的资源,提高资源加载效率。
配置管理器 (ConfigManager): 加载和管理游戏的配置信息,例如游戏设置、难度等级、语言设置等。单例 ConfigManager 可以方便地在任何地方访问和修改游戏配置。
UI 管理器 (UIManager): 管理游戏中的 UI 界面,例如显示/隐藏 UI 元素、处理 UI 事件等。单例 UIManager 可以统一管理 UI 界面的显示和逻辑。
网络管理器 (NetworkManager): 处理游戏中的网络通信,例如连接服务器、发送和接收数据等。单例 NetworkManager 可以统一管理网络连接和数据传输。
日志管理器 (LogManager): 记录游戏运行时的日志信息,方便调试和错误排查。单例 LogManager 可以统一管理日志输出。
对象池管理器 (ObjectPoolManager): 管理对象池,用于复用游戏对象,减少对象创建和销毁的开销。单例 ObjectPoolManager 可以统一管理对象池的创建和使用。
总结:
凡是需要在整个游戏生命周期中保持唯一实例,并且需要全局访问的类,都可以考虑使用单例模式。单例模式可以有效地管理全局资源和状态,简化代码结构,提高开发效率。
优点:
控制实例数量: 保证一个类只有一个实例,避免资源浪费和状态混乱。
全局访问点: 提供一个全局访问点,方便在任何地方访问单例实例。
节约系统资源: 对于只需要一个实例的对象,可以节约系统资源。
易于扩展: 可以在单例类中添加新的方法和属性,扩展单例的功能。
缺点:
违反单一职责原则: 单例类通常承担了过多的职责,既要负责自身的逻辑,又要负责自身的创建和管理,可能导致类变得臃肿复杂。
紧耦合: 由于单例模式提供了全局访问点,容易导致代码之间过度耦合,不利于代码的维护和测试。
难以单元测试: 由于单例实例是全局唯一的,单元测试时难以隔离和模拟单例对象,可能会增加单元测试的难度。
隐藏依赖关系: 单例模式的使用可能会隐藏类之间的依赖关系,使得代码的依赖关系变得不清晰。
可能导致全局状态: 单例模式容易被滥用,导致大量的全局状态,使得程序的状态难以管理和预测。
总结:
单例模式是一个非常有用的设计模式,但在使用时也需要谨慎。要权衡其优缺点,避免滥用单例模式,只在真正需要全局唯一实例的场景下使用。
在使用单例模式时,为了避免其缺点并发挥其优点,可以遵循以下最佳实践:
明确使用场景: 只在真正需要全局唯一实例的场景下使用单例模式,例如全局管理器、资源管理器等。避免在不必要的场景下使用单例模式。
控制单例职责: 尽量控制单例类的职责,避免单例类承担过多的功能,保持单例类的简洁和专注。
延迟初始化: 对于静态类的单例,可以使用延迟初始化(懒加载)的方式,在第一次访问 Instance 属性时才创建实例,避免不必要的资源消耗。
线程安全: 在多线程环境下,需要考虑单例模式的线程安全性,可以使用线程锁等机制来保证线程安全。
避免过度依赖: 尽量避免代码过度依赖单例实例,减少代码之间的耦合度。可以使用接口或依赖注入等方式来解耦。
单元测试考虑: 在设计单例类时,要考虑单元测试的需要,尽量使单例类易于测试。可以使用接口或 Mock 对象等方式来模拟单例对象进行单元测试。
考虑替代方案: 在某些场景下,单例模式可能不是最佳选择。可以考虑使用其他设计模式或架构模式来替代单例模式,例如服务定位器模式、依赖注入模式、工厂模式等。
总结:
合理地使用单例模式可以提高代码效率和可维护性,但滥用单例模式可能会导致代码质量下降。因此,在使用单例模式时,要充分理解其优缺点,并遵循最佳实践,才能更好地发挥单例模式的作用。
理解和掌握单例模式,并合理地应用到 Unity3D 游戏开发中,可以帮助开发者构建更高效、更易于维护的游戏系统。但是,也需要注意单例模式的缺点,避免滥用单例模式,才能真正发挥其优势,提升游戏开发的效率和质量。