前言
在Unity3D游戏开发中,Monobehaviour单例模式是常见的设计模式之一,具有广泛的应用需求。本篇文章参考自一位外国友人的代码,让我们学习一下他的设计思路吧。
代码
v1.0
using UnityEngine;
/// <summary>
/// 非持久化单例
/// </summary>
/// <typeparam name="T">待封装为单例的类型</typeparam>
/// <remarks>
/// 会在场景卸载或程序退出等销毁机制中进行销毁
/// </remarks>
public abstract class MonoSingleton<T> : MonoBehaviour where T : MonoBehaviour
{
public static T instance { get; private set; }
protected virtual void Awake() { instance = this as T; }
protected virtual void OnApplicationQuit()
{
instance = null;
Destroy(gameObject);
}
}
/// <summary>
/// 持久化单例
/// </summary>
/// <typeparam name="T">待封装为单例的类型</typeparam>
/// <remarks>
/// 会在场景卸载的销毁机制中保留
/// </remarks>
public abstract class MonoSingletonPersistant<T> : MonoSingleton<T> where T : MonoBehaviour
{
protected override void Awake()
{
if (instance != null) Destroy(gameObject);
DontDestroyOnLoad(gameObject);
base.Awake();
}
}
v1.1
using UnityEngine;
/// <summary>
/// 非持久化单例
/// </summary>
/// <typeparam name="T">待封装为单例的类型</typeparam>
/// <remarks>
/// <para>在Awake中争取静态属性的指向,未争取到的实例将被标记为待销毁对象</para>
/// <para>只要对象处于过激活状态,被销毁时就会触发OnDestroy回调,检查静态属性的指向并可能进行重置</para>
/// </remarks>
public abstract class MonoSingleton<T> : MonoBehaviour where T : MonoBehaviour
{
public static T instance { get; private set; }
protected virtual void Awake()
{
if (instance == null) instance = this as T;
else Destroy(this);
}
protected virtual void OnDestroy() { if (this == instance) instance = null; }
}
/// <summary>
/// 持久化单例
/// </summary>
/// <typeparam name="T">待封装为单例的类型</typeparam>
/// <remarks>
/// 在场景卸载过程中保留
/// </remarks>
public abstract class MonoSingletonPersistant<T> : MonoSingleton<T> where T : MonoBehaviour
{
protected override void Awake()
{
base.Awake();
if (this == instance) DontDestroyOnLoad(gameObject);
}
}
v1.2
/// <summary>
/// 非持久化单例
/// </summary>
/// <typeparam name="T">待封装为单例的类型</typeparam>
public abstract class MonoSingleton<T> : MonoBehaviour
where T : MonoBehaviour
{
public static T instance => _instance;
static T _instance;
protected virtual void Awake()
{
if (ReferenceEquals(_instance, null)) _instance = this as T;
else Destroy(this);
}
protected virtual void OnDestroy()
{
if (ReferenceEquals(this, _instance))
_instance = null;
}
}
/// <summary>
/// 非持久化单例
/// </summary>
/// <typeparam name="T">待封装为单例的类型</typeparam>
/// <typeparam name="I">待封装为单例的类型的接口类型</typeparam>
public abstract class MonoSingleton<T, I> : MonoBehaviour
where T : MonoBehaviour, I
{
public static I instance => _instance;
static T _instance;
protected virtual void Awake()
{
if (ReferenceEquals(_instance, null)) _instance = this as T;
else Destroy(this);
}
protected virtual void OnDestroy()
{
if (ReferenceEquals(this, _instance))
_instance = null;
}
}
/// <summary>
/// 持久化单例
/// </summary>
/// <typeparam name="T">待封装为单例的类型</typeparam>
public abstract class MonoSingletonPersistant<T> : MonoSingleton<T>
where T : MonoBehaviour
{
protected override void Awake()
{
base.Awake();
if (ReferenceEquals(this, instance)) DontDestroyOnLoad(gameObject);
}
}
/// <summary>
/// 持久化单例
/// </summary>
/// <typeparam name="T">待封装为单例的类型</typeparam>
/// <typeparam name="I">待封装为单例的类型的接口类型</typeparam>
public abstract class MonoSingletonPersistant<T, I> : MonoSingleton<T, I>
where T : MonoBehaviour, I
{
protected override void Awake()
{
base.Awake();
if (ReferenceEquals(this, instance)) DontDestroyOnLoad(gameObject);
}
}
分析
MonoSingleton
- 遵循Unity Monobehaviour脚本的生命周期,该单例有效调用的生命周期起始于Awake,终止于该单例被显式销毁或场景卸载前;
- 该单例与多数游戏对象相似,与场景共存,场景卸载时则单例销毁,场景加载时则单例创建,不同的地方在于该单例提供了一个静态属性使得在有效调用的生命周期内可以正确访问该单例;
- 该单例应尽量避免跨场景调用,例如存在多个激活的场景,当单例被销毁时,其它场景调用则会导致空引用的异常,除非开发者能很好地管理该单例的调用,但多数时候依旧不建议这么做;
- 当在所激活场景中存在多个该单例所代理的同类型Monobehaviour脚本时,单例的静态属性的指向可能是不明确的,并且这往往只能由开发者人为去避免或者进行统一管理,尽管存在这种情况,往往也不会触发显式异常,因为这仅仅是在逻辑上不符合单例模式的唯一原则,并非不符合编译时规范。
- 在OnApplicationQuit中显式销毁单例,是因为静态属性的生命周期大于该单例有效调用的生命周期,当存在其它调用者在有效调用生命周期外调用单例时会阻止单例的销毁使其滞留内存,从而导致内存泄漏的问题;
- 使用该单例应明确有效调用的生命周期,且保持在该生命周期内进行调用,从而避免引发不必要的异常;
MonoSingletonPersistant
- 该单例在所代理的不同Monobehaviour脚本挂载同一游戏对象的情景中表现不太灵活,除非各Monobehaviour脚本具有共同的生命周期,例如某个单例在之前的场景保持持久化,在该场景特定时机需要销毁不再使用,此时会直接销毁其所挂载的游戏对象,致使其它单例一同销毁;
- 在初次实例化单例时,若同一活动场景中存在多个该单例所代理的Monobehaviour脚本,虽然会显式通过Destroy方法标记待销毁的重复实例,但是却继续执行"DontDestroyOnLoad(gameObject);"和"base.Awake();",通常标记为待销毁的对象我们不应该继续使用,虽然它在本帧最后进行销毁,且不说DontDestroyOnLoad(gameObject)是矛盾的调用,base.Awake()中会导致单例的静态属性指向待销毁对象的Monobehaviour脚本实例,使得原本不会被销毁的Monobehaviour脚本实例的引用丢失;
- 若初次实例化单例时,活动场景仅存在一个其所代理的实例,当进入新的活动场景时如果存在重复实例,也会导致第二点所述问题;
- 因持久化的特性,该单例可以进行跨场景调用;
- 该单例有效调用的生命周期起始于Awake,终止于其显式销毁;
版本改进
版本号 | 改进内容 |
---|---|
v1.1 | 1.单例的静态属性指向采用"先到先得"的原则,取决于同类型实例Awake方法执行的顺序,优先获取静态属性指向的实例在销毁前始终不允许被替换,其它未获取指向的实例均会被销毁,以遵循单例唯一原则; 2.重复实例销毁机制改用销毁组件而非其所挂载的游戏对象,降低销毁所影响的范围; 3.静态属性指向重置仅在OnDestroy回调函数中进行,且仅针对拥有该静态属性指向的实例进行销毁才会进行重置,对于非持久单例通常发生于场景卸载或显式销毁,对于持久化单例通常发生于程序退出或显式销毁; 4.对持久化单例在1.0版本中存在的问题进行了修复。 |
v1.2 | 1.分别添加了以接口类型开放单例对象的单例基类; 2.实例比较方式更改为ReferenceEquals方法; |
... | ...... |
系列文章
......
如果这篇文章对你有帮助,请给作者点个赞吧!