aarebrot.net Frode's blog on Sharepoint and other stuff

XNA custom content writer/reader part 1: Introduction

Posted on November 1, 2010
VN:F [1.9.22_1171]
Rating: 4.9/5 (8 votes cast)

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.

  1. The Game project should reference the Engine project.
  2. The Content project should reference the Engine project and the Content Pipeline Extension project.
  3. The Engine project should not reference any of the other projects.
  4. 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:

Class hierarchy

The final set of classes we'll end up with

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:

Weapon.cs   
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;
        }
    }
}
namespace Engine
{
    public abstract class Equipment : GameObject
    {
        public int Price { get; set; }
    }
}
namespace Engine
{
    public abstract class GameObject
    {
        public string Name { get; set; }

        public GameObject ()
        {
            this.Name = this.GetHashCode().ToString();
        }

        public GameObject(string name)
        {
            this.Name = name;
        }
    }
}
namespace Engine
{
    /// <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;
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:

using TWrite = System.String;

to

using TWrite = Engine.Weapon;

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 System;
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:

Content reader/writer chart

A truncated story of the events from XML file to XNB file

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.

protected override void Write(ContentWriter output, Weapon value)
{
    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:

using Microsoft.Xna.Framework.Content;
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.

protected override Weapon Read(ContentReader input, Weapon existingInstance)
{
    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:

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";
}

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:

public override string GetRuntimeReader(TargetPlatform targetPlatform)
{
    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:

<?xml version="1.0" encoding="utf-8" ?>
<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.

protected override void LoadContent()
{
    // 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:

Custom content writer/reader end result

After the class has been loaded from the content pipeline, we can see that it loaded the XML file properly

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.

XNA custom content writer/reader part 1: Introduction, 4.9 out of 5 based on 8 ratings
Comments (6) Trackbacks (3)
  1. What is the purpose of using XML if you have to read/write the values in the correct order?

    • It allows you to separate code from content. This is extremely handy if you have a large number of different kinds of objects in your game. Let’s say based on my example I have 100 different kinds of weapons in my game and I don’t use the content pipeline. I’d have to specify the name, price and damage of each weapon in code, which means I’d probably do it wherever I needed to spawn that particular type of weapon. Now I release my game and it turns out that one of my weapons needs to be tweaked, so now I have to hunt down every place in my code where I created it.

      If you separate code from content, all you have to do is go and change the XML file. Now every place where that particular XML file is called will be creating the new updated weapon instead of the original.

      There are a lot of ways you can accomplish this and the most common way is to use the content pipeline. It’s a very powerful way of importing your assets into your game.

      Part 1 doesn’t really show anything that the content pipeline doesn’t already do, so if you haven’t already, I suggest you read the next three parts of this series as well.

  2. I have a problem with your example. Basically I followed the tutorial with a really simple example but the content writer doesn’t seems to pick up. It just seems the automatic xnb serialisation does all the work. Did I miss soemthing. There is no error or so but in the writer I did alter the data but in game the data appears like in the xml file.

    • Make sure to create the corresponding ContentReader class, and that it’s correctly hooked up correctly. For example, make sure it’s inheriting from ContentTypeReader<> base class. Also make sure that you put the ContentWriter in an Content Pipeline Extension project, and that the different projects are referencing the the other projects correctly.

  3. Hi ,
    im starting with xna and im trying to do some like type of engine,
    i have a question , how can i use the function of contentwriter ?
    i mean, i created that function as you did in this tutorial but i dont see when or how this function is used.
    could you please hep me =D

    • If you’re just starting, I suggest you start with something really simple like Pong or Tetris. Don’t start by building an engine. You’ll be burned out before you know it.

      The article is pretty clear on what the ContentWriter and ContentReader are for.

      The ContentWriter is what takes an in memory object (created from XML) and turns it into a binary file representation (the .xnb file). It is called by XNA when you build your project and should be placed in the pipeline extension library.

      The ContentReader is what loads the binary file representation (the .xnb file) and turns it into an instance of the class. It’s called at run-time by the content pipeline and should be placed in the same project as the class it belongs to (I tend to put it at the bottom of the same class, but some people don’t like multiple classes in a single file).

      If you want to explore this area more, you should complete all the steps in the article (creating all the projects, etc). Not just the one method as it won’t do any good on it’s own.


Leave a comment