本文内容
阅读须知:
- 阅读本文建议提前了解Unity中的单例模式
本文将介绍:
- 简单介绍单例模式
- 编写在Unity中使用的单例模式,它将满足以下需求:
泛型实现 | 全局访问 | 删除重复 | 场景切换保留 | 不存在时创建 | 线程安全 |
---|---|---|---|---|---|
✅ | ✅ | ✅ | ✅(可选) | ✅(虽然不推荐) | ✅ |
单例模式
单例模式提供了一种可由全局访问并取得唯一对象的操作。在Unity中,常用作某些数据共享的情景,如:需要被各种对象广泛访问的“唯一管理对象”。
单例模式与静态类在用途上相似,但更为强大,它最大的优势是遵循面向对象程序设计的理念:
- 它可以实现接口
- 它可以作为接口参数传入函数
- 它可以实现继承
- ⭐继承MonoBehaviour,意味着它可以作为预制体承载预定义物体!(这是Unity中使用单例模式的主要原因)
单例模式虽然强大,但不应该滥用。因为它与全局变量类似,有着污染变量的风险,滥用单例模式往往会造成程序耦合,降低可维护性(成为屎山)。因此在使用单例模式前应考虑是否能通过一般OOP的思想实现。
使用方法
在查看脚本之前,先关注一下这个脚本的使用。
- 继承单例脚本Singleton,并实现你的逻辑。
⚠️注意:如果需要实现MonoBehaviour.Awake方法,需要重写并调用父类方法。
比如下面这个例子:
class SceneLoader : Singleton<SceneLoader>
{
//自定义变量
[SerializeField]
Image transitionScreen;
[SerializeField]
GameObject loadingIndicator;
[SerializeField]
Slider loadingBar;
//注意:实现Awake需要重写
protected override void Awake()
{
//调用父类方法
base.Awake();
//......你的逻辑
}
private void Start()
{
//......你的逻辑
}
//其它逻辑
//......
}
-
将脚本挂在至游戏物体中,在Inspector中进行对变量进行配置:
🟥红色部分:勾选Daemon可使单例跨场景存在。
🟦蓝色部分:你的自定义变量。
-
将该游戏物体加入任意需要使用的场景中。
无须担心在场景切换时会产生重复,因为该脚本会自动清除重复单例。
这意味着可在任意场景都添加同一个单例物体,这在场景测试时会非常方便。 -
在其它脚本中通过
Instance
变量访问单例。
SceneLoader.Instance.DoSomething(1);
单例脚本
实现思路
通过控制受保护的静态对象Instance
的赋值,从而实现唯一性。
需要在第一次被访问前初始化,因此一共有两种初始化的可能:
-
在Awake()调用前就需要访问
Instance
- 先查找启用的Singleton物体
0个:创建空物体,加入Singleton脚本组件,并赋值至Instance
1个:设为Instance
2+个:移除其余Destroy(singletonList[i])
- 返回
Instance
- 先查找启用的Singleton物体
-
Awake()中初始化
- 当前脚本属于第几个出现物体?
不存在:单例将在Instance
被第一次调用时创建
第1个:将自己设为Instance
第2+个:销毁自己Destroy(gameObject)
- 当前脚本属于第几个出现物体?
如此一来,可以保证在场景初始化后,把任何重复的Singleton对象都移除,只保留唯一一个实例;而对于未部署至场景的单例,会在Instance
被第一次访问时生成(⚠️但不推荐这么做,因为这样生成的单例物体没有初始化数据!)
此外,在访问Instance时可加入lock()
语句,保证线程安全。
具体代码
- 为了防止退出时创建单例引起错误,使用变量
quitting
阻止退出时的创建 - 每个操作都加入了
Debug.LogWarning()
帮助追踪问题,实际使用时若无问题可删除
using UnityEngine;
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
[SerializeField]
private bool daemon = true;
private static bool quitting;
private static T instance;
private static readonly object _lock = new object();
public static T Instance
{
get
{
lock (_lock)
{
if (instance == null)
{
if (quitting)
{
return null;
}
var instances = FindObjectsOfType<T>();
if (instances.Length > 0)
{
//只要第一个
instance = instances[0];
Debug.LogWarning($"[{typeof(T).Name} get]: found 1 Singleton({typeof(T).Name}). assigning to instance...");
//只允许场景出现一个T类物体,其余是没用的。
//这需要主动Destroy多余的物体
//否则多余物体的Update仍会被Unity调用,可能造成错误
for (var i = 1; i < instances.Length; i++)
{
Debug.LogWarning($"[{typeof(T).Name} get]: more than 1 Singleton({typeof(T).Name}) exist, destroying No.{i} from the scene...");
Destroy(instances[i]);
}
}
else
{
Debug.LogWarning($"[{typeof(T).Name} get]: Singleton({typeof(T).Name}) not existing, will create one on the scene...");
//创建一个
new GameObject($"[{typeof(T).Name} get]: Singleton({typeof(T).Name})").AddComponent<T>();
}
}
return instance;
}
}
}
protected virtual void Awake()
{
lock (_lock)
{
if (instance == null)
{
instance = this as T;
Debug.LogWarning($"[{typeof(T).Name} Awake]: no {typeof(T).Name} exists, assigning self...");
if (daemon)
{
DontDestroyOnLoad(gameObject);
}
}
else if (instance != this)
{
Debug.LogWarning($"[{typeof(T).Name} Awake]: another {typeof(T).Name} already exists, destorying self...");
Destroy(gameObject);
}
}
}
private void OnApplicationQuit()
{
quitting = true;
}
}
注意事项
FindObjectsOfType()
以及new GameObject()
在非主线程调用时会报错,如果存在多线程情形,请确保Instance
为null时在主线程调用这些方法。- 子类实现Unity消息
Awake()
时需要重写override
该方法并调用base.Awake()
,否则父类Awake()
逻辑不会被调用。 quitting
在Unity新的Enter Play Mode Options勾选✅时不会生效。