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

XNA custom content writer/reader part 2: Reading XML files that have classes within classes

Posted on November 2, 2010
VN:F [1.9.22_1171]
Rating: 5.0/5 (5 votes cast)

In the last post of this series I showed how to write a simple content writer/reader pair. That basic example didn't give you any benefits compared to the automated way of doing it, however it was a simple introduction on how to write a content writer/reader pair. In this post I'm going to take a look at how to create XML files that reference other classes inside of them. This is an alternate way of doing it from my post back in August.

The first time around I was trying to figure out how to have multiple monsters inside a wave, with multiple waves in a level. To ease things a bit, I'm going to use a different example this time. In the last part we created a Weapon class. This time we'll create a Armory class which can contain multiple Weapons. We could use this as an inventory that a player can purchase or select weapons from. The details aren't really important, only the implementation.

Let's start out by creating our Armory class.

Armory.cs   
using System.Collections.Generic;
using Microsoft.Xna.Framework.Content;

namespace Engine
{
    public class Armory : GameObject
    {
        [ContentSerializerIgnore]
        public List<Armor> Armors { get; set; }

        [ContentSerializerIgnore]
        public List<Weapon> Weapons { get; set; }

        public List<string> ArmorList { get; set; }
        public List<string> WeaponList { get; set; }

        public Armory()
        {
            this.Armors = new List<Armor>();
            this.Weapons = new List<Weapon>();
        }
    }
}

In our Armory class we have four properties and a default constructor. The first two properties are a list of Weapons and Armors. The Weapon class is the same one that I created in my last post. The Armor class is a very similar class that looks like this:

Armor.cs   
namespace Engine
{
    public class Armor : Equipment, IDeepCloneable
    {
        public int DmgBlock { get; set; }

        public object Clone()
        {
            Armor armor = new Armor();

            armor.DmgBlock = this.DmgBlock;
            armor.Name = this.Name;

            return armor;
        }
    }
}

The last two properties of our Armory class are lists of strings. These are what we'll use to tell the content reader exactly which XNB file to use when instantiating the Weapon or Armor classes.

But first, let's see what we're actually trying to avoid doing and why. If we used the automatic serialization, we'd have to do something like this in our XML file:

<?xml version="1.0" encoding="utf-8" ?>
<XnaContent>
  <Asset Type="Engine.Armory">
    <Name>Awesome Store</Name>
    <Weapons>
      <Item>
        <Name>Cool Sword</Name>
        <Price>100</Price>
        <Damage>12</Damage>
      </Item>
      <Item>
        <Name>Cool Sword</Name>
        <Price>100</Price>
        <Damage>12</Damage>
      </Item>
      <Item>
        <Name>Cool Axe</Name>
        <Price>100</Price>
        <Damage>15</Damage>
      </Item>
    </Weapons>
  </Asset>
</XnaContent>

In this case we'd populate the Weapons and Armors classes with two instanses of Cool Sword and one of the Cool Axe. That's not too bad, but imagine if we had 10 of each. And 10 more types of weapons. Now you have quite a bit of equipment to keep track of. You could potentially have a typo in one of your Cool Swords and it'll have a different price, eventhough it's supposed to be the same sword. Or let's say you want to change the price of the Cool Sword. Now you have to change a bunch of XML for them all to be the same. This could easily give you lots and lots of headaches.

This is where our custom List property comes in. By marking the Armor and Weapon properties as [ContentSerializerIgnore], we're telling the serializer to not bother looking for these in the XML. Rather, we create our XML like this:

<?xml version="1.0" encoding="utf-8" ?>
<XnaContent>
  <Asset Type="Engine.Armory">
    <Name>Awesome Store</Name>
    <ArmorList>
      <Item>Armor1</Item>
      <Item>Armor2</Item>
    </ArmorList>
    <WeaponList>
      <Item>Weapon1</Item>
      <Item>Weapon2</Item>
    </WeaponList>
  </Asset>
</XnaContent>

Instead of explicitly defining the classes themselves, we're now just referring to the file name of the weapon or armor we want to create. This way you're cutting down on the likelyhood of having items that are supposed to be identical end up with errors. Or, even better in my opinion, if you decide you want to change the values of your armors or swords, you don't have to change potentially tens or hundreds of entries. Just one. Sweet!

But before that all takes place, we're going to have to finish our custom writer/reader pair. So let's start by creating the writer. If you read my last post, hopefully you know what to do.

using System.Collections.Generic;
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 ArmoryContentWriter : ContentTypeWriter<Armory>
    {
        protected override void Write(ContentWriter output, Armory value)
        {
            output.Write(value.Name);
            output.WriteObject<List<string>>(value.ArmorList);
            output.WriteObject<List<string>>(value.WeaponList);
        }

        public override string GetRuntimeReader(TargetPlatform targetPlatform)
        {
            return typeof(ArmoryContentReader).AssemblyQualifiedName;
        }
    }
}

Because we told the serializer to ignore the Weapon and Armor properties, we don't need to worry about them here. We only want to write the Name, and our two lists to the XNB output. Notice that we're actually using the WriteObject() method call here, instead of the Write() method we're using with our string value.

Also make sure your GetRuntimeReader method points to the correct ContentReader (although we haven't actually created it yet).

Open up your Armory class and add a content writer to the bottom of it, like we did with our Weapon class previously.

public class ArmoryContentReader : ContentTypeReader<Armory>
{
    protected override Armory Read(ContentReader input, Armory existingInstance)
    {
        Armory armory = existingInstance;

        if (armory == null)
        {
            armory = new Armory();
        }

        armory.Name = input.ReadString();
        armory.ArmorList = input.ReadObject<List<string>>();
        armory.WeaponList = input.ReadObject<List<string>>();

        // More stuff to come here

        return armory;
    }
}

Notice that we're using the ReadObject<>() method to read our lists back into the class from the XNB file. Other than that, it's pretty much a carbon copy of our Weapon class. This is where we're getting to the juicy part however. Because we now have the two lists of strings with filenames from the XML file, we can instantiate the specific files into our class.

armory.ArmorList = input.ReadObject<List<string>>();

// Add all armors in the armor list
foreach (string armor in armory.ArmorList)
{
    Armor newArmor = input.ContentManager.Load<Armor>(armor).Clone() as Armor;
    armory.Armors.Add(newArmor);
}

Let's take a closer look. First I'm reading the ArmorList into the ArmorList property of our Armory object. Then I'm looping through each item in that list. For each item, I create a new instance of the Armor class using the content managers Load<>() function. I'm actually doing a clone here, because I want each item to be an individual object, not a reference to the same object (the content manager always returns a reference to the same object every time by default, if it's called multiple times for the same type). Then I just add that new object to the Armors list.

Please note that you'll have to create a default constructor that instantiates a new List of type Armor and Weapon, or your code will have a null object exception when you add to the list. Here's the full code file with both classes:

Armory.cs   
using System.Collections.Generic;
using Microsoft.Xna.Framework.Content;

namespace Engine
{
    public class Armory : GameObject
    {
        [ContentSerializerIgnore]
        public List<Armor> Armors { get; set; }

        [ContentSerializerIgnore]
        public List<Weapon> Weapons { get; set; }

        public List<string> ArmorList { get; set; }
        public List<string> WeaponList { get; set; }

        public Armory()
        {
            this.Armors = new List<Armor>();
            this.Weapons = new List<Weapon>();
        }
    }

    public class ArmoryContentReader : ContentTypeReader<Armory>
    {
        protected override Armory Read(ContentReader input, Armory existingInstance)
        {
            Armory armory = existingInstance;

            if (armory == null)
            {
                armory = new Armory();
            }

            armory.Name = input.ReadString();
            armory.ArmorList = input.ReadObject<List<string>>();
            armory.WeaponList = input.ReadObject<List<string>>();

            // Add all armors in the armor list
            foreach (string armor in armory.ArmorList)
            {
                Armor newArmor = input.ContentManager.Load<Armor>(armor).Clone() as Armor;
                armory.Armors.Add(newArmor);
            }

            // Add all weapons in weapon list
            foreach (string weapon in armory.WeaponList)
            {
                Weapon newWeapon = input.ContentManager.Load<Weapon>(weapon).Clone() as Weapon;
                armory.Weapons.Add(newWeapon);
            }

            return armory;
        }
    }
}

With all that done let's create a new Armory object in our Game1.cs file and try to load it from the content manager.

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

Assuming your project is setup correctly, you should get the following:

Deserialization of XML with classes within other classes

Magically we now have weapons and armors in our armory

What does this do for us that the built-in stuff doesn't? You can now instantiate other classes from within the XML declaration of your class by referring to another XML file. This way you can avoid a lot of headaches from typing errors. You can also change any of the values of your "subclasses" easily without having to edit every XML files they're used in.

I said in my last post that next I'd take a look at making it easier to do inherited classes, but I think I'll save that for part 4 and do loading of textures as part 3 instead because it's very similar to this example. Stay tuned.

XNA custom content writer/reader part 2: Reading XML files that have classes within classes, 5.0 out of 5 based on 5 ratings
Comments (4) Trackbacks (0)
  1. Hey thanks alot. This helped a ton and was the most informative read I found on XML and XNA. Saved alot of headaches. I apprecitate it.

  2. Can you save some data to the xml file ?
    For example to read/save a high score .


Leave a comment

 

No trackbacks yet.