【问题标题】:Unity: Static reference disappears after domain reload (editor reload)Unity:域重新加载后静态引用消失(编辑器重新加载)
【发布时间】:2021-05-26 18:39:02
【问题描述】:

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

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

    void Start() {
        Singleton = this;
    }
}

起初运行良好,但我的问题是,当我编辑脚本然后单击返回 Unity 编辑器/IDE 时,我在其他类中得到一整串 NullReferenceExceptions 用于行 SingletonTest.Singleton.Value .

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

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 字段设为静态,这只是一个简化示例,我确实需要/想要一个单例类)

【问题讨论】:

  • 您在运行时或编辑器中需要这个?
  • 我发现将这个问题作为一个副本关闭是不合适的。我明确设置了字段,但重新加载域的过程会重新构建对象,但不是专门构建它们的静态字段,这远非显而易见。

标签: c# unity3d static unity3d-editor


【解决方案1】:

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

虽然 Unity 通常不建议使用带有 MonoBehvior 的构造函数,因为它们应该是 脚本(以及其他 Mono 和 Unity 怪癖)。我发现这非常适合我的单例用例(例如静态编辑器 Dictionary&lt;T&gt;s 保存已加载的元数据等......

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

    public int Value = 42;

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

或者考虑避免假设给定的字段/属性永远不会为空。

例如:

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

【讨论】:

  • 非常感谢您的回答!不幸的是,在构造函数中使用 this 似乎给了我一条错误消息:UnityException: get_gameObject is not allowed to be called from a MonoBehaviour constructor (or instance field initializer), call it in Awake or Start instead. Called from MonoBehaviour 'NwPlayer' on game object 'PlayerCap'. See "Script Serialization" page in the Unity Manual for further details.
  • 似乎该消息实际上是由我还将 MLAPI NetworkObject 脚本附加到对象引起的。当我解决它时,一切似乎都正常。谢谢!
  • 你不应该在派生自MonoBehaviour的类型中实现任何构造函数! Unity 在内部创建实例,并且在它们上使用自定义构造函数可能会比它解决的问题更多;)
  • 你能举一个具体的例子说明为什么它很危险吗?我没有注意到有任何损坏。
  • @xjcl *so far ;) 问题是Monobehaviours 的构造函数可能会被调用很多次。如前所述,它们是由 Unity 在内部创建的,并且需要大量附加信息(例如,关联的 .transform.gameObject 等),如果类型是通过您实现的构造函数创建的,则可能会丢失这些信息(因此也使用 new禁止”)。由于编辑器中正在进行连续的(反)序列化,它也可能被多次调用。在您的用例中,这可能没问题,但总的来说它非常危险,应该完全避免。
【解决方案2】:

首先:到目前为止,您还没有实现任何 Singleton。

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

然后,Singleton 仅被滥用以具有公共访问权限。

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

当/如果我做这样的事情我通常会通过属性使用延迟初始化,例如

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);
    }
}

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

当然你可以两个都喜欢

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

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

【讨论】:

  • 好吧,让我解释一下我要完成的工作。我在一个场景中有这个脚本的一个实例,所以当场景加载时,Unity 会自动创建它的一个实例,我只想引用它。
  • @xjcl 那么我对此的回答有什么问题? ;) 从您的问题来看,您是否需要在运行时或编辑模式下还不太清楚......域重新加载仅在编辑器中发生,例如在重新编译脚本之后,听起来您希望在编辑模式下发生这种情况
  • 我的问题和标题不是多次提到编辑器模式吗?
  • 嗯……这不是我假设的吗?这个答案也适用于编辑模式,因为UnityEditor.InitializeOnLoadMethod 将在每次和任何域重新加载后调用该方法
猜你喜欢
  • 1970-01-01
  • 2014-10-05
  • 1970-01-01
  • 2015-12-30
  • 1970-01-01
  • 2017-03-15
  • 1970-01-01
  • 2013-11-15
  • 2019-06-14
相关资源
最近更新 更多