Skip to main content
 首页 » 资源教程 » Unity3D教程

Unity中的序列化

2016年09月02日 22:55:514560

小编的话:这是一篇关于unity序列化的文章,有原文和译文(有译错的地方还请各位包含)。文章主要介绍的部分由:序列化是Unity的核心,序列化对字段的要求,遇到特殊情况的序列化,如何应对等等。文章较长,大家要耐心读下去啊↖(^ω^)↗。

       In the spirit of sharing more of the tech behind the scenes, and reasons why some things are the way they are, this post contains an overview of Unity’s serialization system. Understanding this system very well can have a big impact on the effectiveness of your development, and the performance of the things you make.  Here we go.

       我愿在幕后为大家分享更多有关技术的资讯和告诉大家有些事为什么是它们现在这样的原因。这篇文章是关于Unity序列化系统的概述。我们很好的了解这个系统,能帮助大家有效开发并更好展现所做的成品。现在我们马上开始。


       Serialization of“things” is at the very core of Unity. Many of our features build ontop of theserialization system:
       序列化是Unity的核心。Unity的许多特色都建立在序列化系统之上。

·        Storing data stored in your scripts. This onemost people are probably somewhat familiar with.
       在脚本中存储数据。这一点,大多数人都非常熟悉。


·       Inspector window. The inspector window doesn’t talk tothe C# api to figure out what the values of the properties of whatever it isinspecting is. It asks the object to serialize itself, and then displays theserialized data.
        检视器窗口。检视器窗口无法与c# api交互,就不能查出无论哪个它正在检验的属性值。它要求对象序列化,然后显示序列化数据。

·       Prefabs. Internally,a prefab is the serialized data stream of one (or more) game objects andcomponents. A prefab instance is a list of modifications that should be made onthe serialized data for this instance. The concept prefab actually only existsat editor time. The prefab modifications get baked into a normal serializationstream when Unity makes a build, and when that gets instantiated, theinstantiated game objects have no idea they were a prefab when they lived inthe editor.
         预制。内层结构中,预制是一个(或多个)游戏对象和组件的序列化数据流。预制实例是需进行实例化数据的修改列表。但这个概念只在编辑环节中用到。当Unity做一个构建及构建实例化时,预制修改表进入正常的序列化流中,而且当在编辑器中运行时,实例化的游戏对象不知道它们是预制。

·       Instantiation. When youcall Instantiate() on either a prefab, or a gameobject that lives in the scene,or on anything else for that matter (everything that derives fromUnityEngine.Object can be serialized), we serialize the object, then create anew object, and then we “deserialize” the data onto the new object. (We thenrun the same serialization code again in a different variant, where we use itto report which other UnityEngine.Object’s are being referenced. We then checkfor all referenced UnityEngine.Object’s if they are part of the data beingInstantiated(). If the reference is pointing to something “external” (like atexture) we keep that reference as it is, if it is pointing to something“internal” (like a child gameobject), we patch the reference to thecorresponding copy).
       实例化。当你要么访问预制,要么访问场景中的游戏物体,要么访问任何可序列化的UnityEngine.Object时,都可以实例化。我们给对象实例化,然后创建一个新的对象,最后我们“并行化”数据到新的对象中。(我们在不同的变体中,再次运行相同的序列化代码,我们用它来写其他UnityEngine.Object’s中引用的报告。如果他们中一部分数据被实例化,我们检查所有引用的UnityEngine.Object’s。如果引用指向“外部”的东西(如文本),我们就保持引用。如果指向“内部”的东西(比如子游戏对象),我们修正引用相应的副本)。


·        Saving. If youopen a .unity scene file with a text editor, and have set unity to “force textserialization”, we run the serializer with a yaml backend.
         存储。如果你用文本编辑器打开一个Unity场景文件,Unity设置为“强制文本序列化”,我们用在服务器用yaml格式进行序列化。


·        Loading. Might notseem surprising, but backwards compatible loading is a system that is built ontop of serialization as well. In-editor yaml loading uses theserialization system, as well as the runtime loading of scenes and assets.Assetbundles also make use of the serialization system.
       载入。听起来有些奇怪的是,向后兼容加载也是一种建立在序列化上的系统。yaml格式的加载使用序列化系统,运行时间的加载和设置也使用序列化系统。此外,资源包也要利用序列化系统。


·       Hot reloading of editor code. When youchange an editor script, we serialize all editor windows (they derive fromUnityEngine.Object!), we then destroy all the windows, unload the old c# code,load the new c# code, recreate the windows, and finally deserialize thedatastreams of the windows back onto the new windows.
        编辑器代码的频繁重载。当改变编辑脚本时,我们得重新序列化所有编辑器窗口(他们继承于UnityEngine.Object !),然后我们关闭所有窗口,卸载旧的c#代码,加载新的c#代码,重新创建窗口,最后反序列化数据流到新窗口中。


·        Resource.GarbageCollectSharedAssets(). This is our native garbage collector and is different to the C# garbage collector. Itis the thing that we run after you load a scene to figure out which things fromthe previous scene are no longer referenced, so we can unload them. The nativegarbage collector runs the serializer in a mode where we use it to have objectsreport all references to external UnityEngine.Objects. This is what makestextures that were used by scene1, get unloaded when you load scene2.
        Resource.GarbageCollectSharedAssets()函数
       本地的垃圾回收站和c#垃圾回收站是不同的。区别就是我们在加载场景并找出哪些东西是从先前的场景到今不再引用的之后,我们卸载它们。本地垃圾回收站以序列化器模式运行,这种模式的原理是用它来进行报告到外部UnityEngine.Objects上的所有引用。例如,这正是场景1使用的文本,当运行场景2时,卸载场景1。

       The serialization system is written in C++, we use it for all our internal objecttypes (Textures, AnimationClip, Camera, etc). Serialization happens at theUnityEngine.Object level, each UnityEngine.Object is always serialized as awhole. They can contain references to other UnityEngine.Objects and thosereferences get serialized properly.
       序列化系统是用c++写的,我们用它编辑所有内部对象类型(文本、动漫剪辑、相机等)。序列化存在于UnityEngine Object层面中,每一UnityEngine Object中总作为一个整体进行序列化。它们包括对UnityEngine的引用和恰当地序列化。

       Now you may say that none of this concerns you very much, you’re just happy that it works andwant to get on with actually creating some content. However, this will concernyou, as we use this same serializer to serialize MonoBehaviour components,which are backed by your scripts. Because of the very high performancerequirements that the serializer has, it does not in all cases behave exactlylike what a C# developer would expect from a serializer. Here we’lldescribe how the serializer works and some best practices on how to make thebest use of it.
      现在你也许会说,我对这些都不太感兴趣,你只是对它能运行并继续创造一些新内容而感到满意。然而,当我们使用相同的序列化器去序列化在脚本尾部的MonoBehaviour组件时,你将会比较在意。由于序列化器有很高的执行要求,在通常情况下,它完全不像C#开发人员对序列化器的要求。在这里我们将告诉大家序列化器是如何工作的,以及通过实例,讲解如何更好利用它。

What does afield of my script need to be in order to be serialized?
序列化操作中,对脚本的字段有何要求?
·        Be public, or have [SerializeField] attribute
·        公共的,或带有[SerializeField]的属性
·        Not be static
·        非静态
·        Not be const
·        非常量
·        Not be readonly
·        非随机
·        The fieldtype needs to be of a type that we canserialize.
·        字段类型必须是可序列化的类型

Which fieldtypescan we serialize?
何种字段类型可以序列化?
·        Custom non abstract classes with [Serializable]attribute.
·        带有可序列化属性的自定义非抽象类
·        Custom structs with [Serializable] attribute. (new inUnity4.5)
·        带有可序列化属性的自定义结构体(在新的Unity4,5中)
·        References to objects that derive from UntiyEngine.Object
·        引动到继承于UntiyEngine.Object的对象
·        Primitive data types (int,float,double,bool,string,etc)
·        原始数据类型(int,float,double,bool,string等等)
·        Array of a fieldtype we can serialize
·        可序列化的数组
·        List<T> of a fieldtype we can serialize
·        可序列化的列表<T>

       So far so good.So what are these situations where the serializer behavesdifferently from what I expect?
       到目前为止一切都很顺利。若是碰到序列化器的操作和我们预想的不一样时,该怎么处理呢?

       Custom classes behave like structs
       自定义类的功能就像结构体
[C++] 纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
[Serializable]
classAnimal
{
    publicstring name;
}
 
classMyScript : MonoBehaviour
{
    publicAnimal[] animals;
}


        If you populate the animals array with three references to a single Animal object, in theserializationstream you will find 3 objects. When it’s deserialized, there arenow three different objects. If you need to serialize a complex object graphwith references, you cannot rely on Unity’s serializer doing that allautomagically for you, and have to do some work to get that object graphserialized yourself. See the example below on how to serialize things Unitydoesn’t serialize by itself.
       举个常用的例子。用3个引用表示动物数组到单个动物的对象,在序列化流程后,会有三个对象。当反序列化后,还会有3个不同的对象。如果用复杂的带有引用的对象图来序列化,就不能依赖于Unity的序列化器来替你自动完成这过程,只能自己去做对象图的序列化。接下来的例子,告诉我们Unity无法序列化时,如何进行序列化操作。


       Note that thisis only true for custom classes, as they are serialized “inline” because theirdata becomes part of the complete serializationdata for the MonoBehaviour theyare used in. When you have fields that have a reference to something that is aUnityEngine.Object derived class, like a “public Camera my Camera”, the datafrom that camera are not serialized inline, and an actual reference to thecamera UnityEngine. Object is serialized.
       要注意的是,这只适用于自定义类。之所以作为内联方式的序列化,是因为他们的数据在MonoBehaviour中作为部分的完整序列化数据而被使用。当有些字段是引用于UnityEngine.Object的派生类时,形如“public Camera myCamera”,来自于camera类中的数据就不是序列化的“行内”,并且引用到camera类的UnityEngine.Object中的数据是已序列化的。


No support fornull for custom classes
不支持空字符的自定义类
       Pop quiz. Howmany allocations are made when deserializing a MonoBehaviour that uses thisscript:
       有个普遍的疑惑。当使用脚本序列化MonoBehaviour时,到底用了多少配置呢?

[C++] 纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
classTest : MonoBehaviour
{
    publicTrouble t;
}
 
[Serializable]
classTrouble
{
    publicTrouble t1;
    publicTrouble t2;
    publicTrouble t3;
}


        It wouldn’t bestrange to expect 1 allocation, that of the Test object. It also wouldn’t bestrange to expect 2 allocations, one for the Test object and one for a Troubleobject.
The correctanswer is 729. The serializer does not support null. If it serializes an objectand a field is null, we just instantiate a new object of that type andserialize that. Obviously this could lead to infinite cycles, so we have arelatively magical depth limit of 7 levels. At that point we just stopserializing fields that have types of custom classes/structs and lists andarrays. [1]
       大家对其中的一个配置,即测试对象,肯定不会感到惊奇。对于另外个配置,大家也不会感到陌生。一个配置叫做测试对象,另一个叫做诊断对象。
正确答案是729。序列化器无法识别空字符。如果它序列化的一个对象或字段是空字符的话,我们只是在实例化那种类型的对象及序列化它。很明显,这将导致无限循环,就像数字7一样有个神奇的无限循环规律一样(当某个数除以7时)。从这点来看,我们不使用有自定义类/结构体和有列表、数组的序列化字段。


        Since so many ofour subsystems build on top of the serialization system, this unexpectedly bigserializationstream for the Test monobehaviour will cause all these subsystemsto perform more slowly than necessary. When we investigate performance problemsin customer projects, we almost always find this problem and we added a warningfor this situation in Unity 4.5. We actually messed up the warning implementation in such a way that it gives youso many warnings, you have no other option but to fix them right away. We’llsoon ship a fix for this in a patch release, the warning is not gone, but youwill only get one per “entering playmode”, so you don’t get spammed crazy.You’d still want to fix your code, but you should be able to do it at a timewhere it suits you.
       因为我们很多的子系统在构建序列化系统,这将给Test Monobehaviour带来巨大的序列化流,并引起子系统比正常的运行速度慢很多。当我们在客户项目中调查运行问题时,我们总是发现这个毛病并把这种情况反馈到Unity4.5,作为警告。实际上,我们这样做反而把警告给搞砸了。出现了更多的警告提示,我们别无选择,只得去修复它们。不久,我们将要发布一个修复补丁,警告还是不会消失,只不过用户只能每次“进入玩家模式”,就不必再为那些警告提示发疯了。你们或许还是想修改代码,但得在适合你们的时间段去完成它。


No support forpolymorphism
不支持多态

       If you have a
       假如你有个这样一段代码
[AppleScript] 纯文本查看 复制代码
?
1
public Animal[] animals

        and you put inan instance of a dog, a cat and a giraffe, after serialization, you will havethree instances of Animal.
        你在实例中加入了狗,猫和长颈鹿。在序列化之后,你将在动物类中有三个实例。

        One way to deal with this limitation is to realize that it only applies to “custom classes”,which get serialized inline. References to other UnityEngine.Object’s getserialized as actual references and for those, polymorphism does actually work.You’d make a ScriptableObject derived class or another MonoBehaviour derivedclass, and reference that. The downside of doing this, is that you need tostore that monobehaviour or scriptable object somewhere and cannot serialize itinline nicely.
       解决此种限制的一种方法是意识到它只应用于得到内联方式的序列化“自定义类”。引用到其他UnityEngine.Object进行真实的序列化,多态也在其中有所使用。你会做一个ScriptableObject的派生类或是MonoBehaviour的派生类。但这样做的不足之处在于,你需要在某地保存monobehaviour或能脚本化的对象,但不能很好进行内联方式的序列化。
                                                                                          
       The reason forthese limitations is that one of the core foundations of the serializationsystem is that the layout of the datastream for an object is known ahead oftime, and depends on the types of the fields of the class, instead of whathappens to be stored inside the fields.
       有这些局限性的原因之一是,序列化系统的核心基础是需提前知道对象的数据流的布局,它取决于这个类的字段类型,而不是考虑存储在字段中会引起什么变化。

I want toserialize something that Unity’s serializer doesn’t support. What do I do?
我想要序列化一些Unity序列化器无法支持的代码。该怎么做?
        
        In many casesthe best approach is to use serialization callbacks. They allow you to benotified before the serializer reads data from your fields and after it is donewriting to them.
You can use thisto have a different representation of your hard-to-serialize data at runtime thanwhen you actually serialize. You’d use these to transform your data intosomething Unity understands right before Unity wants to serialize it, you alsouse it to transform the serialized form back into the form you’d like to haveyour data in at runtime, right after Unity has written the data to your fields.
Let’s say youwant to have a tree datastructure. If you let Unity directly serialize the datastructure, the “no support for null” limitation would cause your datastream tobecome very big, leading to performance degradations in many systems:
       在多数情况下,最有效的方法是使用序列化回调。他们让你在被通知之前,序列化器从你的字段中读取数据字段并在之后写出读取的字段。
你可以在运行期间,选用不容易序列化的数据,而不是那些你真正在做序列化时的数据。你用序列化回调把数据转换成在Unity将要序列化之前,它能够理解的数据。你同样用它在运行时间之前,转换成你想要的序列化的格式。之后,Unity会在你的域内写好转换的数据。
假设你想要有个树型数据结构。如果你让Unity直接序列化数据结构,“不支持空字符”的限制会致使你的数据流变得非常大,导致在许多系统中无法实现功能。

[C++] 纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
usingUnityEngine;
usingSystem.Collections.Generic;
usingSystem;
 
publicclass VerySlowBehaviourDoNotDoThis : MonoBehaviour
{
    [Serializable]
    publicclass Node
    {
        publicstring interestingValue = "value";
         
       //The field below is what makes the serialization data become huge because
       //it introduces a 'class cycle'.
       publicList<Node> children = newList<Node>();
    }
 
    //this gets serialized
    publicNode root = newNode();
 
    voidOnGUI()
    {
        Display (root);
    }
 
    voidDisplay(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 (newNode ());
 
        GUILayout.EndVertical ();
        GUILayout.EndHorizontal ();
    }
}

       Instead, you tell Unity not to serialize the tree directly, and you make a seperate field to store the tree in a serialized format, suited for Unity’s serializer:
       另外,当你告知Unity不直接序列化树型,并在顺序化格式中做了个单独字段来保存树型,以下是适合Unity的序列化器:

[C++] 纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
usingUnityEngine;
usingSystem.Collections.Generic;
usingSystem;
 
publicclass BehaviourWithTree : MonoBehaviour, ISerializationCallbackReceiver
{
    //node class that is used at runtime
    publicclass Node
    {
        publicstring interestingValue = "value";
        publicList<Node> children = newList<Node>();
    }
 
    //node class that we will use for serialization
    [Serializable]
    publicstruct SerializableNode
    {
        publicstring interestingValue;
        publicint childCount;
        publicint indexOfFirstChild;
    }
 
    //the root of what we use at runtime. not serialized.
    Node root = newNode();
 
    //the field we give unity to serialize.
    publicList<SerializableNode> serializedNodes;
 
    publicvoid 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);
    }
 
    voidAddNodeToSerializedNodes(Node n)
    {
        var serializedNode = newSerializableNode () {
            interestingValue = n.interestingValue,
            childCount = n.children.Count,
            indexOfFirstChild = serializedNodes.Count+1
        };
 
        serializedNodes.Add (serializedNode);
        foreach (var child in n.children)
            AddNodeToSerializedNodes (child);
    }
 
    publicvoid 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 = newNode ();
    }
 
    Node ReadNodeFromSerializedNodes(intindex)
    {
        var serializedNode = serializedNodes [index];
        var children = newList<Node> ();
        for(inti=0; i!= serializedNode.childCount; i++)
            children.Add(ReadNodeFromSerializedNodes(serializedNode.indexOfFirstChild + i));
 
        returnnew Node() {
            interestingValue = serializedNode.interestingValue,
            children = children
        };
    }
 
    voidOnGUI()
    {
        Display (root);
    }
 
    voidDisplay(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 (newNode ());
 
        GUILayout.EndVertical ();
        GUILayout.EndHorizontal ();
    }
}

        Beware that theserializer, including these callbacks coming from the serializer, usually donot run on the main thread, so you are very limited in what you can do in termsof invoking Unity API. (Serialization happening as part of loading a scenehappens on a loading thread. Serialization happening as part of you invokingInstantiate() from script happens on the main thread). You can however do thenecessary data transformations do get your data from anon-unity-serializer-friendly format to a unity-serializer-friendly-format.
       You made it tothe end!
       Thanks forreading this far, hope you can put some of this information to good use in yourprojects.
       Bye, Lucas. (@lucasmeijer)
       注意那些序列化器,特别是来自序列化器中的回调,通常不在主线程上运行。所以就调用Unity API而言,你所能做的是要受到限制的。(序列化在加载一个场景时执行,也在加载线程执行。序列化还发生在从脚本中调用Instantiate()函数时,还发生在主线程中。)这样一来,你有必要去将数据从非友好交互型的Unity序列化器格式转换成友好交互型的Unity序列化器格式。
       这样做就完成了!
       感谢你的阅读,希望这些信息会对你的项目有所帮助。
       再见,卢卡斯。(@lucasmeijer)

PS: We’ll addall this information to the documentation as well.
PS:我们会将这些信息添加到文档中。
评论列表暂无评论
发表评论