在Unity中使用单例时一般会选择使用跟随Unity生命周期的MonoBehaviour对象,一般情况下的MonoSingleton对象都是DontDestroyOnLoad处理不会销毁的对象。

切换场景时两个mono对象的生命周期。

https://www.imxqy.com/imagegx/unity/monosingleton_1.jpg

有一些时候为了在场景内更好的可视化调试和修改需要将MonoSingleton的对象放置于场景中而不是动态的添加,这样会出现一个问题,如果重新加载该场景或者其他存在此类型的场景时,将会造成两个MonoSingleton同时在场的情况,为了处理这种情况可以有两种方式:

  • 切换场景时保持旧实例,销毁新实例
  • 切换场景时将将旧实例数据移动到新实例后,销毁旧实例

https://www.imxqy.com/imagegx/unity/monosingleton_2.jpg

为了代码的复用性而实现的MonoSingleton,为了实现该功能不得不让派生类抛弃Awake函数来初始化。

这里使用OnMoveConstructor作为派生类初始化函数,Awake则负责MonoSingleton的初始化工作。

先尝试KeepInstance,如果存在存在旧实例说明当前对象是新实例,则销毁自己并结束函数;

如果没有允许保持旧实例或者没有旧实例,那就说明当前实例是需要初始化并留下来的,调用MoveInstance,将可能存在的旧实例传给新实例,派生类可以在OnMoveConstructor中对新实例进行初始化。

using System;
using UnityEngine;

public abstract class MonoSingleton<T>
    : MonoBehaviour where T : MonoSingleton<T>
{
    [SerializeField]
    private bool IsDontDestroyOnInit = true;
    [SerializeField]
    private bool IsKeepInstance = true;

    private static T mInstance = null;

    public static T Instance
    {
        get
        {
            if (mInstance == null)
            {
                //如果instance为空并且可以在场景里找到该类型,那么就设置为单例,否则新建对象
                mInstance = GameObject.FindObjectOfType(typeof(T)) as T;
                if (mInstance == null)
                {
                    GameObject go = new GameObject("__m_" + typeof(T).Name);
                    mInstance = go.AddComponent<T>();
                }
                if (mInstance.IsDontDestroyOnInit)
                {
                    if (object.ReferenceEquals(mInstance.transform.parent, null))
                    {
                        DontDestroyOnLoad(mInstance.gameObject);
                    }
                }
            }
            return mInstance;
        }
    }
    public static bool HasInstance
    {
        get => mInstance != null;
    }

    protected virtual void Awake()
    {
        //允许保持旧实例并且可以保持旧实例就直接返回
        if (IsKeepInstance && KeepInstance())
        {
            //keep
            return;
        }
        else
        {
            //这里有两种情况,无论是否存在旧实例,都会调用OnMoveConstructor虚函数
            //如存在旧实例则为移动,不存在旧实例(形参为null)则为新实例
            MoveInstance();
        }

        if (IsDontDestroyOnInit)
        {
            if (this.transform.parent == null)
            {
                DontDestroyOnLoad(gameObject);
            }
        }
    }

    protected virtual void OnMoveConstructor(T oldInstance)
    {

    }

    public static T GetInstance()
    {
        return Instance;
    }
    private void MoveInstance()
    {
        var old = mInstance;
        mInstance = this as T;
        OnMoveConstructor(old);
        if (old != null) //销毁旧实例
        {
            Destroy(old.gameObject);
        }
    }

    /// <summary>
    /// 保持实例,如果有新实例则会销毁,并返回true
    /// </summary>
    /// <returns></returns>
    private bool KeepInstance()
    {
        if (HasInstance)
        {
            Destroy(gameObject);
            return true;
        }
        return false;
    }

    protected virtual void OnDestroy()
    {
        if (HasInstance)
        {
            if (mInstance == this)
            {
                mInstance = null;
            }
        }
    }

}