代码
using UnityEngine;
public abstract class MonoSingleton<T> : MonoBehaviour where T : MonoBehaviour
{
/// <summary>
/// 是否是跨场景的全局单例:true跨场景单例,false场景局部单例
/// </summary>
[SerializeField] private bool global;
private bool isInited;
private bool isDisposed;
private static T instance;
public static T GetInstance()
{
if (instance == null)
{
instance = new GameObject(typeof(T).Name).AddComponent<T>();
}
return instance;
}
private void Awake()
{
isDisposed = false;
if (instance == null)
{
instance = gameObject.GetComponent<T>();
isInited = true;
OnAwake();
}
else
{
isInited = false;
DestroyImmediate(gameObject);
return;
}
}
private void Start()
{
if (global)
{
DontDestroyOnLoad(gameObject);
}
OnStart();
}
private void OnDestroy()
{
if (!isInited)
{
return;
}
if (!isDisposed)
{
OnDispose();
}
instance = null;
}
/// <summary>
/// 单例销毁方法
/// </summary>
public void Dispose()
{
OnDispose();
isDisposed = true;
DestroyImmediate(gameObject);
}
/// <summary>
/// Awake回调方法
/// </summary>
protected virtual void OnAwake() { }
/// <summary>
/// Start回调方法
/// </summary>
protected virtual void OnStart() { }
/// <summary>
/// Destroy回调方法
/// </summary>
protected virtual void OnDispose() { }
}
使用
- 设置
global = true
即可成为全局单例; - 设置
global = false
即可成为场景局部单例; - 覆写虚函数
OnStart()
、OnAwake()
、OnDispose()
实现Unity消息Start()
、Awake()
、OnDestroy()
。
生命周期说明
- 全局单例:在任何场景中保持唯一,如果在多个场景中挂在了多个此单例脚本的单例物体,以第一个场景初始化的单例为准,以场景中第一个初始化的单例为准。之后切换场景加载同单例脚本的单例物体或者在同一场景生成已存在的单例脚本物体,在其初始化时就会被销毁。如果没有在任何场景中挂在单例脚本物体,在其他脚本调用此单例时,会在此场景中生成单例物体,并在后面场景中保持唯一。
- 局部场景单例:在某一场景中保持唯一,如果在多个场景中挂在了多个此单里脚本的单例物体,以场景中第一个初始化的单例物体为准,此场景后面初始化的单例会立即销毁,切换场景时单例物体同其他非单例游戏物体一样一起销毁,保证此场景此单例生命周期结束,让下一场景的同意局部单例能够初始化生成。如果没有在任何场景中挂在单例脚本物体,在其他脚本调用此单例时,会在此场景中生成单例物体,并在此场景中保持唯一。
- 单例的销毁:单例可以通过手动调用
Dispose()
方法销毁,手动调用销毁方法会先执行OnDispose()
,再进行销毁程序,再执行OnDistroy()
Unity消息。而通过场景切换或退出游戏销毁则会先执行OnDestroy()
Unity消息,再执行OnDispose()
。
注意事项
- 问题描述:若其他Mono脚本的
OnDestroy()
中或单例脚本的OnDispose()
中引用了单例,则在退出游戏或切换场景时有可能会报错Some objects were not cleaned up when closing the scene. (Did you spawn new GameObjects from OnDestroy?)
,这是因为Unity销毁物体的顺序不一样,可能销毁单例物体在Mono脚本物体之前,Mono脚本销毁时调用了一次单例,此时单例已被销毁,而调用又生成了新的单例,造成了在OnDestroy()
中生成新物体的错误。 - 解决办法:第一种办法是理清调用关系,在切换场景或退出游戏前,按调用顺序依次手动调用单例
Dispose()
方法,也就是在销毁前进行事件解绑之类的事情;第二种办法是尽量避免在游戏物体的OnDestroy()
或OnDispose()
中引用单例。 - 规避与规范:这种错误在Mono脚本物体单向引用时还能通过手动调用
OnDispose()
解决,若出现在OnDispose()
或OnDestroy()
中单例互相引用的情况,则没有解决办法,一定要避免这种情况发生。尽量在销毁单例时能够清楚调用关系,且保持单向的调用关系。所有在OnDispose()
或OnDestroy()
中被调用的单例通过手动调用OnDispose()
销毁。 - 代码示例:
public class GameObjectManager : MonoSingleton<GameObjectManager>
{
protected override void OnStart()
{
UIMainGameManager.GetInstance().OnAction += OnAction;
GameManager.GetInstance().Gm_ActionB += OnActionB;
}
protected override void OnDispose()
{
//出现对其他单例的引用,需手动注销单例
UIMainGameManager.GetInstance().OnAction -= OnAction;
GameManager.GetInstance().Gm_ActionB -= OnActionB;
}
private void OnAction(){}
private void OnActionB(){}
public class UIMainGameManager : MonoSingleton<UIMainGameManager>
{
public Action OnAction;
protected override void OnStart()
{
GameManager.GetInstance().Gm_ActionA += ActionA;
}
protected override void OnDispose()
{
//出现对其他单例的引用,需手动注销单例
GameManager.GetInstance().Gm_ActionA -= ActionA;
}
private void ActionA(){}
}
public class GameManager : MonoSingleton<GameManager>
{
public event Action Gm_ActionA;
public event Action Gm_ActionB;
private bool isMannalQuit;
private void OnApplicationQuit()
{
if (isMannalQuit)
return;
//程序退出时自动注销单例,OnApplication()执行在OnDestroy()之前
GameObjectManager.GetInstance().Dispose();
UIMainGameManager.GetInstance().Dispose();
}
private void OnGUI()
{
if (GUI.Button(new Rect(0, 0, 200, 30), "Next Scene"))
{
//切换场景时手动注销单例
GameObjectManager.GetInstance().Dispose();
UIMainGameManager.GetInstance().Dispose();
SceneManager.GetInstance().LoadScene($"NextScene");
}
if (GUI.Button(new Rect(0, 30, 200, 30), "Exit"))
{
//退出时手动注销单例
isMannalQuit = true;
GameObjectManager.GetInstance().Dispose();
UIMainGameManager.GetInstance().Dispose();
SceneManager.GetInstance().Dispose();
Dispose();
Application.Quit();
}
}
}