前言
游戏实践当中,序列化是一个十分关键的概念。wiki的定义是: 序列化是将结构化数据或物体转化为可以存储格式(文件、内存缓存等)的过程。
序列化用途:
- 传递message
- 存储数据
- RPC等
下面介绍在c#和U3D中如何使用序列化。
在C#中使用序列化
Stream
首先了解一下流(stream)的概念。参考自StackOverflow,stream代表按顺序排列的物体集合,流支持不同的权限:读、写和seek等。通过抽象出流的概念,可以更好的描述文件、IO、socket等,可以更方便的进行读写操作。根据存放位置又可分为MemoryStream、FileStream等。
基本使用
最简单的使用序列化的方法就是在类上加入Serializable属性,然后通过不同的Formatter进行序列化或反序列化。
[Serializable]
public class MyObject {
public int n1 = 0;
public int n2 = 0;
public String str = null;
}
MyObject obj = new MyObject();
obj.n1 = 1;
obj.n2 = 24;
obj.str = "Some String";
IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream("MyFile.bin", FileMode.Create, FileAccess.Write, FileShare.None);
formatter.Serialize(stream, obj);
stream.Close();
以上为使用二进制序列化方式,将MyObj类二进制序列化到MyFile.bin文件中。反序列化:MyObject obj = (MyObject) formatter.Deserialize(stream);
。
同时,可以参考MSDN相关文档学习XML序列化的相关知识。
序列化流程及原理
可以通过OnDeserializedAttribute,OnDeserializingAttribute,OnSerializedAttribute,OnSerializingAttribute
在相关序列化阶段进行自定义操作。试例如下:
string member;
[OnSerializing()]
internal void OnSerializingMethod(StreamingContext context)
{
member = "OnSerializing";
}
[OnSerialized()]
internal void OnSerializingMethod(StreamingContext context)
{
member = "OnSerialized";
}
此外,通过实现接口ISerializable来控制序列化过程。接口包含GetObjectData
和一个在反序列化中使用的特殊构造函数如下所示:
///序列化
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("i",n1);
info.AddValue("j",n2);
info.AddValue("k",str);
}
///反序列化
protected MyObject(SerializationInfo info,StreamingContext context)
{
n1 = info.GetInt32("i");
n2 = info.GetInt32("j");
str = info.GetString("k");
}
参考自MSDN序列化文档。序列化时,通过GetObjectData
自定义填充SerializationInfo;反序列化时,通过特殊构造函数读取SerializationInfo中数据。具体序列化步骤参考此处。
序列化在U3D中的实际应用
相较于c#中序列化实现,在U3D中序列化有一些新的特性与使用方式。参考自官方手册,U3D中序列化同样是用来从硬盘等持久化数据中加载Asset、AssetBundle等资源。以下为几种常见用途:
- 脚本中存储数据
- Inspector Window
- Prefabs
- Instantiation
脚本中可以被序列化的条件:
- public,或[SerializeField]属性
- 非静态
- 非常量
- 非只读
- 类型为可序列化类型
在U3D中,使用序列化有一些需要注意的地方,具体详情参考自这篇Unity官方blog,和这篇系列博客。
定义继承自MonoBehaviour的类MyBehaviour:
public class MyBehaviour : MonoBehaviour
{
public float pi = 3.1415f;
private int mySecret = 42;
public static int myStatic = 10;
}
已知在使用Instantiate
时使用序列化,使用下面这段代码,在点击鼠标右键时实例化物体。因为private
不序列化,所以在Instantiate时数据不变。
void Start()
{
Debug.Log (pi + " " + myStatic + " " + mySecret);
}
void Update () {
if (Input.GetMouseButton (0)) {
pi = -4;
mySecret = -11;
myStatic = 13;
GameObject.Instantiate (gameObject);
}
}
注意,myStatic
变化是因为静态变量,并非是序列化造成的结果。
通常,与c#中的序列化类似,在使用时,也是在类上加入属性[System.Serializable]
,但是在U3D中会出现如下的一些问题:
- 由于序列化脚本继承自
MonoBehaviour
需要将脚本加到GameObject上 - 无法实现多态
- 引用耦合(decoupled references) : 如
testClass[] tClass
会序列化为多个物体,而非引用 - 循环调用,因为在序列化时不允许为
null
,Unity内部为阻止无限循环声明,限制最多调用7层
解决方案是使用ScriptableObject: 继承自ScriptableObject的类无需附加到GameObject上,更利于那些只用于存储数据的类。
先看一下ScriptableObject的基本使用:
public static T CreateScriptable<T>() where T: ScriptableObject
{
T newScriptable = ScriptableObject.CreateInstance<T>();
string path = AssetDatabase.GetAssetPath(Selection.activeObject);
if(path.Length == 0)
{
path = "Assets/";
}
string className = typeof(T).Name;
path = AssetDatabase.GenerateUniqueAssetPath(path+"/new"+className+".asset");
AssetDatabase.CreateAsset(newScriptable,path);
return newScriptable;
}
上面这段代码是创建asset资源的通用方式。
关于(decoupled references),可以看一下下面这段代码。
public class ScriptableCity : ScriptableObject
{
public string name;
}
[ExecuteInEditMode]
public class ScriptableDecoupleTest : MonoBehaviour
{
public ScriptableCity city1;
public ScriptableCity city2;
private void OnEnable()
{
if (city1 == null)
{
city1 = ScriptableObject.CreateInstance<ScriptableCity>();
city1.name = “Chicago“;
city2 = city1;
}
Debug.Log(city1.name);
Debug.Log(city2.name);
city1.name = “New York“;
Debug.Log(city1.name);
Debug.Log(city2.name);
}
}
当将脚本附加到GameObject上时,结果:当city2的name改变时,city1的name也发生改变,表明: city1和city2指向同一物体,为reference。
除此之外,使用ScriptableObject还可以解决多态和循环调用的bug,详情看这篇博客。
总结
序列化在存储数据方面是一个核心概念,本篇讲述了序列化的核心思想和在c#、U3D中的具体使用流程,以及ScriptableObject使用方法。