XNA Game Engine系列教程11:串行化

任何一个可用的游戏引擎都会提供一个功能:将它的状态和组件的状态保存在一个文件中。这对保存游戏很有用,对在编辑器中保存场景尤其如此。C#中提供了两种类型的串行器。

首先是二进制串行器,但它会保存我们不想要的额外信息,而且对阅读代码来说也不是很有用。(但它安全性更好,如有必要以后我们会用其他方法来加密文件)。

第二个是XML串行器。这对我们更有用,但它需要在串行化前知道串行对象的类型。因此,我们也无法使用它,因为事先我们不知道场景中会包含什么类型的对象。

最后的选择是创造我们自己的串行器。这可能是最好的解决办法,因为我们可以用我们想要保存的任何格式保存它,我们可以保存多个对象在一个文件中,包括引擎的状态数据。我们的串行器工作原理如下:

串行化的工作流程如下:

反串行的工作顺序差不多就是反方向运行

这样的设计使我们能够保存(大多数情况下)任何类型的组件,而且只有对应的信息被保存。它意味着我们将不得不在每个组件中添加新的代码以实现保存和重新加载的功能,但实际上这是一件好事,因为我们将在创建组件的过程中拥有更大的控制权。

一些重要的提示:

让我们开始吧。

在开始串行化之前,我们需要一种方法用来重置引擎,加载新的场景等,将以下函数添加到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

2006 - 2024,推荐分辨率 1024*768 以上,推荐浏览器 Chrome、Edge 等现代浏览器,截止 2021 年 12 月 5 日的访问次数:1872 万 9823 站长邮箱

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号