【译】UNITY的序列化

发表于2016-03-16
评论0 8.1k浏览

 

原文地址:http://blogs.unity3d.com/2014/06/24/serialization-in-unity/

原文作者未做版权声明,视为共享知识产权进入公共领域,自动获得授权

为了分享更多底层的技术以及讲述为什么要这么做,本文讲述了Unity的序列化系统。充分理解这个系统将对开发效率和效果有很大的提升。现在让我们开始.

序列化对于Unity而言是非常核心的功能。许多功能就是基于这个功能实现的:

1)存储脚本数据:这个功能绝大多数人都熟悉

2)Inspector窗口:Inspector窗口并不是用C#API来得知它检视数据的属性。它要求物体进行序列化,然后展示序列化信息。

3)预制件:在内部实现上,一个预制件就是一个游戏物体或者组件的序列化信息流。一个实例就是一系列对于序列化信息的更改。预制件这个改变只存在编辑器里。当Unity执行构建的时候,预制件的更改将序列化到版本中,然后在初始化的时候,这些信息会被反序列化出来,这样,初始化出来的物体就会和编辑器中的预制件一样了。

4)初始化:当你对预制件或者场景中的游戏物体或者任何继承自UnityEngine.Object的东西调用Instantiate()的时候,我们对物体进行序列化,然后创建一个新的物体,然后把数据反序列化到新的物体里面去。(然后我们会检查所有继承于UnityEngine.Object的成员,如果这个引用指向一个外部成员,那么这个引用将保留不动,如果它指向的是一个内部成员,那么我们将把引用指向正确的位置)。

5)保存:如果你用文本编辑器打开一个后缀为unity的场景文件,然后选择“forcetext serialization”,我们会在后台用yaml进行序列化。

6)加载 : 向后兼容的加载系统也是基于序列化系统构建的。在编辑器里,yaml加载也是使用的序列化系统,并且运行时场景和资源的加载也是这样。Assetbundles同样使用了序列化系统。

7)编辑器代码的热重载:当你修改了一个编辑器代码,我们会对所有编辑器窗口进行序列化(它们也是继承于UnityEngine.Object)。然后我们摧毁所有的窗口,加载新的C#代码,重新创建窗口,然后把窗口的数据反序列化回新的窗口。

8)Resource.GarbageCollectSharedAssets(): 这是我们自己的垃圾收集器并且和C#的垃圾收集器不同。在你运行了一个新场景后,我们用它来判断哪些旧场景的数据不再被使用,然后我们就可以卸载它们。这个垃圾收集器使用序列化系统来记录物体之间的引用关系。这就是为什么我们可以把场景1中的纹理,在你使用场景2的时候进行卸载的原因。


序列化系统是用C++代码实现的,我们把它用在了所有内部数据类型上(纹理,AnimationClip,相机等等)。序列化是发生在UnityEngine.Object这个级别的,每个UnityEngine.Object都被作为一个整体进行序列化。它们包含了对其他UnityEngine.Objects的引用,并且这些引用也会被正确的序列化。你可能会觉得这些概念你并不感兴趣,只要它们能正确工作不影响你创建内容就可以。实际上这是和你有关的,我们使用相同的序列方法来数列化MonoBehaviour组件,而这个组件是你的脚本代码的基类。下面我们将描述序列化是如何工作的以及如何更好地使用它们。

 


脚本中那些成员会被按顺序序列化?

·        公有的,或者有[SerializeField]属性

·        非静态

·        非常量

·        非只读

·        是我们能序列化的类型


哪些类型我们能序列化?

·        自定义非抽象类,并且有Serializable属性

·        Serializable属性的自定义结构。(Unity4.5新引入的)

·        对继承自UntiyEngine.Object的物体的引用。

  • 原始数据类型(int,float,double,bool,string等等)
  • 可序列化类型的数组
  • 可序列化类型组成的List


什么时候序列化的行为会和我预期的不符?

自定义类的行为会和结构一样:

[Serializable]
class Animal
{
    public string name;
}

class MyScript : MonoBehaviour
{
    public Animal[] animals;
}


 

如果你用了一个animals数组,三个引用都指向同一个Animal对象,在序列化流里面你会发现三个物体。当它被反序列的时候,就是三个不同的物体。如果你需要序列化一个带有非常复杂引用关系的物体的时候,你不能依赖Unity帮你全部搞定。看下面这个Unity无法单独完成序列化的例子。

请注意这样的事情仅发生在自定义类上,因为他们是由使用的MonoBehaviour数据的序列化信息拼成的整体。如果一个成员是对UnityEngine.Object继承类的引用,比如“public Camera myCamera”,这个数据并不会被内联翻译,而是指向一个已经序列化好的camera类。

不支持自定义类型的null

 

突击测试,使用如下一个脚本,在发生反序列化的时候,进行了多少分配操作:




class Test : MonoBehaviour

{

    public Trouble t;

}


[Serializable]

class Trouble

{

    public Trouble t1;

    public Trouble t2;

    public Trouble t3;

}


如果答案是1的话不奇怪,因为要给Test分配空间,如果答案是2的话也不奇怪,一个给Test一个给Trouble。正确的答案是729。序列化器不支持null。如果它序列一个对象并且一个域为空,我们将会按照类型创建一个新的对象并初始化,显然这会导致无限循环,所以我们对于最大层数有一个魔数为7的限制。到达最大层数,我们就停止序列化对象为自定义类型或者list或者array的对象。


因为我们的子系统是构建在序列化系统之上的。所以当一个非常大的序列化流导致整个系统变慢的时候很吃惊。当我们研究特定项目的性能问题的时候,我们总能发现这个问题,我们在Unity4.5加了一个警告。但是我们把警告的实现搞出问题了,它会给出非常多的警告,除了立刻修复它,你别无选择。我们很快会发布一个正式版本的补丁。只会在运行的时候弹出一个警告,所以你不会再被它们搞疯了,你还是要修复这些问题,只是可以在你合适的时候。


不支持多态

如果你有这么一个变量

public Animal[] animals

并且你放入一个dog,一个cat,一个giraffe的实例,在序列化以后,你会的骚三个动物实例。

针对这个限制的一个解决方案是这个限制仅针对自定义类,会即时的序列化。如果是对其他UnityEngine.Object的引用会指向真正的引用。你应该继承ScriptableObject或者MonoBehaviour。这样就可以避开这个限制。

之所以有这个限制是因为序列化需要提前知道物体的内存分布,而这个信息依赖于成员的类型,而不是成员的内部数据。


如果我想序列化一些Unity不支持的类型,我该怎么做?

在很多情况下,最好的办法是用序列化回调。这样当序列化读取数据之前和写入之后都会通知你。你可以利用这个机制来序列化一些比较难的数据。你需要在Unity的序列化之前把数据转换成Unity能够正确理解的,并在序列化之后再把数据转成你需要的。

假设你有一个树的结构。如果你让Unity直接序列化这个结构,“no support for null”这个限制会导致你的数据流变得非常大,导致非常严重的性能问题。

using UnityEngine;

using System.Collections.Generic;

using System;


public class VerySlowBehaviourDoNotDoThis : MonoBehaviour

{

    [Serializable]

    public class Node

    {

        public string interestingValue = "value";

        

       //The field below is what makes the serialization data become huge because

       //it introduces a 'class cycle'.

       public List children = new List();

    }


    //this gets serialized

    public Node root = new Node();


    void OnGUI()

    {

        Display (root);

    }


    void Display(Node node)

    {

        GUILayout.Label ("Value: ");

        node.interestingValue = GUILayout.TextField(node.interestingValue, GUILayout.Width(200));


        GUILayout.BeginHorizontal ();

        GUILayout.Space (20);

        GUILayout.BeginVertical ();


        foreach (var child in node.children)

            Display (child);


        if (GUILayout.Button ("Add child"))

            node.children.Add (new Node ());


        GUILayout.EndVertical ();

        GUILayout.EndHorizontal ();

    }

}


 

相反,你直接告诉Unity不需要序列化这个树结构,你用一个序列化格式来单独存储这个树结构,以适应Unity的序列化器:

using UnityEngine;

using System.Collections.Generic;

using System;


public class BehaviourWithTree : MonoBehaviour, ISerializationCallbackReceiver

{

    //node class that is used at runtime

    public class Node

    {

        public string interestingValue = "value";

        public List children = new List();

    }


    //node class that we will use for serialization

    [Serializable]

    public struct SerializableNode

    {

        public string interestingValue;

        public int childCount;

        public int indexOfFirstChild;

    }


    //the root of what we use at runtime. not serialized.

    Node root = new Node();


    //the field we give unity to serialize.

    public List serializedNodes;


    public void OnBeforeSerialize()

    {

        //unity is about to read the serializedNodes field's contents. lets make sure

        //we write out the correct data into that field "just in time".

        serializedNodes.Clear();

        AddNodeToSerializedNodes(root);

    }


    void AddNodeToSerializedNodes(Node n)

    {

        var serializedNode = new SerializableNode () {

            interestingValue = n.interestingValue,

            childCount = n.children.Count,

            indexOfFirstChild = serializedNodes.Count+1

        };


        serializedNodes.Add (serializedNode);

        foreach (var child in n.children)

            AddNodeToSerializedNodes (child);

    }


    public void OnAfterDeserialize()

    {

        //Unity has just written new data into the serializedNodes field.

        //let's populate our actual runtime data with those new values.


        if (serializedNodes.Count > 0)

            root = ReadNodeFromSerializedNodes (0);

        else

            root = new Node ();

    }


    Node ReadNodeFromSerializedNodes(int index)

    {

        var serializedNode = serializedNodes [index];

        var children = new List ();

        for(int i=0; i!= serializedNode.childCount; i++)

            children.Add(ReadNodeFromSerializedNodes(serializedNode.indexOfFirstChild + i));


        return new Node() {

            interestingValue = serializedNode.interestingValue,

            children = children

        };

    }


    void OnGUI()

    {

        Display (root);

    }


    void Display(Node node)

    {

        GUILayout.Label ("Value: ");

        node.interestingValue = GUILayout.TextField(node.interestingValue, GUILayout.Width(200));


        GUILayout.BeginHorizontal ();

        GUILayout.Space (20);

        GUILayout.BeginVertical ();


        foreach (var child in node.children)

            Display (child);


        if (GUILayout.Button ("Add child"))

            node.children.Add (new Node ());


        GUILayout.EndVertical ();

        GUILayout.EndHorizontal ();

    }

}


请小心使用序列化器,包括序列化器的回调,因为它们通常不是运行在主线程,所以在调用UnityAPI的时候是受限的。(加载场景过程中的序列化是发生在加载线程的,部分发生在主线程对脚本调用Instantiate()的时候)。但是不管怎样,你是可以把对Unity不友好的格式转成对Unity友好的格式。

非常感谢你能阅读到这里,希望你从本文中得到一些有用的信息。

再见,Lucas. (@lucasmeijer)


PS:我们会将这些信息也添加到文档。


【1】    我撒谎了,正确答案并不是729。这是因为在有7层深度限制之前的古老日子里,Unity会无限循环,在你创建一个类似Troubleone的脚本时候会耗尽内存。5年前我们第一次修复这个问题,那时候还不是系列化系统。很显然这不是一种健壮的方式,很容易创建Trouble1->Trouble2->Trouble1->Trouble2这样的循环。不久以后我们用7层深度限制来捕获这些情况。我想说的是具体是什么数字不重要,重要的是你要意识到有一个循环就很麻烦

 

 

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引