序列化的实际应用

序列化的含义,以及在c#和U3D中的具体应用

Posted by Luffy on February 1, 2017

前言

游戏实践当中,序列化是一个十分关键的概念。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使用方法。