Unity:静态引用在域重新加载(编辑器重新加载)后消失

Unity: Static reference disappears after domain reload (editor reload)

我有一个 C# 脚本可以归结为:

public class SingletonTest : MonoBehaviour
{
    public static SingletonTest Singleton = null;
    public int Value = 42;

    void Start() {
        Singleton = this;
    }
}

起初运行良好,但我的问题是,当我编辑脚本然后单击回到 Unity editor/IDE 时,我得到一整串 NullReferenceExceptionSingletonTest.Singleton.Value 在其他一些 class.

我用谷歌搜索了一下,这个编辑器重新加载似乎触发了一个名为 Domain Reloading (see also 的过程。 显然,重新加载域还会重置所有静态字段,这解释了我的错误消息。我尝试了该页面上建议的一些解决方法,但 none 有效:

using UnityEngine;

public class SingletonTest : MonoBehaviour
{
    public static SingletonTest Singleton = null;
    public int Value = 42;

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] 
    static void StaticStart()
    {
        var arr = FindObjectsOfType<SingletonTest>();
        Debug.Log($"Len arr: {arr.Length}");  // This is 0! :(
        if (arr.Length > 0)
            Singleton = arr[0];
    }

    void Start()
    {
        Singleton = this;
    }

    void OnAfterDeserialize()
    {
        Singleton = this;
    }
}

我可以通过将 Singleton = this 放在 Update() 函数中来让它工作,但这个解决方案很难看,而且仍然在第一个给我一个 NullReferenceException重新加载后的帧。

(请不要建议将 Value 字段设置为静态,这只是一个简化的示例,我 need/want 是一个单例 class)

当 CLR 创建 SingletonTest 对象时,考虑使用私有构造函数强制用一个值初始化静态字段。

尽管 Unity 通常不建议将构造函数与 MonoBehvior 一起使用,因为它们应该是 scripts(以及其他 Mono 和 Unity 怪癖)。我发现这非常适合我的单例用例(例如静态编辑器 Dictionary<T> 保存加载的元数据等...

public class SingletonTest : MonoBehaviour
{
    public static SingletonTest Singleton { get; set; } = null;

    public int Value = 42;

    protected SingletonTest()
    {
        Singleton ??= this;
    }
}

或者考虑避免假设给定的 field/property 永远不会为空。

例如:

void Awake()
{
     // call method example ↓ (null-coalescing operator)
     SingletonTest.Singleton?.Invoke("Something");
     
     // property example
     if(SingletonTest.Singleton != null)
     {
         Debug.Log($"{SingletonTest.Singleton.gameObject.Name}");
     }
}

首先:到目前为止你还没有实现任何单例。

顾名思义,“单例”的主要概念是确保存在一个单一实例。在您的代码中,无法控制另一个实例是否会覆盖实例字段。

Singleton 被滥用后也只有 public 访问权限。

您的方式也很危险:任何其他脚本都可以简单地将字段设置为其他内容。

When/If 我这样做我通常会通过 属性 使用惰性初始化,例如

public class SingletonTest : MonoBehaviour
{
    // Here you store the actual instance
    private static SingletonTest _instance;

    // Only allow read access for others
    public static SingletonTest Instance
    {
        get
        {
            // If instance is assigned and exists return it right away
            if(_instance) return _instance;

            // Otherwise try to find one in the scene
            _instance = FindObjectOfType<SingletonTest>();

            // Found one? Nice return it
            // this would only happen if this getter is called before the 
            // Awake had its chance to set the instance
            if(_instance) return _instance;

            // Otherwise create an instance now 
            // this will only happen if you forgot to put one into your scene
            // it is not active/enabled
            // or it was destroyed for some reason
            _instance = new GameObject("SingletonTest").AddComponent<SingletonTest>();

            return _instance;
        }
    }

    public int Value = 42;

    private void Awake() 
    {
        // If another instance exists already then destroy this one
        // This is the actual Singleton Pattern
        if(_instance && _instance != this)
        {
            Destroy(gameObject);
            return;
        }

        _instance = this;

        // Optional: also make sure this instance survives any scene changes
        DontDestroyOnLoad(gameObject);
    }
}

然后如果你在编辑器中也需要这样的东西,而不是在PlayMode/runtime你可以使用[InitializeOnLoadMethod] (NOT [RuntimeInitializeOnLoadMethod],顾名思义就是运行时/PlayMode).

当然你可以两者都喜欢

#if UNITY_EDITOR
    [UnityEditor.InitializeOnLoadMethod]
#endif
    [RuntimeInitializeOnLoadMethod]
    private static void Init()
    {
        Debug.Log("Initialized", Instance);
    }

Instance 属性 的简单访问已经确保它已被初始化。