Shorthand 对于单例实例,在某些 类 中给出 NullException

Shorthand for singleton instance, giving NullException in some classes

我在 Unity 工作,有一个名为 GameManager 的单例,其中包含一个名为 GameSettings 的对象,我想从其他 classes:

public class GameManager : MonoBehaviour {
    public static GameManager instance;
    public GameSettings gameSettings; 

    void Awake() {
        if(instance != null)
            GameObject.Destroy(instance);
        else
            instance = this;
        DontDestroyOnLoad(this);
    }
}

例如,我可以使用 GameManager.instance.gameSettings.offlineMode 从另一个 class 到达它。但我想要一种更简单的写法,让代码更清晰,所以在另一个 class 中我做了:

public class Health : Photon.MonoBehaviour {
    private GameManager gm;

    void Awake(){
        // Get components
        anim = transform.GetChild(0).GetComponent<Animator>();
        unitController = GetComponent<UnitController>();
        gm = GameManager.instance;
    }
}

那我就可以用gm.gameSettings.offlineMode了。这在大多数 classes 中运行良好,但在一个 class 中它不起作用,并给我 NullReferenceException: Object reference not set to an instance of an object.

为什么会发生这种情况,而且只有一个 class?我应该检查什么,"shorthand" 这样的单例实例是个坏主意吗?

发生这种情况是因为 Script Execution Order

https://docs.unity3d.com/Manual/class-MonoManager.html

进入

Edit -> Project Settings -> Script Execution Order

+、select 您的 GameManager 并将其向上拖动 "before" 默认时间,或将值更改为低于 0 的值。现在,您的 GameManagers Awake() 将在其他脚本 Awake()

之前执行

另一种方法是始终在 Awake() 中分配单例,但是当您在其他 类 中获取引用时,您会在 Start()

中执行此操作

我的猜测是您的 Health class 在 GameManager class 之前执行。您可以尝试使用 execution order,或者只是移动将实例分配给 Start() 方法的代码行。

应该是执行顺序的问题吧。某些脚本在实际创建此对象之前尝试访问对象。请注意,除非您不在项目设置中进行设置,否则您无法控制唤醒函数的执行顺序。有很多方法可以解决它。您可以在 Start 函数中分配您的引用,该函数始终在 Awake 之后调用,因此您将确保您想要访问的对象已经创建。另一种方法是更改​​项目设置中的执行顺序(最坏的解决方案)或使用延迟加载。 延迟加载对于单例来说非常方便,但这不是最佳实践。我宁愿建议您避免单例并在一个中心点初始化所有服务。通过构造函数传递依赖比单例模式要清晰得多。你也可以使用一些依赖注入框架,比如 zenject。

此处的脚本执行顺序有误,但我发现您的代码存在另一个问题:

    if(instance != null)
        GameObject.Destroy(instance);
    else
        instance = this;

假设您在某个时候得到两个实例。

  • 第一个副本将 instance 视为 null 并落入 else 语句并将实例设置为其自身
  • 第二个副本将 instance 设置为第一个副本并... 销毁它(然后退出,使 instance 等于 null !).

请注意,此处的 "second copy" 可能是第一个副本出于任何原因第二次执行 Awake()