XNA Game Engine系列教程11:串行化
任何一个可用的游戏引擎都会提供一个功能:将它的状态和组件的状态保存在一个文件中。这对保存游戏很有用,对在编辑器中保存场景尤其如此。C#中提供了两种类型的串行器。
首先是二进制串行器,但它会保存我们不想要的额外信息,而且对阅读代码来说也不是很有用。(但它安全性更好,如有必要以后我们会用其他方法来加密文件)。
第二个是XML串行器。这对我们更有用,但它需要在串行化前知道串行对象的类型。因此,我们也无法使用它,因为事先我们不知道场景中会包含什么类型的对象。
最后的选择是创造我们自己的串行器。这可能是最好的解决办法,因为我们可以用我们想要保存的任何格式保存它,我们可以保存多个对象在一个文件中,包括引擎的状态数据。我们的串行器工作原理如下:
- 类:SerializationData -这个类实际上是一个Dictionary,,而且包含了其他代码用来保存对象的配置信息。
- 方法:void AddData(string Key, object Data)-将数据添加到SerializationData
- 方法:object GetData(string Key)-从SerializationData获取数据
- 方法:void AddDependency(Type type)-告知串行器确保类型使用的配置在反串行化时可用
- 方法:Type GetTypeFromDependency (string Type)-获取从串行器返回的类型并从中获得相应的配置
- 类:Serializer -这个类处理将串行化的对象保存至文件,所有数据都保存为XML格式。
串行化的工作流程如下:
- 创建一个XmlWriter
- 创建一个串行器
- 该串行器将当前GameScreens和场景中的每个组件的信息写入文件
- SerializationData对象来自于组件
- 串行器将SerializationDate对象中的数据保存至文件
- 当写入数据时,串行器保存来源数据中的类型配置,这样它们就可以在反串行化时被加载。
- 将被使用的类型和配置集合写入文件中
反串行的工作顺序差不多就是反方向运行
- 依赖类型和配置集合被重新加载
- 重建GameScreens
- GameScreen中的的所有组件被重建并提供它们自己的SerializationData数据
这样的设计使我们能够保存(大多数情况下)任何类型的组件,而且只有对应的信息被保存。它意味着我们将不得不在每个组件中添加新的代码以实现保存和重新加载的功能,但实际上这是一件好事,因为我们将在创建组件的过程中拥有更大的控制权。
一些重要的提示:
- 接下来的代码不能保存List,Dictionary等数据类型,以后可以将这个功能加入,但现在我们没有实现这一点。我建议通过创建一个用逗号分隔的长字符串或类似的方法扩展lists。
- 数据保存不应过于复杂,被保存的对象的任何属性应该是公开成员而不是属性(即:不要使用get/set访问器,只需公有对象)。保存更复杂的对象时可以做些尝试,但这里还不能保证能正常工作。如Vector3,BoundingBox之类的数据类型这样做OK,但不要试图保存相机对象。对于相机来说应该保存它的位置,旋转等不是很复杂的类型。这不仅能够简化串行器,也能使存盘文件不至太大。
- 串行化的数据最终将被细分为基本类型为:int,float,byte,string等。
让我们开始吧。
在开始串行化之前,我们需要一种方法用来重置引擎,加载新的场景等,将以下函数添加到Engine类中:
// Resets the Engine to its initial state public static void Reset() { List<Component> destroy = new List<Component>(); foreach (GameScreen screen in Engine.GameScreens) foreach (Component component in screen.Components) destroy.Add(component); foreach (Component component in destroy) component.DisableComponent(); List<GameScreen> screenDestroy = new List<GameScreen>(); foreach (GameScreen screen in Engine.GameScreens) if (screen != Engine.BackgroundScreen) screenDestroy.Add(screen); foreach (GameScreen screen in screenDestroy) screen.Disable(); Engine.Services.Clear(); Engine.Content.Unload(); }
现在我们创建一些串行化的辅助类-SerializationData和SerializationData:
// Information about Services struct ServiceData { public string Type; public bool IsService; }
// Provides a link between the Component class and Serializer class. A component // adds data and keys to this class to simplify serialization public class SerializationData { // Dictionary that stores the data. It is public so the Serializer can save // the data it holds. public Dictionary<string, object> Data = new Dictionary<string, object>(); // Add data to the dictionary with the string key Key and value object Data public void AddData(string Key, object Data) { this.Data.Add(Key, Data); } // Get data of type T from the dictionary with the string key Key public T GetData<T>(string Key) { object val = this.Data[Key]; return (T)val; } // Whether or not the data contains the specified Key public bool ContainsData(string Key) { return Data.ContainsKey(Key); } public XmlWriter Writer { get { return Writer; } } // The serializer being used for serialization/deserialization Serializer serializer; XmlWriter writer; // Constructor sets the serializer being used public SerializationData(Serializer Serializer, XmlWriter Writer) { serializer = Serializer; writer = Writer; } // Tell the serializer we will be using the specified Type public void AddDependency(Type type) { serializer.Dependency(type); } // Get the Type specified through the serializer, so the right // Assembly will be used. public Type GetTypeFromDependency(string type) { return Assembly.Load(serializer.DependencyMap[type]).GetType(type); } }
现在开始编写串行化代码:
// Class used to serialize Components to an Xml file public class Serializer { }
接下来添加一些代码处理dependencies,在这里dependency是一个类型,配置信息从它获取数据。
// Keeps track of what Assembly Types are located in. The Type is the Key and the // Assembly is the value public Dictionary<string, string> DependencyMap = new Dictionary<string, string>(); // Uses the DependencyMap to write the dependencies to the Xml File public void WriteDependencies(XmlWriter Writer) { Writer.WriteStartElement("Dependencies"); // Temporary list is used to keep track of the dependencies we need to write List<string> assemblies = new List<string>(); // For each Type, add its Assembly to the assembly list if it has not been added foreach (string assembly in DependencyMap.Values) if (!assemblies.Contains(assembly)) assemblies.Add(assembly); // Write each assembly to the file foreach(string assembly in assemblies) { Writer.WriteStartElement("Assembly"); Writer.WriteAttributeString("Name", assembly); // Write each type in the Assembly as a child node foreach(string type in DependencyMap.Keys) { if (DependencyMap[type] == assembly) { Writer.WriteStartElement("Type"); Writer.WriteAttributeString("Name", type); Writer.WriteEndElement(); } } Writer.WriteEndElement(); } } // Add the type to the DependencyMap public void Dependency(Type type) { string name = type.FullName; string assembly = type.Assembly.FullName; if (!DependencyMap.ContainsKey(name)) DependencyMap.Add(name, assembly); } // Load back the Assemblies used from file public void PopulateAssemblies(XmlNode DependenciesRoot) { foreach (XmlNode Node in DependenciesRoot.ChildNodes) foreach (XmlNode child in Node.ChildNodes) // For each node, add the type to the list. Attribute 0 = type name, // attribute 1 = assembly name DependencyMap.Add(child.Attributes[0].Value, Node.Attributes[0].Value); } // Clear the list of dependencies public void ClearDependencies() { DependencyMap.Clear(); }
下一个是保存GameScreens信息的代码:
// Write the GameScreens in Engine to file public void WriteGameScreens(XmlWriter Writer) { Writer.WriteStartElement("GameScreens"); foreach (GameScreen screen in Engine.GameScreens) { // The background screen is created automatically, so we dont need to serialize it if (screen != Engine.BackgroundScreen) { Writer.WriteStartElement("GameScreen"); Writer.WriteAttributeString("Name", screen.Name); Writer.WriteAttributeString("Type", screen.GetType().FullName); Dependency(screen.GetType()); if (screen.BlocksInput) { Writer.WriteElementString("BlocksInput", null); } if (screen.OverrideInputBlocked) { Writer.WriteElementString("OverrideInputBlocked", null); } if (screen.BlocksUpdate) { Writer.WriteElementString("BlocksUpdate", null); } if (screen.OverrideUpdateBlocked) { Writer.WriteElementString("OverrideUpdateBlocked", null); } if (screen.BlocksDraw) { Writer.WriteElementString("BlocksDraw", null); } if (screen.OverrideDrawBlocked) { Writer.WriteElementString("OverrideDrawBlocked", null); } if (screen == Engine.DefaultScreen) { Writer.WriteElementString("DefaultScreen", null); } Writer.WriteEndElement(); } } Writer.WriteEndElement(); }
现在我们将添加串行化一个对象的函数。当串行化每个组件时要使用这个函数。
// Saves the data contained in an object public void SerializeObject(XmlWriter Writer, object Input, string InstanceName) { // We can't serialize a null object if (Input == null) return; // If we are dealing with a seperate field, we can save its name if (InstanceName != null) { Writer.WriteStartElement("Field"); Writer.WriteAttributeString("Name", InstanceName); } Type t = Input.GetType(); // Add the type's assembly to the dependencies if neccessary Dependency(t); // If we have a value type, we can save it directly if (t == typeof(short) || t == typeof(long) || t == typeof(float) || t == typeof(decimal) || t == typeof(double) || t == typeof(ulong) || t == typeof(uint) || t == typeof(ushort) || t == typeof(sbyte) || t == typeof(int) || t == typeof(byte) || t == typeof(char) || t == typeof(string) || t == typeof(bool)) { // Write the type of value, then the value of the value Writer.WriteAttributeString("Type", t.FullName); Writer.WriteValue(Input.ToString()); } else { // If its not a value type, we need to break it dowm Writer.WriteStartElement(Input.GetType().FullName); // Serialize all fields recursively. This will break them // down automatically if they are not value types too. foreach (FieldInfo info in Input.GetType().GetFields()) SerializeObject(Writer, info.GetValue(Input), info.Name); Writer.WriteEndElement(); } // Write the end if this is not a one line value if (InstanceName != null) Writer.WriteEndElement(); }
代码的其余部分是从XML文件重建对象。
// Deserialize the component defined in the ComponentNode public Component Deserialize(XmlNode ComponentNode) { // Find the type from the component nodes name Assembly a = Assembly.Load(DependencyMap[ComponentNode.LocalName]); Type t = a.GetType(ComponentNode.LocalName); // Create an instance of the type and a SerializationData object Component component = (Component)Activator.CreateInstance(t); SerializationData data = new SerializationData(this, null); // For each field defined, get its value and add the field to the // SerializationData foreach (XmlNode child in ComponentNode.ChildNodes) { // Make name and object values, and get the name from the 0 // attribute, "Name" string name = child.Attributes[0].Value; object value = null; // If the field node contains text only, it is a value type // and we can set object directly if (child.ChildNodes[0].NodeType == XmlNodeType.Text) value = parse(child); // Otherwise we need to recreate a more complex object from the data else if (child.ChildNodes[0].NodeType == XmlNodeType.Element) value = parseTree(child.FirstChild); // Save the field to the SerializationData data.AddData(name, value); } // Tell the component to load from the data component.RecieveSerializationData(data); return component; } // Returns an object from an XmlNode that contains a value type object parse(XmlNode value) { // Get the type being parsed Assembly a = Assembly.Load(DependencyMap[value.Attributes["Type"].InnerText]); Type t = a.GetType(value.Attributes["Type"].InnerText); // If it is a string, we can return it how it is if (t == typeof(string)) return value.InnerText; // Otherwise, it can be parsed using the "Parse()" method all value // types have, invoked using reflection MethodInfo m = t.GetMethod("Parse", new Type[] { typeof(string) }); // Return the value "Parse()" returns, using the node text // as the argument return m.Invoke(null, new object[] { value.InnerText }); } // Returns an object constructed from a tree of XmlNodes object parseTree(XmlNode root) { // Get the type to be built Assembly a = Assembly.Load(DependencyMap[root.Name]); Type t = a.GetType(root.Name); // Create an instance of the type object instance = Activator.CreateInstance(t); // For each field in the node's children foreach (XmlNode member in root.ChildNodes) { // Get the info on it FieldInfo fInfo = t.GetField(member.Attributes["Name"].Value); // If the node contains a value type, set the value directly if (member.ChildNodes[0].NodeType == XmlNodeType.Text) fInfo.SetValue(instance, parse(member)); // Otherwise, we need to parse it again as a tree. This will // do the same recursively if the parsed type isn't a value type else fInfo.SetValue(instance, parseTree(member)); } return instance; }
现在我们要将组件保存到一个文件中,要做到这点我们需要有一个方式定义它们。我们可以通过给组件的名称添加一个简单的字符串定义它们。这个名称会自动设置成一个唯一值,如果需要也可以在以后重命名。
static int count = 0; public string Name; public override string ToString() { return this.Name; } // InitializeComponent() count++; Name = this.GetType().FullName + count;
接下来组件需要添加一些基本方法获取串行化的数据。它也包含一些虚方法,从此继承的组件可以用自己的数据重写这些虚方法。
// Returns a SerializationData a Serializer can use to save the state // of the object to an Xml file public SerializationData GetSerializationData(Serializer Serializer, XmlWriter Writer) { // Create a new SerializationData SerializationData data = new SerializationData(Serializer, Writer); // Add the basic Component values data.AddData("Component.DrawOrder", DrawOrder); data.AddData("Component.ParentScreen", Parent.Name); data.AddData("Component.Visible", Visible); data.AddData("Component.Name", this.Name); // Tell the serializer that it will need to know the type of // component data.AddDependency(this.GetType()); // Construct a ServiceData ServiceData sd = new ServiceData(); // If this object is a service, find out what the // provider type is (the type used to look up the service) Type serviceType; if (Engine.Services.IsService(this, out serviceType)) { // Tell the serializer about the provider type data.AddDependency(serviceType); // Set the data to the ServiceData sd.IsService = true; sd.Type = serviceType.FullName; } // Add the ServiceData to the SerializationData data.AddData("Component.ServiceData", sd); // Call the overridable function that allows components to provide data SaveSerializationData(data); return data; } // Reconstructs the Component from SerializationData public void RecieveSerializationData(SerializationData Data) { // Set the basic Component values this.DrawOrder = Data.GetData<int>("Component.DrawOrder"); this.Visible = Data.GetData<bool>("Component.Visible"); this.Name = Data.GetData<string>("Component.Name"); // Get the ServiceData from the data ServiceData sd = Data.GetData<ServiceData>("Component.ServiceData"); // If the component was a service if (sd.IsService) { // Get the type back from the serializer Type t = Data.GetTypeFromDependency(sd.Type); // Add the service to the Engine Engine.Services.AddService(t, this); } // Set the owner GameScreen string parent = Data.GetData<string>("Component.ParentScreen"); this.Parent = Engine.GameScreens[parent]; // Call the overridable function that allow components to load from data LoadFromSerializationData(Data); } // Overridable function to allow components to save data during serialization public virtual void SaveSerializationData(SerializationData Data) { } // Overridable function to allow components to load data during deserialization public virtual void LoadFromSerializationData(SerializationData Data) { }
最后,Engine需要添加一些方法从文件中保存/加载场景:
// Save the current state of the engine to file public static void SerializeState(string Filename) { // Get the start time DateTime startTime = DateTime.Now; // Create an XmlWriter XmlWriterSettings set = new XmlWriterSettings(); set.Indent = true; XmlWriter writer = XmlWriter.Create(new FileStream(Filename, FileMode.Create), set); // Create a Serializer Serializer s = new Serializer(); // Write the start of the document, including the root node and save time writer.WriteStartDocument(); writer.WriteStartElement("EngineState"); writer.WriteAttributeString("Time", startTime.ToString()); // Serialize the list of GameScreens s.WriteGameScreens(writer); // Write the component root node writer.WriteStartElement("Components"); // Serialize all the components, if they want to be serialized foreach (GameScreen gameScreen in GameScreens) foreach (Component component in gameScreen.Components) if (component.Serialize) { writer.WriteStartElement(component.GetType().FullName); s.Serialize(writer, component.GetSerializationData(s, writer)); writer.WriteEndElement(); } // Finish the Components node writer.WriteEndElement(); // Write out Assembly dependencies s.WriteDependencies(writer); // Finish the document writer.WriteEndElement(); writer.WriteEndDocument(); // Finish writing writer.Close(); // Calculate elapsed time DateTime stopTime = DateTime.Now; TimeSpan elapsedTime = stopTime - startTime; } // Reload the state of the engine from file public static void DeserializeState(string Filename) { // Get the start time DateTime startTime = DateTime.Now; // Load the Xml document from file XmlDocument doc = new XmlDocument(); doc.Load(Filename); // Locate the Components root node XmlNode ComponentsNode = doc.GetElementsByTagName("Components")[0]; // Create a serializer Serializer s = new Serializer(); // Reload the Assembly dependencies s.PopulateAssemblies(doc.GetElementsByTagName("Dependencies")[0]); // Deserialize each component in the file foreach (XmlNode node in ComponentsNode.ChildNodes) s.Deserialize(node); // Calculate the elapsed time DateTime stopTime = DateTime.Now; TimeSpan elapsedTime = stopTime - startTime; }
这就是串行化的全部代码。当然,我们需要更新大多组件才能使用它。我不想在这篇文章中写出这些改变,因为它们实在是太多了,不久以后我会在另一篇文章中发布组件的的升级版本。
发布时间:2009/3/4 下午4:06:23 阅读次数:6318