This will probably be the last post in this series, at least for now. In part one I looked at the basics on how to create my own custom writer/reader. If you’re unfamiliar with the process, you might want read that to begin with. In the second part I showed how to load lists of classes by referring to other XML files. In the third part I showed how you can easily use this technique to load textures and other classes. In this fourth and final part, I’m going to show how you can make life a little easier for yourself when you are loading derived classes.
Here’s the scenario. You’re about 6 levels deep in derived classes, and each of them have 20 properties and/or fields each. Now you want to implement your own writer/reader, and the lower down in the hierarchy you get the more stuff you have to manually write and read. In your first object you only have 20. But in your second, you have the the ones from your derived object and the ones from your base object, so now you’re counting 40. Then at the third level, suddenly you have another 20 so now you’ve got to do 60 in total. And so it grows every time you inherit.
Sure you could just do copy and paste from your past object, which would work reasonably enough… But what If you change or add an attribute in your base class? Now you’ve got to go through 6 content writers and readers and change them all. You’re going to have to keep your tongue pretty straight in your mouth to not mess up.
Lucky for you, there’s an easier way to do this.
First of all, I’m going to create a new class that inherits the Player class from my last example. I’ll name it FooPlayer. Because every game needs a little more Foo in it (in retrospect, maybe I should have called it CowBellPlayer). FooPlayer isn’t much different from a regular Player.
namespace Engine
{
public class FooPlayer : Player, IDeepCloneable
{
/// <summary>
/// Gets or sets the player weapon
/// </summary>
[ContentSerializerIgnore]
public Weapon Weapon { get; set; }
/// <summary>
/// Gets or sets the name of the Weapon asset
/// </summary>
public string WeaponAsset { get; set; }
/// <summary>
/// Creates a deep clone of the current player
/// </summary>
/// <returns>A copy of the current player</returns>
public override object Clone()
{
FooPlayer p = new FooPlayer();
// Base class
p.Name = this.Name;
p.Armor = this.Armor.Clone() as Armor;
p.ArmorAsset = this.ArmorAsset;
p.Texture = this.Texture;
p.TextureAsset = this.TextureAsset;
// Blah class
p.Weapon = this.Weapon.Clone() as Weapon;
p.WeaponAsset = this.WeaponAsset;
return p;
}
}
}
As you can see from the code, the only thing we’re introducing is the addition of a Weapon class.
The custom content writer and reader we’ll create in the same fashion as before, but we’re going to add something extra to our writer this time around. Let’s imagine that our base Player class has 42 properties and fields on it. If we do it the same as before, we’d have to to all of those 42, in addition to the new properties and fields we introduced in our FooPlayer class. So now I’m calling output.Write() on 42 properties/fields of my base class, and on however many properties/fields I created in my FooPlayer class.
Well, we could just copy and paste… But then if we make a change to our Player class, we have to also remember to update our FooPlayer class. And then when I realize that I really should have a CowBellPlayer class, and I make that inherit from the FooPlayer class… Well now we have to edit three classes if we make any changes to the base Player class. Bleck!
There’s got to be an easier way to do this, right? The answer is; Of course. We’ve already created a custom writer for the Player class so why don’t we just use that? That way, if we need to make any changes, the FooPlayer class doesn’t need to be touched because it’s using the Player writer which is where we made the changes.
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 FooPlayerContentWriter : ContentTypeWriter<FooPlayer>
{
PlayerContentWriter playerWriter = null;
protected override void Initialize(ContentCompiler compiler)
{
playerWriter = compiler.GetTypeWriter(typeof(Player)) as PlayerContentWriter;
base.Initialize(compiler);
}
protected override void Write(ContentWriter output, FooPlayer value)
{
output.WriteRawObject<Player>(value as Player, playerWriter);
output.Write(value.WeaponAsset);
}
public override string GetRuntimeReader(TargetPlatform targetPlatform)
{
return typeof(FooPlayerContentReader).AssemblyQualifiedName;
}
}
}
Notice how I’ve declared a PlayerContentWriter field in our FooPlayerContentWriter class. Then I override the Initialize() method and get an instance of the writer for the Player class from the compiler. Next in my overriden Write() method, I start out by calling the WriteRawObject() method, specifying that I want to serialize a Player class. Then I pass in the playerWriter object, essentially telling it that I want to use that custom writer to write the Player class to the serialized stream.
So now, when the FooPlayer class is being serialized, it first serializes all the fields/properties declared in the Player class, before it moves on to the ones we’ve specifically created in our FooPlayer class (which is only WeaponAsset).
For our reader it’s as easy as saying we want to read a raw object of type Player, before we read all the specific FooPlayer properties/fields.
{
protected override FooPlayer Read(ContentReader input, FooPlayer existingInstance)
{
FooPlayer player = existingInstance;
if (player == null)
{
player = new FooPlayer();
}
// Read the Player settings
input.ReadRawObject<Player>(player as Player);
// Read the rest of the stuff
player.WeaponAsset = input.ReadString();
player.Weapon = input.ContentManager.Load<Weapon>(player.WeaponAsset).Clone() as Weapon;
return player;
}
}
And that’s all there is to it. When we compile, the FooPlayer writer will serialize the Player part of itself, then the rest. At runtime when we load, the FooPlayer reader will deserialize the Player part of itself, then the rest.
To test it out, let’s create an XML file for our FooPlayer called Player2.xml.
<XnaContent>
<Asset Type="Engine.FooPlayer">
<Name>Timmy</Name>
<ArmorAsset>Armor2</ArmorAsset>
<TextureAsset>PlayerRed</TextureAsset>
<WeaponAsset>Weapon2</WeaponAsset>
</Asset>
</XnaContent>
I also created an Armor2.xml and a Weapon2.xml file. In the LoadContent() section I added another line like this:
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
Weapon weapon = Content.Load<Weapon>("Weapon1");
Armory armory = Content.Load<Armory>("Armory1");
player = Content.Load<Player>("Player1");
FooPlayer fooPlayer = Content.Load<FooPlayer>("Player2").Clone() as FooPlayer;
}
Putting in a break point and looking at the result, we end up with this:

That’s all there is to it. You can now make all kinds of crazy hierarchy of classes with custom writers/readers, but still maintain your sanity when it comes to serializing and deserializing them.
You may have noticed that our Clone() method suffers from somewhat of the same problem. I’ve been looking into a few ways of solving this, but so far I haven’t come up with a good solution.
Leave a Reply