阅读须知:
本文将介绍:
泛型实现 | 全局访问 | 删除重复 | 场景切换保留 | 不存在时创建 | 线程安全 |
---|---|---|---|---|---|
✅ | ✅ | ✅ | ✅(可选) | ✅(虽然不推荐) | ✅ |
单例模式提供了一种可由全局访问并取得唯一对象的操作。在Unity中,常用作某些数据共享的情景,如:需要被各种对象广泛访问的“唯一管理对象”。
单例模式与静态类在用途上相似,但更为强大,它最大的优势是遵循面向对象程序设计的理念:
单例模式虽然强大,但不应该滥用。因为它与全局变量类似,有着污染变量的风险,滥用单例模式往往会造成程序耦合,降低可维护性(成为屎山)。因此在使用单例模式前应考虑是否能通过一般OOP的思想实现。
在查看脚本之前,先关注一下这个脚本的使用。
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
Instance
Instance
Destroy(singletonList[i])
Instance
Awake()中初始化
Instance
被第一次调用时创建Instance
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时在主线程调用这些方法。Awake()
时需要重写override
该方法并调用base.Awake()
,否则父类Awake()
逻辑不会被调用。quitting
在Unity新的Enter Play Mode Options勾选✅时不会生效。