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

XNA custom content writer/reader part 4: Dealing with inheritance and derived classes

Posted on November 4, 2010
VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)

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.

using Microsoft.Xna.Framework.Content;

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 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 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.

public class FooPlayerContentReader : ContentTypeReader<FooPlayer>
{
    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.

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

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");
    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:

A derived class loaded with it's custom writer/reader pair

A derived class loaded with it's custom writer/reader pair

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.

Comments (4) Trackbacks (0)
  1. Thank you for these posts. I have learned so much from them and it has solved many of my problems that I’ve been having.

  2. I have followed ur tutorial to the letter, everything works fine but I need to know how to use the writer to actually write the xml instead of having to make the file myself. I assume that the writer is just an overload of something else but I need to know what it’s overloading so I can call it and write to disc. thanks :)

    • Hi Daniel,

      The ContentTypeWriter doesn’t write to XML files. It only writes to a binary format (.xnb) that can be easily loaded into memory. This is used so that you can easily create your objects like enemies, armor, weapons, etc. in XML and load them into your game.

      If you want to write a class (or something else) to an xml format, you should use IntermediateSerializer.

  3. My content hierarchy works a bit differently. First, my game objects are aggregated in what some would call a scene graph. The content class hierarchy works like this:

    StateContent -> List, PhysicsContent, List(optional)
    SpriteContent -> List, List(optional)
    SpriteEntryContent -> ImageContent, TransformContent, List(optional)
    BehaviorContent -> List, List

    The game objects themselves look similar but cannot inherit from the content types. Many of the data fields that are public by necessity in the content types are protected or even private in the object types.

    GameState-> List, Physics, List(optional)
    Sprite-> List, List(optional)
    SpriteEntry-> Image, Transform, Animation(optional)
    Behavior-> List, List

    I can’t just pull data from external files and use it unmodified. Sprites in particular need to be individually placed and oriented in the GameState even if the rest of their data is identical. I left behind automatic serialization a while back in favor of XDocument to allow for attributes and optional elements.

    My ContentProcessor classes have an attribute-based messaging system whereby I can, and need to, aggregate the data I collect for each GameState into one massive StateContent .xnb. The ContentProcessorContext has a Convert method that uses the DisplayName attribute of the other processors to do this. Writing, and especially reading, these objects is proving more cumbersome as they lack such amenities.


Leave a comment

 

No trackbacks yet.