前言
在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);
}
}
测试
测试采用的是Unity Test Framework(UTF)
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
public class MonoSingletonTest
{
// 非持久单例派生类
public class A : MonoSingleton<A>, IMonoBehaviourTest
{
public bool IsTestFinished { get; private set; }
protected override void Awake()
{
base.Awake();
IsTestFinished = true;
}
}
// 持久性单例派生类
public class B : MonoSingletonPersistant<B>, IMonoBehaviourTest
{
public bool IsTestFinished { get; private set; }
protected override void Awake()
{
base.Awake();
IsTestFinished = true;
}
}
/*
测试用例通过表示实际结果与预期结果相同,不通过则表示不相同,前置测试表示
必须先启动的测试用例。
*/
// 测试 MonoSingleton 单例的生命周期起始点
// 预期结果:Awake
// 前置测试:无
[UnityTest, Order(0)]
public IEnumerator MonoSingleton_LifecycleStartNode_Test()
{
yield return new MonoBehaviourTest<A>(false);
Assert.IsTrue(A.instance != null);
}
// 测试 MonoSingletonPersistant 单例的生命周期起始点
// 预期结果:Awake
// 前置测试:无
[UnityTest, Order(0)]
public IEnumerator MonoSingletonPersistant_LifecycleStartNode_Test()
{
yield return new MonoBehaviourTest<B>();
Assert.IsTrue(B.instance != null);
}
// 测试 MonoSingleton 对于重复实例的处理
// 预期结果:静态属性指向最先执行Awake的实例,其它重复实例销毁
// 前置测试:MonoSingleton_LifecycleStartNode_Test
[UnityTest]
public IEnumerator MonoSingleton_RepeatSingleton_Test()
{
int instanceID = A.instance.GetInstanceID();
yield return new MonoBehaviourTest<A>(false);
LogAssert.Expect(UnityEngine.LogType.Log, instanceID.ToString());
LogUtility.Log(A.instance.GetInstanceID());
}
// 测试 MonoSingletonPersistant 对于重复实例的处理
// 预期结果:静态属性指向最先执行Awake的实例,其它重复实例被销毁
// 前置测试:MonoSingletonPersistant_LifecycleStartNode_Test
[UnityTest]
public IEnumerator MonoSingletonPersistant_RepeatSingleton_Test()
{
int instanceID = B.instance.GetInstanceID();
yield return new MonoBehaviourTest<B>();
LogAssert.Expect(UnityEngine.LogType.Log, instanceID.ToString());
yield return null;
LogUtility.Log(B.instance.GetInstanceID());
}
// 测试 MonoSingleton 单例的生命周期终点
// 预期结果:场景卸载后被销毁
// 前置测试:MonoSingleton_LifecycleStartNode_Test
[UnityTest, Order(1)]
public IEnumerator MonoSingleton_LifecycleEnd_Test()
{
var ao = SceneManager.LoadSceneAsync(1);
ao.completed += a =>
{
LogUtility.Log(SceneManager.GetActiveScene().name);
};
yield return ao;
Assert.IsTrue(A.instance == null);
}
// 测试 MonoSingletonPersistant 单例的生命周期终点
// 预期结果:场景卸载后保留,静态属性指向不变
// 前置测试:MonoSingletonPersistant_LifecycleStartNode_Test
[UnityTest, Order(1)]
public IEnumerator MonoSingletonPersistant_LifecycleEnd_Test()
{
int instanceID = B.instance.GetInstanceID();
var ao = SceneManager.LoadSceneAsync(1);
ao.completed += a =>
{
LogUtility.Log(SceneManager.GetActiveScene().name);
};
yield return ao;
LogAssert.Expect(UnityEngine.LogType.Log, instanceID.ToString());
yield return null;
LogUtility.Log(B.instance.GetInstanceID());
Assert.IsTrue(B.instance != null);
}
// 测试 MonoSingleton 在新的激活场景中的情况
// 预期结果:静态属性指向重置为 null,若新场景中存在实例则指向该实例
// 前置测试:MonoSingleton_LifecycleStartNode_Test
[UnityTest, Order(1)]
public IEnumerator MonoSingleton_ActiveSceneChanged_Test()
{
int instanceID = A.instance.GetInstanceID();
var ao = SceneManager.LoadSceneAsync(1);
ao.completed += a =>
{
LogUtility.Log(SceneManager.GetActiveScene().name);
LogUtility.Log(A.instance);
GameObject go = new GameObject("MonoSingleton", typeof(A));
};
yield return ao;
LogAssert.Expect(UnityEngine.LogType.Log, "Null");
LogAssert.Expect(UnityEngine.LogType.Log, "True");
yield return null;
LogUtility.Log(A.instance.GetInstanceID() != instanceID);
Assert.IsTrue(A.instance != null);
}
// 测试 MonoSingletonPersistant 在新的激活场景中的情况
// 预期结果:静态属性指向的实例不变,销毁其它重复实例
// 前置测试:MonoSingletonPersistant_LifecycleStartNode_Test
[UnityTest, Order(1)]
public IEnumerator MonoSingletonPersistant_ActiveSceneChanged_Test()
{
int instanceID = B.instance.GetInstanceID();
var ao = SceneManager.LoadSceneAsync(1);
ao.completed += a =>
{
LogUtility.Log(SceneManager.GetActiveScene().name);
GameObject go = new GameObject("MonoSingletonPersistant", typeof(B));
};
yield return ao;
LogAssert.Expect(UnityEngine.LogType.Log, "True");
yield return null;
LogUtility.Log(B.instance.GetInstanceID() == instanceID);
Assert.IsTrue(B.instance != null);
}
// 测试 MonoSingleton 在多个激活场景中的情况
// 预期结果:静态属性所指向实例不变,其它重复实例被销毁
// 前置测试:MonoSingleton_LifecycleStartNode_Test
[UnityTest, Order(1)]
public IEnumerator MonoSingleton_MultipleActiveScenes_Test()
{
int instanceID = A.instance.GetInstanceID();
var ao = SceneManager.LoadSceneAsync(1, LoadSceneMode.Additive);
ao.completed += a =>
{
LogUtility.Log(SceneManager.GetActiveScene().name);
GameObject go = new GameObject("MonoSingleton", typeof(A)); // 创建新场景中的单例实例
};
yield return ao;
LogAssert.Expect(UnityEngine.LogType.Log, "True");
yield return null;
LogUtility.Log(A.instance.GetInstanceID() == instanceID);
Assert.IsTrue(A.instance != null);
}
// 测试 MonoSingletonPersistant 在多个激活场景中的情况
// 预期结果:静态属性指向的实例不变,销毁其它重复实例
// 前置测试:MonoSingletonPersistant_LifecycleStartNode_Test
[UnityTest, Order(1)]
public IEnumerator MonoSingletonPersistant_MultipleActiveScenes_Test()
{
int instanceID = B.instance.GetInstanceID();
var ao = SceneManager.LoadSceneAsync(1, LoadSceneMode.Additive);
ao.completed += a =>
{
LogUtility.Log(SceneManager.GetActiveScene().name);
GameObject go = new GameObject("MonoSingletonPersistant", typeof(B));
};
yield return ao;
LogAssert.Expect(UnityEngine.LogType.Log, "True");
yield return null;
LogUtility.Log(B.instance.GetInstanceID() == instanceID);
Assert.IsTrue(B.instance != null);
}
}
测试用例
测试用例通过表示实际结果与预期结果相同,不通过则表示不相同,前置测试表示必须先启动的测试用例。
v1.0
用例ID | 用例名称 | 预期结果 | 前置测试 | 是否通过 |
---|---|---|---|---|
1 | 测试 MonoSingleton 单例的生命周期起始点 | Awake | 无 | 通过 |
2 | 测试 MonoSingletonPersistant 单例的生命周期起始点 | Awake | 无 | 通过 |
3 | 测试 MonoSingleton 对于重复实例的处理 | 静态属性指向最先执行Awake的实例,其它重复实例销毁 | 1 | 通过 |
4 | 测试 MonoSingletonPersistant 对于重复实例的处理 | 静态属性指向最先执行Awake的实例,其它重复实例销毁 | 2 | 未通过 |
5 | 测试 MonoSingleton 单例的生命周期终点 | 场景卸载后被销毁,静态属性重置为 null | 1 | 通过 |
6 | 测试 MonoSingletonPersistant 单例的生命周期终点 | 场景卸载后保留,静态属性指向不变 | 2 | 通过 |
7 | 测试 MonoSingleton 在新的激活场景中的情况 | 静态属性指向重置为 null,若新场景中存在实例则指向该实例 | 1 | 通过 |
8 | 测试 MonoSingletonPersistant 在新的激活场景中的情况 | 静态属性指向的实例不变,销毁其它重复实例 | 2 | 未通过 |
9 | 测试 MonoSingleton 在多个激活场景中的情况 | 静态属性所指向实例不变,其它重复实例被销毁 | 1 | 通过 |
10 | 测试 MonoSingletonPersistant 在多个激活场景中的情况 | 静态属性指向的实例不变,销毁其它重复实例 | 2 | 未通过 |
v1.1
用例ID | 用例名称 | 预期结果 | 前置测试 | 是否通过 |
---|---|---|---|---|
1 | 测试 MonoSingleton 单例的生命周期起始点 | Awake | 无 | 通过 |
2 | 测试 MonoSingletonPersistant 单例的生命周期起始点 | Awake | 无 | 通过 |
3 | 测试 MonoSingleton 对于重复实例的处理 | 静态属性指向最先执行Awake的实例,其它重复实例销毁 | 1 | 通过 |
4 | 测试 MonoSingletonPersistant 对于重复实例的处理 | 静态属性指向最先执行Awake的实例,其它重复实例销毁 | 2 | 通过 |
5 | 测试 MonoSingleton 单例的生命周期终点 | 场景卸载后被销毁,静态属性重置为 null | 1 | 通过 |
6 | 测试 MonoSingletonPersistant 单例的生命周期终点 | 场景卸载后保留,静态属性指向不变 | 2 | 通过 |
7 | 测试 MonoSingleton 在新的激活场景中的情况 | 静态属性指向重置为 null,若新场景中存在实例则指向该实例 | 1 | 通过 |
8 | 测试 MonoSingletonPersistant 在新的激活场景中的情况 | 静态属性指向的实例不变,销毁其它重复实例 | 2 | 通过 |
9 | 测试 MonoSingleton 在多个激活场景中的情况 | 静态属性所指向实例不变,其它重复实例被销毁 | 1 | 通过 |
10 | 测试 MonoSingletonPersistant 在多个激活场景中的情况 | 静态属性指向的实例不变,销毁其它重复实例 | 2 | 通过 |
分析
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版本中存在的问题进行了修复。 |
...... | ...... |
系列文章
......
如果这篇文章对你有帮助,请给作者点个赞吧!