FishPlayer

一个喜欢摸鱼的废物

0%

Unity Serialization 序列化小笔记

序列化(serialization)在计算机科学的资料处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。

序列化的发生

当我们用(UnityEngine.Object.Instantiate())此方法去生成一个 UnityObject 时候,就经历了序列化和反序列化的操作。

Original Object —serialize—> mid-data —deserialize—> Spawned Object

序列化使用场景

保存相关资产

我们的许多 UnityObject 资产 scene, prefab, material 都是以文本文件(YAML)的形式保存在项目中。编辑器时,会在 Library 中存放’中间物体’。
我猜测,编辑器时会使用这些’中间物体’以减少实际序列化的步骤来加速运行。

当我们将磁盘里的 Asset 加载到游戏的运行时里的 UnityObject 的过程,是一个反序列化操作,把YAML反序列化成 UnityObject。

在编辑器时,我们会修改脚本,改变一些数据类型。把字段名称修改后,原本序列化的值丢失了。我猜是因为项目在 Refresh 的时候,会先把所有必要的数据都序列化存入’中间物体’,然后等代码编译完,生成了新的类型,再尝试把原本的’中间物体’用新的类型反序列化生成新物体并在序列化生成新的’中间物体’。这个过程中由于是使用新类型进行反序列化了,自然会抛弃新类型中没有的(不同名且类型不匹配的)数据。引擎C#层重载 HotReloading 会报错但是有可能凑合能跑估计也是这个原因,所以 HotReloading 实际上是为了让我们快速修改业务逻辑看看有无修好的。

FormerlySerializedAs(PROPERTY_NAME) 这个特性可以解决上面说的这个问题,但其实就是要求这个字段做序列化相关操作时沿用旧的字段名(PROPERTY_NAME)。这样在 prefab 中序列化下来记录的字段名一直都是那个旧的字段名。

编辑器的字段绘制

当我们对字段做做编辑器绘制的时候,也会有序列化以及反序列化的操作。

Inspector —update changes—> Serialized Properties —deserialize—> Spawned Object
Inspector <—update view— Serialized Properties —serialize—> Spawned Object

UnityObject 中的字段值会被序列化到 Serialized Properties 集合中,然后编辑器绘制相关的代码拿到序列化后得到的数据集合,再把他们提出来并根据数据类型画不同的样式。
下面是一个 PropertuDrawer 的例子,在绘制方法的参数中有 SerializedProperty,这个就是用于存放 UnityObject 中被序列化字段的 wrapper。

1
2
3
4
5
6
7
8
9
10
11
12
13
[CustomPropertyDrawer(typeof(NormalizedCurve))]
public class NormalizedCurvePropertyDrawer : PropertyDrawer
{
const string CURVE_FIELD = "_curve";
static readonly Rect RANGES = new Rect(0.0f, 0.0f, 1.0f, 1.0f);

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
var curveProperty = property.FindPropertyRelative(CURVE_FIELD);
// ....
}
}

对 UnityObject 引用的序列化

哦我的老兄,这个大家都很熟悉了。以文本的形式打开我们的 prefab 康康。摘一段

UnityScriptClassType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
--- !u!114 &7941049597127225971 // !u!CLASS_TYPE &OBJECT_FILE_ID
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3753066996938937369}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 43f865a8433367346843b29a2464c1a7, type: 3} // guid point to this object instance's asset source
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}

首行表明了该 UnityObject 的类型以及它在这个 Prefab 中的唯一标识ID。
m_Script 这一行表明了这个物体是个 .cs 脚本。

我们还能发现有些字段只记录 { FileID }, 有些却要记录三个参数 {fileID, guid, type} 。记录三个参数的表明的是这个 prefab 内的物体的外部来源。听起来很别扭,我说详细点吧,就好比说一个 prefab A 中包含了一个 prefab B,那么 B 物体就是需要在 prefab A 中以如上方式记录。 其中 guid 是 prefab B 所在(定义处)的资源文件的 guid, FileID 则是这个物体在资源内的唯一ID,type 是这个物体的资源类型。

所以 UnityObject 引用的序列化是依靠以文本的形式记录ID完成的。

对空引用(null)的支持

空引用只支持 UnityObject 捏,如果是我们自定义的 C# class, 序列化时会生成默认的给他。这个时候如果有疯狂嵌套的 C# class就会爆炸了,因为他会一直往里层生成默认值给这些可序列化的 C# Class object。