I did a post on automatic xnb serialization of classes within classes a while back. Now while the method works decently enough, I’ve realized it’s probably not the best way of going about doing it. I’m now using a different method. It’s a little bit trickier, as it involves using custom content writers and readers, but I think the overall result is neater code. There’s a lot of stuff to cover here, so I’m going to try and split the content into four parts. This part will just be about how to setup a simple content writer/read pair, in case you’re not familiar with how to do it.
One of the major drawbacks of my other method of loading classes within classes, was that you had to first call content.Load<>() followed by another LoadContent(). This can easily lead to confusion when you reach a large enough number of classes. Some of them have a LoadContent() method, and some don’t. It would be much nicer and easier if we can just load the content without this method call. This is where the custom reader/writer pair comes in.
Instead of tacking on an extra method in the class itself, we can do it as part of the xnb serialization. This way we don’t need to remember whether our class needs to call LoadContent() or not, it’ll just take care of itself.
As a setup you should have 4 projects. The first is the main game project. The second is your Content project, where we will put all images for textures and xml files for classes. The third is what I like to call the “Engine” project, which is just a class library. I use this so I can separate the game from the engine. It also helps avoiding cyclic references between the game and the content projects. The last project is a Content Pipeline Extension Library. This is where we’ll put our content writers. We also need to setup some references.
- The Game project should reference the Engine project.
- The Content project should reference the Engine project and the Content Pipeline Extension project.
- The Engine project should not reference any of the other projects.
- The Content Pipeline Extension project should reference the Engine project.
Eventually we’re going to want to end up with a class hierarchy like this:
We have a very basic hierarchy going on here. Some equipment classes, a couple of player classes and an Armory class. Some of them are very simple and only have string and int properties, while some contain other classes within them.
Very simple classes we don’t need to write a custom writer/reader for, because XNA does this automatically. Here’s an example:
{
public class Weapon : Equipment, IDeepCloneable
{
public int Damage { get; set; }
public object Clone()
{
Weapon weapon = new Weapon();
weapon.Name = this.Name;
weapon.Price = this.Price;
weapon.Damage = this.Damage;
return weapon;
}
}
}
{
public abstract class Equipment : GameObject
{
public int Price { get; set; }
}
}
{
public abstract class GameObject
{
public string Name { get; set; }
public GameObject ()
{
this.Name = this.GetHashCode().ToString();
}
public GameObject(string name)
{
this.Name = name;
}
}
}
{
/// <summary>
/// An interface that allows deep cloning
/// </summary>
public interface IDeepCloneable
{
/// <summary>
/// Creates and returns a deep copy of the current object
/// </summary>
/// <returns>A deep copy of the current object</returns>
object Clone();
}
}
We only need to create our own reader/writer pair when we want to do things that the built-in mechanisms won’t do for us. One of these things is the classes-within-classes behaviour that I mentioned in my previous post. Another common task is to load textures. I’ll take a look at both of these tasks in part 2 and 4 respectively. In part 3 I’ll take a look at easing up reading and writing inherited classes.
But, to ease into it, we’re going to create a custom reader/writer for our Weapon class even though we don’t need to.
In your Engine project, create class hierarchy from above. Then in your extension library project, add a new item called “Content Type Writer”. Name it WeaponContentWriter. It’ll look a little something like this:
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
// TODO: replace this with the type you want to write out.
using TWrite = System.String;
namespace ExLib
{
/// <summary>
/// This class will be instantiated by the XNA Framework Content Pipeline
/// to write the specified data type into binary .xnb format.
///
/// This should be part of a Content Pipeline Extension Library project.
/// </summary>
[ContentTypeWriter]
public class WeaponContentWriter : ContentTypeWriter<TWrite>
{
protected override void Write(ContentWriter output, TWrite value)
{
// TODO: write the specified value to the output ContentWriter.
throw new NotImplementedException();
}
public override string GetRuntimeReader(TargetPlatform targetPlatform)
{
// TODO: change this to the name of your ContentTypeReader
// class which will be used to load this data.
return "MyNamespace.MyContentReader, MyGameAssembly";
}
}
}
There’s two methods to this class. I’m a little fuzzy on the details here, but this is my best guess. The first method, Write(), is the method that’s called from the content pipeline to write whatever the pipeline has into a serialized object that ends up being saved as an XNB file. The second method, GetRuntimeReader(), is used to figure out which content reader to use at runtime to read the XNB that we’ve created (hint: it’s the one we’ll create later in this blog post).
The first thing we want to do is to specify exactly what king of object we’re serializing here. Basically all we need to do is change the line:
to
Personally I prefer not to use the using alias they’ve got going here, so I instead change all instances of TWrite to Engine.Weapon, and remove the alias. It’s up to you however to do whatever you want though. If you do it my way, you’ll be looking at something like this:
using Engine;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
namespace ExLib
{
/// <summary>
/// This class will be instantiated by the XNA Framework Content Pipeline
/// to write the specified data type into binary .xnb format.
///
/// This should be part of a Content Pipeline Extension Library project.
/// </summary>
[ContentTypeWriter]
public class WeaponContentWriter : ContentTypeWriter<Weapon>
{
protected override void Write(ContentWriter output, Weapon value)
{
// TODO: write the specified value to the output ContentWriter.
throw new NotImplementedException();
}
public override string GetRuntimeReader(TargetPlatform targetPlatform)
{
// TODO: change this to the name of your ContentTypeReader
// class which will be used to load this data.
return "MyNamespace.MyContentReader, MyGameAssembly";
}
}
}
Now we have to take care of the actual serialization of the object. Basically what we’re going to do is to take every single property of the object and write it to an output object. Essentially, the object we want to serialize is the “value” object parameter, while the object we want to write to is the “output” object parameter.
Ok that’s a bit confusing. Here’s a chart to hopefully make it a bit more clear:
The XML file comes in from the left. There’s a bunch of magic happening there but it eventually ends up as the value parameter of our Write() method. Then we manually convert write it to the output object. The output object is then written to disk as a XNB file. If you’re interested in the “Magic” part of this, check out this post by Shawn Hargreaves.
Now the question is, how do we actually write something to this output object. Well, it’s pretty simple. The output object has about five or so write methods that you can call, depending on what you want to write. As we can see from the code further up, our Weapon object has one string and two ints. For these simple data types we’ll use two (out of 24) different overloads of the Write() method.
{
output.Write(value.Name);
output.Write(value.Price);
output.Write(value.Damage);
}
That’s it. When the content writer is called, it will now write each of the properties to the output object in order. XNA will take care of the rest and save that object as an XNB file.
Please note that it’s important which order you write your properties in! You can write them in any order in the ContentWriter, but you have to make sure that you’re reading them in the exact same order in your ContentReader.
Which brings us to our next task. Creating a ContentReader class for our Weapon class. The ContentReader is what reads our XNB file and converts it into the Weapon class. I like to stick the ContentReader at the bottom of the same file of the class which it will read. A lot of people thinks having multiple classes in one code file is bad practice, and I somewhat subscribe to this chain of thought, but it does make it a little cleaner and easier to find your code if you do it this way. Anyway, adjust your Weapon class to look something like this:
namespace Engine
{
public class Weapon : Equipment, IDeepCloneable
{
public int Damage { get; set; }
public object Clone()
{
Weapon weapon = new Weapon();
weapon.Name = this.Name;
weapon.Price = this.Price;
weapon.Damage = this.Damage;
return weapon;
}
}
public class WeaponContentReader : ContentTypeReader<Weapon>
{
protected override Weapon Read(ContentReader input, Weapon existingInstance)
{
Weapon weapon = existingInstance;
if (weapon == null)
{
weapon = new Weapon();
}
return weapon;
}
}
}
We now have two classes in our code file. The first is the actual class we’ll use in the game. The second is the class we’ll use to read our class from the XNB file. There’s just the one method, which is Read(). It’s called at run time by the Content.Load() method. Right now it doesn’t do much. It sets a Weapon object to an existing instance (which can be null, if one doesn’t exist). Then if that object is null (which it will be the first time Read() is called) it creates a new instance of Weapon. Finally it returns the object.
The input variable is the key here, which is what we’ll use to read the XNB file we wrote in our ContentWriter. We can call a wide array of Read() methods here, depending on the type we want to read. Also note that you have to call the Read() methods in the EXACT same order as we wrote them in.
{
Weapon weapon = existingInstance;
if (weapon == null)
{
weapon = new Weapon();
}
weapon.Name = input.ReadString();
weapon.Price = input.ReadInt32();
weapon.Damage = input.ReadInt32();
return weapon;
}
While being careful with the order here, we’re essentially just reading values from the serialized XNB files and assigning them to properties in our Weapon class. Then at the end, we return the instance of the class, which is also what Content.Load() will return when called.
Now we’re almost at the point where we can test our code. But first we have to go back to our content writer and fix our GetRuntimeReader() method. Currently it looks like this:
{
// TODO: change this to the name of your ContentTypeReader
// class which will be used to load this data.
return "MyNamespace.MyContentReader, MyGameAssembly";
}
You could hard code this to match your assembly. That’s a bad idea though. If you ever change something, you’d be updating every single ContentWriter. So rather, we’ll do this:
{
return typeof(WeaponContentReader).AssemblyQualifiedName;
}
Finally, the very last thing we need to do is to create an XML file to represent our class and then create an instance of our class in the Game1.cs file. In your content project, create an XML file called Weapon1.xml and paste in this:
<XnaContent>
<Asset Type="Engine.Weapon">
<Name>Cool Sword</Name>
<Price>100</Price>
<Damage>12</Damage>
</Asset>
</XnaContent>
In the Game1.cs file, put this in your LoadContent() method.
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
Weapon weapon = Content.Load<Weapon>("Weapon1");
}
Assuming you have all the references setup correctly, you should get the following if you run and debug the game:
So what does this do for us that the automatic serialization doesn’t do? Well… Nothing. But it should give you a basic understanding of how to write a custom content reader/writer pair. In part 2 of this series I’ll take a look at how to simplify loading of nested classes in our XML files.
Leave a Reply