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

Automatic XNB serialization of classes contained within classes

Posted on August 10, 2010
VN:F [1.9.22_1171]
Rating: 5.0/5 (1 vote cast)

I've been playing around with XNA for a few months now, although I have nearly nothing to show for it so far, but I figured I should probably start writing down my thoughts so I can remember them for later.

Right now I'm in the early stages of creating a Tower Defense game. Yes I know, very original. But I figure it's a very good game type for a learning experience. There's a wide range of things you need to figure out, but the scope of the game itself is not too big that it becomes overwhelming.

Update: I've written a series of posts explaining a different, and arguably better, way of handling this. You can read the first one here.

One of the great new things of XNA 3.1 and newer is the automatic XNB serialization of content. In my simple little game, I plan to have 5-6 levels. Each of those should have somewhere between 10 and 50 waves. Each wave should have maybe 20-30 monsters. I used class diagrams to build an object diagram, with a nice hierarchy.

Tower Defense object diagram

An object diagram of my Tower Defense game in it's very early stages.

I've never really done anything with UML before, but I found it surprisingly easy and fun to build a complete object inheritance hierarchy. I keep going back to the diagram for reference and I find it a great way to wrap my head around all my classes and what's actually going on.

One of the things that I really wanted to be able to do, was to define levels, waves and monsters as XML and automatically map them to their C# classes. XNA lets you do this starting with version 3.1. Great! Here's an example of one of my monsters in it's file Bug.xml:

<?xml version="1.0" encoding="utf-8" ?>
<XnaContent>
  <Asset Type="GameEngine.Monster">
    <Location>350 25</Location>
    <IsActive>true</IsActive>
    <SimpleGraphics>x</SimpleGraphics>
    <Color>FFCC00FF</Color>
    <Speed>10</Speed>
    <Path >10 10 20 20</Path>
    <MoneyReward>10</MoneyReward>
  </Asset>
</XnaContent>

With this I can easily load my monster into my code by just doing this in my LoadContent() method in GameplayScreen.cs (I'm using the Game State Management Sample):

Monster m = content.Load<Monster>(@"Game\Monsters\Bug");

Of course, I don't want to be hard-coding my monsters for each wave in my GameplayScreen.cs, so I ventured out to figure out a way to specify in my Wave1.xml which monsters to spawn for each wave. Then in my Level1.xml, I want to specify which wave to spawn doing the same thing. Because the serialization handles List<> just fine, I could do something like this in my Wave1.xml:

<?xml version="1.0" encoding="utf-8" ?>
<XnaContent>
  <Asset Type="GameEngine.Wave">
    <Monsters>
      <Item>
        <Location>350 25</Location>
        <IsActive>true</IsActive>
        <SimpleGraphics>x</SimpleGraphics>
        <Color>FFCC00FF</Color>
        <Speed>10</Speed>
        <Path >10 10 20 20</Path>
        <MoneyReward>10</MoneyReward>
      </Item>
      <Item>
        <Location>300 20</Location>
        <IsActive>true</IsActive>
        <SimpleGraphics>y</SimpleGraphics>
        <Color>FFCC00FF</Color>
        <Speed>12</Speed>
        <Path >10 10 20 20</Path>
        <MoneyReward>10</MoneyReward>
      </Item>
    </Monsters>
  </Asset>
</XnaContent>

But that seems very cumbersome. I'd have to specify each of my monsters full stats in the Wave.xml, so if I have 30 monsters in a wave, the xml file will be huge. Not to mention that it'd be prone to errors. The other problem is, I want to embed my Wave object inside my Level object, which means I'd have to put the entire wave inside the Level1.xml. For each individual wave!

Imagine an XML file containing 50 waves with 30 monsters each? That'd be one monster XML file.

So I set out to figure out a way to call other XML files, from inside other XML files. Obviously I'm still pretty fresh with XNA, so there might be an easier way to do this. I'm not sure if my method is good or not, I guess I'll find out. But here's what I came up with.

I created another Property on my Level and Wave objects called WaveList and MonsterList respectively. They're just a simple List<string>. Then, when I create the XML for these objects, I simply enter the string of the path of the XML file for the monster (or wave) that I want to load in my wave (or level). Here is my Wave1.xml:

<?xml version="1.0" encoding="utf-8" ?>
<XnaContent>
  <Asset Type="GameEngine.Wave">
    <MonsterList>
      <Item>Game\Monsters\Bug</Item>
      <Item>Game\Monsters\Bug</Item>
      <Item>Game\Monsters\Bug</Item>
      <Item>Game\Monsters\Bug</Item>
      <Item>Game\Monsters\Bug</Item>
      <Item>Game\Monsters\Bug2</Item>
      <Item>Game\Monsters\Bug2</Item>
      <Item>Game\Monsters\Bug2</Item>
      <Item>Game\Monsters\Bug2</Item>
      <Item>Game\Monsters\Bug2</Item>
    </MonsterList>
  </Asset>
</XnaContent>

This is early prototyping here, so there's no start delay between all the monsters, but please bear with me. In my Wave object, I've added the following code:

protected List<Monster> monsters;
protected List<string> monsterList;

[ContentSerializerIgnore]
public List<Monster> Monsters
{
    get { return monsters; }
    set { monsters = value; }
}

public List<string> MonsterList
{
    get { return monsterList; }
    set { monsterList = value; }
}

public Wave()
{
    this.monsters = new List<Monster>();
}

public void LoadContent(ContentManager content)
{
    for (int x = 0; x < this.monsterList.Count; x++)
    {
        this.monsters.Add(content.Load<Monster>(this.monsterList[x]));
    }
}

So what's going on here? Well, we're passing in the content manager into the LoadContent method of the Monster object. Then I'm looping through the List<string> MonsterList and grabbing the location of the xml file of the Monster object we want to instantiate. Then we pass that string into the content manager Load method, instantiating a new Monster object based on that file.

Rinse and repeat for the Wave objects. Here's my Level1.xml file:

<XnaContent>
  <Asset Type="GameEngine.Level">
    <PlayerLife>20</PlayerLife>
    <WaveList>
      <Item>Game\Levels\Level_1\Wave1</Item>
      <Item>Game\Levels\Level_1\Wave2</Item>
      <Item>Game\Levels\Level_1\Wave3</Item>
      <Item>Game\Levels\Level_1\Wave4</Item>
    </WaveList>
  </Asset>
</XnaContent>

And here's the code from my Level object:

protected List<string> waveList;
protected List<Wave> waves;

public Level()
{
    this.waveCurrent = 0;
    this.waves = new List<Wave>();
}

[ContentSerializerIgnore]
public List<Wave> Waves
{
    get { return waves; }
}

public List<string> WaveList
{
    get { return waveList; }
    set { waveList = value; }
}

public void LoadContent(ContentManager content)
{
    for (int x = 0; x < this.waveList.Count; x++)
    {
        this.waves.Add(content.Load<Wave>(this.waveList[x]));
        this.waves[x].LoadContent(content);
    }
}

We're doing the same thing here. We pass in the content manager to to the LoadContent method. Then we loop through the List<string> WaveList and instantiate a new Wave object for each item in the list. Then we call the LoadContent method on each of those waves, which again instantiates each monster for that wave (as in my example above).

Then from my GameplayScreen.cs all I have to do is the following:

class GameplayScreen : GameScreen
{
    ContentManager content;
    Level l;

    // Constructors and other fluff here

    /// <summary>
    /// Load graphics content for the game.
    /// </summary>
    public override void LoadContent()
    {
        if (content == null)
        {
            content = new ContentManager(ScreenManager.Game.Services, "Content");
        }

        l = content.Load<Level>(@"Game\Levels\Level_1\Level");
        l.LoadContent(content);

        // once the load has finished, we use ResetElapsedTime to tell the game's
        // timing mechanism that we have just finished a very long frame, and that
        // it should not try to catch up.
        ScreenManager.Game.ResetElapsedTime();
    }

    // UnloadContent(), Update(), Draw() and other fluff here
}

Notice how I just load the level using the content manager, and then call the LoadContent() method. Boom! All my waves for that level, with their individual monsters each, are now loaded and ready to go. Obviously this is still early in the code, so the monsters don't move yet, and they aren't spawned in delayed waves but rather all at once. But all I wanted to test was how to spawn objects from within other objects, but still utilize the excellent automatic serialization.

Like I said, I don't know if this is the most flexible and efficient way to do it, but it sure does work. Use with caution.

Automatic XNB serialization of classes contained within classes, 5.0 out of 5 based on 1 rating
Comments (3) Trackbacks (1)
  1. Looks great! You should see if you can take advantage of multi-threading and/or parallel processing additions to .NET 4.

  2. This is pretty much what I do. Sadly, it’s not very automated :S

    • I’ve actually started using custom content readers and writers instead of doing it this way. It’s still not as automated as I’d like it to be, but the benefit is that I don’t have to call the LoadContent() on all my classes anymore.

      I’ll try and remember to post an example later this week.


Leave a comment