开发中都会遇到类型单例的需求,在本文,笔者将为大家整理一个通用的MonoBehavour单例类:
写在前面:
很多时候,笔者都是图简单,使用下面这个方式做的单例:
public static DemoClass Get{get;private set;}
void Awake()
{
Get = this;
}
对,你没看错,就是这么简单粗暴!但是,单例需求量上来了,这么写也不是个办法,也学习别人整理一个通用的单例类来好了。
MonoSingleton:
using System;
using UnityEngine;
[AutoSingleton(true)]
public class MonoSingleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance;
private static object _lock = new object();
public static T Instance
{
get
{
Type _type = typeof(T);
if (_destroyed)
{
Debug.LogWarningFormat("[Singleton]【{0}】已被标记为销毁,返 Null!", _type.Name);
return (T)((object)null);
}
lock (_lock)
{
if (_instance == null)
{
_instance = (T)FindObjectOfType(_type);
if (FindObjectsOfType(_type).Length > 1)
{
Debug.LogErrorFormat("[Singleton]类型【{0}】存在多个实例.", _type.Name);
return _instance;
}
if (_instance == null)
{
object[] customAttributes = _type.GetCustomAttributes(typeof(AutoSingletonAttribute), true);
AutoSingletonAttribute autoAttribute = (customAttributes.Length > 0) ? (AutoSingletonAttribute)customAttributes[0] : null;
if (null == autoAttribute || !autoAttribute.autoCreate)
{
Debug.LogWarningFormat("[Singleton]欲访问单例【{0}】不存在且设置了非自动创建~", _type.Name);
return (T)((object)null);
}
GameObject go = null;
if (string.IsNullOrEmpty(autoAttribute.resPath))
{
go = new GameObject(_type.Name);
_instance = go.AddComponent<T>();
}
else
{
go = Resources.Load<GameObject>(autoAttribute.resPath);
if (null != go)
{
go = GameObject.Instantiate(go);
}
else
{
Debug.LogErrorFormat("[Singleton]类型【{0}】ResPath设置了错误的路径【{1}】", _type.Name, autoAttribute.resPath);
return (T)((object)null);
}
_instance = go.GetComponent<T>();
if (null == _instance)
{
Debug.LogErrorFormat("[Singleton]指定预制体未挂载该脚本【{0}】,ResPath【{1}】", _type.Name, autoAttribute.resPath);
}
}
}
}
return _instance;
}
}
}
protected virtual void Awake()
{
if (_instance != null && _instance.gameObject != gameObject)
{
Debug.Log("创造了新的克隆体!");
if (Application.isPlaying)
{
GameObject.Destroy(gameObject);
}
else
{
GameObject.DestroyImmediate(gameObject);
}
}
else
{
_instance = GetComponent<T>();
if (!transform.parent) //Unity 只允许最最根节点的 游戏对象不销毁加载。
{
DontDestroyOnLoad(gameObject);
}
OnInit();
}
}
public static void DestroyInstance()
{
if (_instance != null)
{
GameObject.Destroy(_instance.gameObject);
}
_destroyed = true;
_instance = (T)((object)null);
}
/// <summary>
/// 清除 _destroyed 锁
/// </summary>
public static void ClearDestroy()
{
DestroyInstance();
_destroyed = false;
}
private static bool _destroyed = false;
/// <summary>
/// 当播放停止时,Unity 会以随机顺序销毁对象
/// 若单例 gameObject 先于其他对象销毁,不排除这个单例再次被调用的可能性。
/// 故而在编辑器模式下,即便播放停止了,也可能会生成一个 gameObject 对象残留在编辑器场景中。
/// 所以,此方法中加把锁,避免不必要的单例调用
/// </summary>
public void OnDestroy()
{
if (_instance != null && _instance.gameObject == base.gameObject)
{
_instance = (T)((object)null);
_destroyed = true;
}
}
/// <summary>Awake 初始化完成之后 </summary>
public virtual void OnInit()
{
Debug.Log("OnInit");
}
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class AutoSingletonAttribute : Attribute
{
public bool autoCreate; //是否自动创建单例
public string resPath; //从指定的预制体路径生成单例
public AutoSingletonAttribute(bool _autoCreate, string _resPath = "")
{
this.autoCreate = _autoCreate;
this.resPath = _resPath;
}
}
Tips:
这个单例能够解决以下痛点:
- 使用了泛型 T 实现逻辑复用,避免重复撰写单例逻辑。
- 使用了自定义属性,加入单例的实例化规则:
- 当单例获取失败,用户可决定是否动态创建。
- 当单例获取失败,用户可决定是否从Resource目录下获取预制体动态创建,或者创建空游戏对象。
- 使用了 _destoryed 锁,避免了编辑器模式下停止播放时生成单例游戏对象的bug。
- Awake 中新增 “gamaObject 实例清除” 逻辑, 确保了来回跳场景也不会反序列化出多余的单例对象。
使用示例
using UnityEngine;
[AutoSingleton(true,"Player")] //第一个参数表示如获取不到则自动创建单例,第二个代表希望加载的预制体路径
public class Player : MonoSingleton<Player>
{
void Start ()
{
Debug.Log("Player 闪亮登场!!");
}
}
扩展阅读:
- QFrameWork中的单例实现
- Graphy 中的单例(链接整个拷贝哈)
https://github.com/Tayx94/graphy/blob/master/Assets/Tayx/Graphy - Ultimate Stats Monitor/Scripts/Util/G_Singleton.cs
2019年04月30日补充:
其实还有一个痛点需要解决,那就是指定单例挂载的游戏对象在hierarchy 中的路径.
这个需求我们同样使用属性来标注和传递数据。这样一来,你就能把单例对象挂载在你指定的深度的层级下了。
于是高度可配置的单例用起来就更灵活方便和通用了…
PS:这个痛点点 QFramwork的单例已经考虑到了哦。