Saturday, August 27, 2011

Baddies 4–Saving

Introduction

Objective

The objective of the saving system is, given a node, any node, serialize it completely and automatically without having to go through the mistake prone method of each time we add a member, having to remember to update the load and save functions.

To this aim, we will use the IntermediateSerializer class, with a few auxiliary classes to handle logical exceptions in our design and in the IntermediateSerializer class.

Considered options

There are several options when saving the game in XNA, the 3 more common ones are serializing using the XmlSerializer class, serializing using the IntermediateSerializer class, and finally, saving with a BinaryWriter class.

The reasons why I won’t use the XmlSerializer are explained in the conclusion in this post, so I won’t expand on it. The BinaryWriter, on the other hand, requires knowing the data you are going to read beforehand, as it’s in binary format and you need to select “chunks” of specific lengths to be interpreted as variables. This does not work well with the initial objective of the saving being automatic or not having to make custom save / load functions for each class. This leaves us with the IntermediateSerializer.

Serialization

Serializing with the IntermediateSerializer

The use of the IntermediateSerializer is best documented in these 2 articles by Shawn Hargraves: Teaching a man to fish, and Everything you ever wanted to know about IntermediateSerializer.

On the positive side, it’s simple (doesn’t require the multithreading gymnastics for the container that the XmlSerializer needs), and highly customizable (adding private members, excluding public members, setting members as references, dealing with collections, etc…). On the negative side, it only works on windows, but that’s an acceptable inconvenient, and it needs the full .Net Framework 4.0 instead of the client version (explanation).

Working with the Node

For our specific situation, we want to serialize a subtree of nodes, this nodes being any class that might derive from node. For example, we might have a situation like this:

Saving1

This diagram represents that there is a root node “Scene” with two added child nodes “Camera” and “Map” (ignore the UML notation, it was the only editor at hand). We want to serialize Scene without caring about what hangs from it and have everything serialized to XML.

The first attempt at serializing this launched an error of cyclic reference, as each Node has a pointer to it’s parent. To solve this we declared the parent member of a node as a ContentSerializerAttribute.SharedResource so instead of storing the parent, it just stored a reference to this. This serialized, but upon closer investigation of the XML code it became apparent that this generated “shadow” parents, as we had both the original parent that called the serialization, and the one each child was referencing, resulting in something like this:

Ssaving2

That’s because the children of each node are not references themselves, if they were we’d just have a very slim XML tree with just references, no content, and then the references at the bottom. Something like this:

Code Snippet
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <XnaContent xmlns:Utils="Baddies.Utils"
  3.             xmlns:Scenes="Baddies.Scenes"
  4.             xmlns:Nodes="Baddies.Nodes">
  5.   
  6.   <!-- Root of the tree-->
  7.   <Asset Type="Utils:NodeSerializer">
  8.     <root>#Resource1</root>
  9.   </Asset>
  10.  
  11.   <!-- References-->
  12.   <Resources>
  13.  
  14.     <!-- Scene node-->
  15.     <Resource ID="#Resource1" Type="Scenes:Scene">
  16.       <Name>Node</Name>
  17.       <ParentRelativePos>false</ParentRelativePos>
  18.       <Children Type="Utils:SharedResourceDictionary[int,Nodes:Node]">
  19.         <Item>
  20.           <Key>0</Key>
  21.           <Value>#Resource2</Value>
  22.         </Item>
  23.         <Item>
  24.           <Key>1</Key>
  25.           <Value>#Resource3</Value>
  26.         </Item>
  27.       </Children>
  28.       <Visible>true</Visible>
  29.       <Position>-0 -0 -0</Position>
  30.       <Parent />
  31.     </Resource>
  32.  
  33.     <!-- Camera node -->
  34.     <Resource ID="#Resource2" Type="Baddies.Camera">
  35.       <Name>Cam</Name>
  36.       <ParentRelativePos>false</ParentRelativePos>
  37.       <Children Type="Utils:SharedResourceDictionary[int,Nodes:Node]" />
  38.       <Visible>true</Visible>
  39.       <Position>0 0 0</Position>
  40.       <Parent>#Resource1</Parent>
  41.       <W>673</W>
  42.       <H>442</H>
  43.       <DisplayBorder>true</DisplayBorder>
  44.     </Resource>
  45.  
  46.     <!-- Map node -->
  47.     <Resource ID="#Resource3" Type="Baddies.Map.MapGrid">
  48.       <Name>Map</Name>
  49.       <ParentRelativePos>true</ParentRelativePos>
  50.       <Children Type="Utils:SharedResourceDictionary[int,Nodes:Node]" />
  51.       <Visible>true</Visible>
  52.       <Position>0 0 0</Position>
  53.       <Parent>#Resource1</Parent>
  54.       <DisplayTerrain>true</DisplayTerrain>
  55.       <DisplayGrid>true</DisplayGrid>
  56.       <DisplayCollisionMap>false</DisplayCollisionMap>
  57.       <MapWidth>16</MapWidth>
  58.       <MapHeight>16</MapHeight>
  59.       <TileSize>16</TileSize>
  60.       <Tiles>
  61.         <!-- Map tiles excuded for brevity -->
  62.       </Tiles>
  63.       <tileSetTex>
  64.         <Name>TileMap/tileset1</Name>
  65.         <contentRef>#Resource3</contentRef>
  66.       </tileSetTex>
  67.     </Resource>
  68.     
  69.   </Resources>
  70. </XnaContent>

To achieve that structure we need to set the children of the Node as references as well. The children of each node are kept in a dictionary, so to solve this “dictionary of shared resources” dilemma, I wrote a class to handle it, explained in this article. This is one of the few exceptions to the “not writing custom code for saving different data” but only because the IntermediateSerializer doesn’t support these type of dictionaries out of the box.

Another exception is that even if we mark everything as references, the first node to be serialized is not considered a reference because it’s the starting point of the serializer. To solve this I made a wrapper class to serialize that only contains the root node, as a reference.

Code Snippet
  1. /// <summary>
  2. /// Auxiliary class that takes care of
  3. /// the serialization of a Node tree.
  4. /// <remarks>
  5. /// Works by having a reference to the
  6. /// node and serializing itself, so the root
  7. /// node is also regarded as a reference.
  8. /// </remarks>
  9. /// </summary>
  10. public class NodeSerializer
  11. {
  12.     /// <summary>
  13.     /// Root node to serialize.
  14.     /// </summary>
  15.     [ContentSerializerAttribute(SharedResource = true)]
  16.     private Node root;
  17.  
  18.     /// <summary>
  19.     /// Serializes a node to XML.
  20.     /// </summary>
  21.     /// <param name="node">Node to
  22.     /// serialize.</param>
  23.     /// <param name="name">Name of the
  24.     /// file to serialize to.</param>
  25.     public void Serialize(Node node, string name)
  26.     {
  27.         this.root = node;
  28.  
  29.         XmlWriterSettings settings =
  30.             new XmlWriterSettings();
  31.         settings.Indent = true;
  32.  
  33.         using (XmlWriter writer =
  34.             XmlWriter.Create(name, settings))
  35.         {
  36.             IntermediateSerializer.
  37.                 Serialize(writer, this, null);
  38.         }
  39.     }
  40.  
  41.     /// <summary>
  42.     /// Deserializes the XML file provided
  43.     /// and returns the created node.
  44.     /// </summary>
  45.     /// <param name="name">Name of the xml file.</param>
  46.     /// <returns>Node serialized in that file.</returns>
  47.     public Node Deserialize(string name)
  48.     {
  49.         Node node = null;
  50.         XmlReaderSettings settings =
  51.             new XmlReaderSettings();
  52.  
  53.         using (XmlReader reader =
  54.             XmlReader.Create(name, settings))
  55.         {
  56.             NodeSerializer serial =
  57.                 IntermediateSerializer.
  58.                 Deserialize<NodeSerializer>(reader, null);
  59.             node = serial.root;
  60.         }
  61.  
  62.         return node;
  63.     }
  64. }

The last exception comes with the Texture2D class. I wanted that the serializing of a tree included the actual textures used, as not to have to do a “second” step when loading. This has 2 problems. First, the texture is binary data, so kind of difficult to serialize in xml. Second, the texture has a reference to the ContentManager that loaded it, so it creates a circular dependency that there is no way to solve, short of dumping the use of the ContentManager all together.

To work around these 2 issues I created a Texture2D proxy class, as shown here:

Code Snippet
  1. public class Texture2DProxy
  2. {
  3.     /// <summary>
  4.     /// Reference to the the parent class
  5.     /// that holds a ContentManager to load
  6.     /// this texture.
  7.     /// </summary>
  8.     [ContentSerializerAttribute(SharedResource = true)]
  9.     private IContentHolder contentRef;
  10.  
  11.     /// <summary>
  12.     /// Texture we wrap.
  13.     /// </summary>
  14.     private Texture2D texture;
  15.  
  16.     /// <summary>
  17.     /// Name of the texture in the
  18.     /// ContentManager.
  19.     /// </summary>
  20.     private string name;
  21.  
  22.     /// <summary>
  23.     /// Initializes a new instance of
  24.     /// the Texture2DProxy class.
  25.     /// </summary>
  26.     /// <param name="contentRef">
  27.     /// Content manager that will be
  28.     /// used for loading.</param>
  29.     public Texture2DProxy(IContentHolder contentRef)
  30.     {
  31.         this.contentRef = contentRef;
  32.         this.name = string.Empty;
  33.         this.texture = null;
  34.     }
  35.  
  36.     /// <summary>
  37.     /// Initializes a new instance of
  38.     /// the Texture2DProxy class.
  39.     /// </summary>
  40.     public Texture2DProxy()
  41.     {
  42.         this.contentRef = null;
  43.         this.name = string.Empty;
  44.         this.texture = null;
  45.     }
  46.  
  47.     /// <summary>
  48.     /// Gets or sets the name of the texture.
  49.     /// </summary>
  50.     /// <value>Name of the texture.</value>
  51.     public string Name
  52.     {
  53.         set { this.name = value; }
  54.         get { return this.name; }
  55.     }
  56.  
  57.     /// <summary>
  58.     /// Gets or sets the texture of the proxy.
  59.     /// <remarks>
  60.     /// The Get method has lazy
  61.     /// initialization of the texture,
  62.     /// in the sense that if we have the
  63.     /// texture name and not the texture,
  64.     /// it loads it when we request the
  65.     /// texture. This is useful for when
  66.     /// we load a texture via serialization.
  67.     /// </remarks>
  68.     /// </summary>
  69.     /// <value>Texture that we wrap.</value>
  70.     [ContentSerializerIgnore]
  71.     public Texture2D Texture
  72.     {
  73.         get
  74.         {
  75.             if (this.texture == null &&
  76.                 this.name != string.Empty)
  77.             {
  78.                 ContentManager man =
  79.                     this.contentRef.GetContent();
  80.  
  81.                 this.texture =
  82.                     this.contentRef.GetContent().
  83.                     Load<Texture2D>(this.name);
  84.                 
  85.             }
  86.  
  87.             return this.texture;
  88.         }
  89.  
  90.         set
  91.         {
  92.             this.texture = value;
  93.         }
  94.     }
  95.  
  96.     /// <summary>
  97.     /// Loads the selected texture.
  98.     /// </summary>
  99.     /// <param name="name">
  100.     /// Name of the texture to
  101.     /// load.</param>
  102.     public void Load(string name)
  103.     {
  104.         this.name = name;
  105.  
  106.         this.texture =
  107.             this.contentRef.GetContent().
  108.             Load<Texture2D>(name);
  109.     }
  110. }

What is serialized in the texture is the actual name of the texture, so afterwards we can load the name, and load the actual texture by request in the “get” field.  This makes for a simple method of loading the textures from xml, the only problem is a project can have many ContentManagers running, and we have no way of knowing which one is the one the texture is associated to. To get around this we create a IContentHolder interface.

Code Snippet
  1. /// <summary>
  2. /// Class that has a content manager.
  3. /// </summary>
  4. public interface IContentHolder
  5. {
  6.     /// <summary>
  7.     /// Returns the ContentManager
  8.     /// associated to this class.
  9.     /// </summary>
  10.     /// <returns>A ContentManager object.</returns>
  11.     ContentManager GetContent();
  12. }

The parent class that owns the texture implements this interface, and is required to pass itself to the texture as a reference upon creation. That way the steps when deserializing are as follows:

  1. When the serializer creates the parent class, the parent class creates the Texture2DProxy object and assigns itself as the ContentHolder.
  2. The deserializer fills the serialized data of the Texture2DProxy (the name of the texture),
  3. At some point, the game requests the texture for the first time, in the “get” field we see it’s not loaded yet, so we create it using the ContentManager of our parent, and we’re ready to go.

No comments:

Post a Comment