为什么ScriptableObject资产中嵌套的资源引用没有被加载?

4

在回答之前,请务必阅读整个问题并运行示例。


概述

我在Unity 5.6.1中遇到了一些不一致的行为,当在静态Editor脚本中(因此在标记有[InitializeOnLoad]的类的静态构造函数中)加载嵌套资产时。

我正在使用Resources.Load加载一个带有公共引用另一个资源的ScriptableObject资源,假设是一个GameObject预制件。从这一点上来说,在这个简化的示例中,我将引用ScriptableObject作为'Wrapper'。

虽然Resources.Load正确返回Wrapper,但是第一次运行时经常还没有加载嵌套的Prefab引用,但是在第二次运行后它就被加载了:

Screencap showing that the prefab is not loaded on the first run, but is on the second

据我所知,这是一个执行顺序问题,在静态构造时尚未加载所涉及的Prefab资源,在后续运行中仍然被缓存。

我认为,当加载一个序列化引用另一个资产的资产时,默认情况下,嵌套的资产会自动加载,而不管这是否是在静态初始化期间进行的。然而,在这里似乎并非如此。

证明Wrapper资产确实正确地引用了Prefab在其序列化数据中(Asset Serialization设置为Force Text): Proof that prefab reference is correctly serialized

我还尝试使用AssetDatabase.LoadAssetAtPath(至少在编辑器中),但没有任何区别。


示例项目

您可以从此处下载UnityPackage,其中包含以下内容:

在此输入图片描述

或者按以下方式复制:

  • The Scripts:

    ExampleWrapper.cs:

    using UnityEngine;
    public class ExampleWrapper : ScriptableObject
    {
      public GameObject Value;
    }
    

    StaticLoader.cs:

    using UnityEngine;
    #if UNITY_EDITOR
    using UnityEditor;
    [InitializeOnLoad]
    #endif
    public class Loader
    {
      static Loader()
      {
        var Wrapper = Resources.Load<ExampleWrapper>("Wrapper");
        Debug.Log(Wrapper);         // Prints the Wrapper ScriptableObject
        Debug.Log(Wrapper.Value);   // Prints the Wrapped GameObject
      }
    }
    
  • Create an empty "ExampleObject" GameObject in the Hierarchy, then save it as a Prefab at Assets/Resources/ExampleObject.prefab

  • Create an asset instance of the ExampleWrapper and at Assets/Resources/Wrapper.asset

    • Since Unity 5 does not provide UI for spawning ScriptableObjects either create your own menu item, or use an automated solution. This question assumes you are familiar enough with ScriptableObjects to have your own preferred method.
  • Set the Wrapper asset's Value Field to the ExampleObject prefab enter image description here

  • Note that because on occasion unity does correctly cache the asset,


背景

这个例子是故意简化的,但是它基于使用ScriptableObjects存储/共享自定义系统配置数据的真实项目。

不要回复以下内容:

  • "只需使用Object.Instantiate" - 不会改变结果,并且在某些情况下修改Resources.Load返回的原始对象是可取的。
  • "跳过包装器,直接引用prefab/手动加载" - 虽然这样可以避免加载问题,但也错过了问题的重点。增加抽象层可以使系统之间共享资源更易于维护。此外,这个问题并不仅限于Prefabs(在这里只是为了简单的例子)。更现实的例子将包含几个嵌套对象,如Sprites、Materials、其他ScriptableObjects等。
  • "不要在静态构造期间加载" - Unity支持静态系统(否则为什么提供[InitializeOnLoad]?),并且使用基于ScriptableObjects的资产来存储这些系统的配置信息是一个非常真实的用例。在完全重新架构系统之前,我想看看其他可能的替代方案。

想要的是:

  • 是否可以强制Unity在这种静态上下文中预加载包装器中序列化的资产,而不必通过路径手动加载其内容?
  • 换句话说,我不想只运行Resources.Load<GameObject>("ExampleObject"),因为那样会抵消封装的整个意义。我可以修改ExampleWrapper类,但任何潜在的解决方案都需要自动化到添加prefab到检查器字段的工作流程就足够了。

编辑: 还应该注意到,当我关闭项目并重新打开时,我会看到以下内容:

enter image description here

  • 在启动过程中,静态构造函数被调用一次,Wrapper被加载,并且嵌套的prefab实际上被正确加载了。
  • 然后(仍然在初始启动期间,因为它发生在我输入任何操作之前)它再次静态构造,这次当Wrapper被加载时,嵌套的prefab没有被加载。

这个,我真的不理解。


首先,你说Resources.Load第一次不起作用,然后你又说你甚至不想使用Resources.Load...你希望听到什么样的答案?那你想要使用什么呢? - Programmer
我想在包装器上只使用一次 Resource.Load,而不是在每个封装资源上手动调用 Resource.Load,因为这样做会削弱将它们封装在单个包装器中的优点。请参考Galandil的答案,那里有我正在寻找的内容。 - Johannes
1个回答

3
关于您的问题存在一个误解。
引用被传递给 Loader 类,您可以在场景初始化完成后记录 Wrapper.Value 来检查它。
最可能的问题是(如您所指出)在执行/序列化顺序上,显然会发生以下情况:
1. 调用 Loader 构造函数,并正确传递了 Wrapper 引用。 2. Debug.Log(Wrapper.Value) 返回 null,因为脚本对象的字段尚未被序列化。 3. Wrapper 的字段已被序列化,现在记录 Wrapper.Value 将显示 ExampleObject。
因此,除非您计划在初始化期间对包装器字段进行“特殊”处理,否则您的代码中确实没有问题:我尝试在 ExampleWrapper 的 OnEnable 中运行 Debug.Log(Loader.Wrapper.Value),并得到了正确的值。
关于您的编辑,显然这是“按设计”发生的,正如在此问题中明确说明的那样:https://issuetracker.unity3d.com/issues/unityeditor-dot-initializeonload-calls-the-constructor-twice-when-the-editor-opens

啊,这很有道理。任何序列化的值都还没有加载,所以即使添加了 public int Blah 并更改它,在静态构造函数中打印时也会得到默认值。那么这是否意味着在执行顺序方面是:静态初始化 -> 应用序列化数据 -> 重置 -> 等等...? - Johannes
是的,我在ExampleWrapper中测试了一个int,并且它始终在开始时记录为0。关于初始化期间的执行顺序,你的猜想和我的一样好,即我们无法确定Unity底层发生什么事情以及何时发生。但可能static构造函数确实是最先执行的(Mono本地优先于Unity管理?真的不清楚)。 - Galandil
有道理,感谢反馈!自从你的回答后,我发现了另一个相关资源:https://blogs.unity3d.com/2016/06/06/serialization-monobehaviour-constructors-and-unity-5-4/,它确实表明Unity非常不希望用户在构造函数中加载资源。 - Johannes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接