Unity3D :脚本序列化
推荐:将NSDT场景编辑器加入你的3D工具链
3D工具集:NSDT简石数字孪生
脚本序列化
序列化是将数据结构或游戏对象状态转换为 Unity 以后可以存储和重建的格式的自动过程。
在 Unity 项目中组织数据的方式会影响 Unity 序列化数据的方式,这可能会对项目的性能产生重大影响。本页概述了 Unity 中的序列化以及如何针对它优化项目。
本文档涵盖以下主题:
- 序列化规则
- 自定义序列化
- Unity 如何使用序列化
- 序列化错误
- 序列化最佳实践
序列化规则
Unity 中的序列化程序专门设计用于在运行时高效运行。因此,Unity 中的序列化与其他编程环境中的序列化行为不同。Unity 中的序列化程序直接处理 C# 类的字段,而不是其属性,因此字段必须符合一些规则才能序列化。以下部分概述了如何在 Unity 中使用字段序列化。
若要使用字段序列化,必须确保字段:
- 是公共的,或者具有序列化字段属性
- 不是静态的
- 不是常量
- 不是只读的
- 具有可序列化的字段类型:
- 基元数据类型(整数、浮点数、双精度、布尔值、字符串等)
- 枚举类型(32 口或更小)
- 固定大小的缓冲区
- Unity 内置类型,例如矢量 2、矢量 3、矩形、矩阵 4x4、颜色、动画曲线
- 具有可序列化属性的自定义结构
- 对派生自 UnityEngine.Object 的对象的引用
- 具有可序列化属性的自定义类。(请参阅自定义类的序列化)。
- 上述字段类型的数组
- 上述字段类型的 A
List<T>
注意:Unity 不支持多级类型(多维数组、交错数组、字典和嵌套容器类型)的序列化。如果要序列化这些内容,则有两个选项:
- 将嵌套类型包装在类或结构中
- 通过实现 ISerializationCallbackReceiver 使用序列化回调来执行自定义序列化。
自定义类的序列化
要使 Unity 序列化自定义类,必须确保该类:
- 具有可序列化属性
- 不是静态的。
当您将派生类的实例分配给字段并且 Unity 保存该字段时,Unity 会将该字段序列化为对该实例的引用。Unity 独立序列化实例本身,因此在为实例分配多个字段时不会重复。但是对于不是派生自 的自定义类,Unity 将实例的状态直接包含在引用它们的 MonoBehavior 或 ScriptableObject 的序列化数据中。有两种方式可以发生这种情况:内联和通过 [SerializeReference]。
UnityEngine.ObjectUnityEngine.Object
- 内联序列化:默认情况下,当您未在引用类的字段上指定自定义类时,Unity 会按值内联序列化自定义类。这意味着,如果在多个不同的字段中存储对自定义类实例的引用,则它们在序列化时将成为单独的对象。然后,当 Unity 反序列化字段时,它们包含具有相同数据的不同对象。
[SerializeReference]
[序列化引用]
序列化:如果指定,Unity 将对象建立为托管引用。host 对象仍直接将对象存储在其序列化数据中,但存储在专用的注册表部分中。[SerializeReference]
[SerializeReference]
增加一些开销,但支持以下情况:
- 字段可以为空。内联序列化不能表示 null,而是将 null 替换为具有未赋值字段的内联对象。
- 对同一对象的多个引用。如果在不使用 的情况下将对自定义类实例的引用存储在多个不同的字段中,则它们在序列化时将成为单独的对象。
[SerializeReference]
- 图形和循环数据(例如,具有对自身的引用的对象)。内联类序列化不支持 null 或共享引用,因此数据中的任何循环都可能导致意外结果,例如奇怪的检查器行为、控制台错误或无限循环。
- 多态性。如果创建派生自父类的类并将其分配给使用该父类作为其类型的字段,则 Unity 仅序列化属于父类的字段。当 Unity 反序列化类实例时,它会实例化父类而不是派生类。
[SerializeReference]
- 当数据结构需要稳定的标识符来指向特定对象而不对对象的数组位置进行硬编码或搜索整个数组时。请参阅 SerializationUtility.SetManagedReferenceIdForObject。
注意:内联序列化更有效,除非您特别需要支持的功能之一,否则应使用它。有关如何使用 的完整详细信息,请参阅序列化参考文档。[SerializeReference][SerializeReference]
属性的序列化
Unity 通常不会序列化属性,但以下情况除外:
- 如果属性具有显式支持字段,Unity 将根据常规序列化规则对其进行序列化。例如:
public int MyInt
{
get => m_backing;
private set => m_backing = value;
}
[SerializeField] private int m_backing;
- Unity 仅在热重载期间使用自动生成的字段序列化属性。
如果不希望 Unity 使用自动生成的字段序列化属性,请使用 [字段:非序列化] 属性。public int MyInt { get; set; }
自定义序列化
有时,您可能希望序列化 Unity 的序列化程序不支持的内容(例如,C# 字典)。最好的方法是在类中实现 ISerializationCallbackReceiver 接口。这允许您实现在序列化和反序列化期间在关键点调用的回调:
- 当对象即将序列化时,Unity 会调用回调。在此回调中,您可以将数据转换为 Unity 理解的内容。例如,若要序列化 C# 字典,请将字典中的数据复制到键数组和值数组中。
OnBeforeSerialize()
- 回调完成后,Unity 将序列化数组。
OnBeforeSerialize()
- 稍后,当对象反序列化时,Unity 会调用回调。在此回调中,您可以将数据转换回对内存中的对象方便的形式。例如,使用键和值数组重新填充 C# 字典。
OnAfterDeserialize()
Unity 如何使用序列化
保存和加载
Unity 使用序列化将场景、资源和资源包加载到设备内存中或从设备内存中保存。这包括保存在您自己的脚本 API 对象(如 MonoBehavior 组件和 ScriptableObjects)中的数据。
Unity 编辑器中的许多功能都是在核心序列化系统之上构建的。序列化时需要特别注意的两件事是“检查器”窗口和热重载。
“检查器”窗口
“检查器”窗口显示已检查对象的序列化字段的值。更改检查器中的值时,检查器将更新序列化数据并触发更新检查对象的反序列化。
这同样适用于内置 Unity 对象和脚本对象(如 MonoBehavior 派生类)。
当您在“检查器”窗口中查看或更改值时,Unity 不会调用任何 C# 属性获取器和资源库;相反,Unity 直接访问序列化的支持字段。
热重载
热重载是在编辑器打开时创建或编辑脚本并立即应用脚本行为的地方。您无需重新启动编辑器即可使更改生效。
更改并保存脚本时,Unity 会热重载当时加载的所有脚本数据。Unity 将所有可序列化变量存储在所有加载的脚本中,然后重新加载这些脚本并还原序列化的变量。热重载会丢弃所有不可序列化的数据,因此以后将无法访问这些数据。
这会影响项目中的所有编辑器窗口和所有单体行为。与其他序列化情况不同,Unity 在重新加载时默认序列化私有字段,即使它们没有“序列化字段”属性也是如此。
当 Unity 重新加载脚本时:
- Unity 在所有加载的脚本中序列化和存储所有变量。
- Unity 将它们恢复到其原始的序列化前值:
- Unity 会还原满足序列化要求的所有变量(包括私有变量),即使变量没有属性也是如此。有时,您需要阻止 Unity 恢复私有变量,例如,如果您希望在从脚本重新加载后引用为 null。在这种情况下,请使用
[字段:非序列化]
属性。[SerializeField]
- Unity 从不恢复静态变量,因此不要将静态变量用于 Unity 重新加载脚本后需要保留的状态,因为重新加载过程会丢弃它们。
预制件
预制件是一个或多个游戏对象或组件的序列化数据。预制件实例包含对预制件源的引用和对其的修改列表。这些修改是 Unity 需要对预制件源执行的操作,以创建该特定的预制件实例。
预制件实例仅在您在 Unity 编辑器中编辑项目时存在。Unity 编辑器从游戏对象的两组序列化数据中实例化游戏对象:预制件源和预制件实例的修改。
实例
对场景中存在的任何内容(如预制件或游戏对象)调用实例化
时:
- Unity 对其进行序列化。这在运行时和编辑器中都会发生。Unity 可以序列化派生自 的所有内容。
UnityEngine.Object
- Unity 创建一个新的游戏对象,并将数据反序列化到新的游戏对象上。
- Unity 在不同的变体中运行相同的序列化代码,以报告它引用的其他变体。它会检查所有引用的内容,以查看它们是否是 Unity 实例化数据的一部分。如果引用指向外部内容(例如纹理),Unity 将保留该引用原样。如果引用指向内部内容,例如子游戏对象,Unity 会将引用修补到相应副本。
UnityEngine.ObjectsUnityEngine.Objects
卸载未使用的资产
EditorUtility.UnloadUnusedAssetsImmediate
是本机 Unity 垃圾回收器,其用途与标准 C# 垃圾回收器不同。它会在您加载场景后运行,并检查不再引用的对象(如纹理)并安全地卸载它们。本机 Unity 垃圾回收器在变体中运行序列化程序,其中对象报告对外部 .这就是一个场景使用的纹理,垃圾回收器在下一个场景中卸载的方式。UnityEngine.Objects
编辑器和运行时序列化之间的差异
大多数序列化发生在编辑器中,而反序列化是运行时的重点。Unity 仅在编辑器中序列化某些功能,而它可以在编辑器和运行时序列化其他功能:
功能 | 编辑 器 | 运行 |
---|---|---|
二进制格式的资产 | 支持读/写 | 支持读取 |
YAML 格式的资产 | 支持读/写 | 不支持 |
保存场景、预制件和其他资产 | 支持,除非处于播放模式 | 不支持 |
使用 JsonUtility 序列化单个对象 | JsonUtility 的读/写支持。 使用 EditorJsonUtility 支持其他类型的对象 | JsonUtility 的读/写支持 |
序列化引用 | 受支持 | 受支持 |
ISerializationCallbackReceiver | 受支持 | 受支持 |
以前序列化为 | 受支持 | 不支持 |
对象可以具有只有编辑器序列化的其他字段,例如,当您在UNITY_EDITOR脚本符号中声明字段时:
public class SerializeRules : MonoBehaviour
{
#if UNITY_EDITOR
public int m_intEditorOnly;
#endif
}
在上面的示例中,字段仅在编辑器中序列化,不包含在生成中。这允许您通过省略构建中编辑器中仅需要的数据来节省内存。使用该字段的任何代码也需要有条件地编译,例如在#if UNITY_EDITOR块中,以便类可以在构建时编译。m_intEditorOnly
编辑器不支持具有 Unity 仅在运行时序列化的字段的对象(例如,当您在 UNITY_STANDALONE 指令中声明字段时)。
脚本序列化错误
脚本序列化可能会导致错误。下面列出了其中一些的修复程序。
“不允许从 MonoBehavior 构造函数(或实例字段初始值设定项)调用 find,而是在 Awake 或 Start 中调用。
在 MonoBehavior 构造函数或字段初始值设定项中调用脚本 API(如 GameObject.Find)
会触发此错误。
要解决此问题,请在 MonoBehavior.Start
中调用脚本 API,而不是在构造函数中调用。
“在序列化期间不允许调用 find,请改为从唤醒或开始调用它。”
从标有 GameObject.Find 的类的构造函数中调用脚本 API(如 GameObject.Find)会触发此错误。System.Serializable
要解决此问题,请编辑代码,确保不会在任何序列化对象的构造函数中调用任何脚本 API。
线程安全的统一脚本 API
上述限制会影响大多数脚本 API。只有 Unity 脚本 API 的某些部分是豁免的,您可以从任何位置调用它们:
Debug.Log
数学
函数- 简单的自包含结构;例如,像
Vector3
和四元数
这样的数学结构
为了降低序列化期间出错的风险,除非没有替代方法,否则只需调用自包含且不需要在 Unity 本身中获取或设置数据的 API 方法。
序列化最佳实践
您可以组织数据,以确保充分利用 Unity 的序列化。
- 旨在让 Unity 序列化尽可能小的数据集。这样做的目的不是节省计算机硬盘驱动器上的空间,而是确保您可以保持与以前版本的项目的向后兼容性。如果处理大型序列化数据集,则在开发后期,向后兼容性可能会变得更加困难。
- 切勿让 Unity 序列化重复数据或缓存数据。这会导致向后兼容性出现重大问题:由于数据可能不同步,因此存在很高的错误风险。
- 避免在引用其他类时使用嵌套的递归结构。序列化结构的布局始终需要相同;独立于数据,仅取决于脚本中公开的内容。引用其他类的唯一方法是通过派生自 的类。这些类是独立的;它们仅相互引用,不嵌入内容。
UnityEngine.Object
由3D建模学习工作室整理翻译,转载请注明出处!