Contents

07Serialization相关

Serialization相关

本文主要记录了UnitySerialization的一些细节。 大部分Unity书籍都有介绍到Serialization(序列化)的相关功能。官方文档也有详细的介绍:

如Serialization规则: https://docs.unity3d.com/Manual/script-Serialization.html

SerializationReference规则: https://docs.unity3d.com/ScriptReference/SerializeReference.html

Serialization规则:

根据官方文档,序列化规则会直接作用在C#类的字段上,以方便快速的将对象数据序列化成之久数据。对于Prefab,场景,数据类等都是必须有的功能。相对应的,unity对所对应的字段需要满足一定程度的规则。

Serialization大部分情况主要作用在MonoBehaviour和ScriptableObject这两个类的环境下。也就是说继承该类型的子类的实例,会被序列化成持久化数据。最常见的就是ScriptableObject类型的序列化。

对于类字段来说:

  • public字段,或者又SeializeField注解的会被序列化。
  • static,const,readonly字段不会被序列化。

对字段的类型也有一定要求:

  • C#原始数据类型:int,float,double,bool,string,etc
  • 枚举类型:需要32位以内的格式。
  • 固定大小的buffer。
  • Unity内置类型:Vector2,Vector3,Rect,Matrix4x4,Color,AnimationCurve
  • 带有Serializable注解的自定义Struct。
  • 带有Serializable注解的自定义Class。
  • UnityEngine.Object的子类。
  • 以上述类型为参数的,数组或者List类型。

额外的说明

  • 对于嵌套类型,多维数组,Dictionary等类型是不支持序列化的。也不会反射显示再Inspector面板上面。
  • 可以自定义序列化,反序列化流程。只要实现ISerializationCallbackReceiver接口即可。其中有两个方法,OnBeforeSerialize会在序列化前调用触发。OnAfterDeserialize 会在反序列化后触发。通过这两个方法可以自定义一些复炸的数据结构方式。

这里需要注意到的是:

对于序列化类型来说,文件名字必须和类名相同,否则序列化时可以正常运行。反序列化时会因为找不到对应类型,而初始化会默认值。

对于UnityEngine.Object的子类来说,只要是对应类型的即可序列化。但是自定义类型,没有继承自UnityEngine.Object的类型需要标注Serializable。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[CreateAssetMenu]
public class TestScriptObject : ScriptableObject
{
    public NoSerializableClass field1;
    public HasSerializableClass field2;
    public InnerScriptObject field3;
}

public class InnerScriptObject : ScriptableObject
{
    public string str;
}

public class NoSerializableClass
{
    public string str;
}

[Serializable]
public class HasSerializableClass
{
    public string str;

}

创建对应的ScriptableObject会发现Inspector面板如下图。其中不存在field1,只有field2可以展开,填写对应的数据字段。而field3则是一个可以放入其他ScriptableObject的引用类型值。

这就是因为未标记Serializable的字段不认为可以被序列化,再反序列化的时候也不会被处理。

SerializeReference

在上面我们已经可以看到对于自定义类型的序列化,和,继承UnityEngine.Object类型的序列化是有一点不太一样的。一个是直接展示暴露编辑字段,一个是引用对象处理。这里就涉及到Unity序列化的两种方式。

  • 对于通常默认的序列化,称之为Inline Serialization。unity会将对象的值直接序列化成数据,放在prefab对应字段处。当反序列化时,直接从对应字段处反序列化出来。
  • 还有一种称之为SerializeReference。即引用序列化。会给序列化后的值赋值给一个引用ID,并将数据放在对应引用ID下面。这样当反序列化后,引用相同ID的字段,会反序列化同一对象。

举例来说明,对于:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[CreateAssetMenu]
public class TestScriptObject : ScriptableObject
{
    public HasSerializableClass field1;
    public HasSerializableClass field2;

    [Sirenix.OdinInspector.Button]
    public void FillField()
    {
        var value = new HasSerializableClass() {str = "SaveString" };
        field1 = value;
        field2 = value;
    }
}

[Serializable]
public class HasSerializableClass
{
    public string str;

}

当我们运行FillField方法是,其会给field1和field2注入一个相同的类型。此时看Inspector面板,会发现field2显示Reference to field1表示是对field1上对象的一个引用。当修改field1中str字段时,会发现,field2中字段也跟着改变。 但是当我们保存,触发反序列化之后会发现field1和field2变成了两个不相干的对象。这就是每个对应字段都序列化到了自己的inline字段中。

如果修改成如下结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//情况1
[CreateAssetMenu]
public class TestScriptObject : ScriptableObject
{
    [SerializeReference] public HasSerializableClass field1;
    [SerializeReference] public HasSerializableClass field2;
}

//情况2
[CreateAssetMenu]
public class TestScriptObject : ScriptableObject
{
    public HasSerializableClass field1;
    [SerializeReference] public HasSerializableClass field2;
}

则会发现,

对于情况1:触发反序列化后,两个任然指向同一个对象。这是因为序列化后两个字段都指向同一个引用编号。进而反序列化同一份数据,并且根据编号,两者引用同一个值。 对于情况2:触发反序列化后,会发现两个是不同对象。并没有说,field2直接引用到field1上面。这是因为field1是inline数据,field2是指向引用id。并不是说直接指向field1,所以还是会产生两个不同数据。

通过prefab数据我们可以看到这个结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
...
  m_EditorClassIdentifier: 
  field1:
    str: SaveString
  field2:
    rid: 4498571975599325184
  references:
    version: 2
    RefIds:
    - rid: 4498571975599325184
      type: {class: HasSerializableClass, ns: , asm: Assembly-CSharp}
      data:
        str: SaveString

可以看到field1是直接内嵌序列化了一段数据。而对于field2则是记录了一个rid。并且在references中又对应rid对应的类型和数据。


这就是Reference和直接数据序列化上的区别。这里就说到对于Unity来说

  • 继承子UnityEngine.Object类型的都是引用序列化。也就是Monobehaviour,Transform,animation等等都是以Reference的方式序列化的。
  • 其他类型,除非特别声明则是按值内嵌序列化。

同时官方文档也给出了使用SerializeReference的场景。

  • 如果你希望多个字段引用同一份数据,可以将其全部声明成SerializReference。
  • 如果你希望使用字段的多态特性,则可以申明为SerializeReference。 使用多态特性,即显示的字段,序列化后的数据,是该字段实际类型的数据结构。如果不适用引用序列化,那么其会按照字段申明的类型来进行值序列化,这样就会丢失子类型数据。
  • 你希望序列化null值。对于值类型来说,并不存在null值结构。

同时对于值序列化来说其效率是高于SerializeReference的。对于存储,内存占用,加载时间等来说都是如此。

而对SerializeReference的使用来说,伴随着跟Unity序列化一样的限制结构。前面已经看到Serialization的字段有着很多限制,同时可以思考,这些字段类型,尤其是Array和List的泛型参数,可以是继承UnityEngine.Object类型的。所以SerializeReference也有可以使用的场景,和不能的情况。

  • Unity支持对Array以及List类型的引用序列化。对于这些类型,SerializeReference注解相当于作用在其容器中的每个元素上面。但是你不可以申明一个System.Object类型并赋值一个Array或List以期望其按照对应方式序列化。
  • 对于继承子UnitEngine.Object类型的不可以注解SerializeReference。
  • 不可以是C#的值类型。
  • 不可以是Serialization中不可以序列化的容器结构。

Odin情况下的Serialization

Odin插件也提供了一套序列化方式,这很大一部分原因是因为Inspector的编辑操作依赖于对象类型上的序列化,只有序列化成数据才比较方便去做对应的UI绘制,以及数据编辑操作。

Odin提供了几种方式来使用Odin序列化。可以看出其实际也是在Unity原有的管线接口上,进行了一层自己的封装。

  • 方式1: 直接继承SerializedMonoBehaviour或者SerializedScriptableObject。这个基类里面有对序列化方式的支持。
  • 方式2: 实现ISerializationCallbackReceiver。并且在对应接口里面桥接实现Odin的序列化操作。为此Odin提供了一个序列化UlilityUnitySerializationUtility。其需要当前序列化的类,以及一个SerializationData对象。

Odin的序列化主要就是针对上面所不支持的一些情景进行了填充。操作方式就是将不支持的数据结构,转化成Unity支持序列化的数据结构来进行存储。

  • 对Dictionary的支持。