【译】UNITY的序列化
原文地址: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
什么时候序列化的行为会和我预期的不符?
自定义类的行为会和结构一样:
如果你用了一个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 ();
}
}
请小心使用序列化器,包括序列化器的回调,因为它们通常不是运行在主线程,所以在调用Unity的API的时候是受限的。(加载场景过程中的序列化是发生在加载线程的,部分发生在主线程对脚本调用Instantiate()的时候)。但是不管怎样,你是可以把对Unity不友好的格式转成对Unity友好的格式。
非常感谢你能阅读到这里,希望你从本文中得到一些有用的信息。
再见,Lucas. (@lucasmeijer)
PS:我们会将这些信息也添加到文档。
【1】 我撒谎了,正确答案并不是729。这是因为在有7层深度限制之前的古老日子里,Unity会无限循环,在你创建一个类似Troubleone的脚本时候会耗尽内存。5年前我们第一次修复这个问题,那时候还不是系列化系统。很显然这不是一种健壮的方式,很容易创建Trouble1->Trouble2->Trouble1->Trouble2这样的循环。不久以后我们用7层深度限制来捕获这些情况。我想说的是具体是什么数字不重要,重要的是你要意识到有一个循环就很麻烦