My last post talked about how to easily read XML files with lists of other classes in them. Now the neat thing about this is that you can very easily modify the code to read any kind of class, including textures and sounds. One of the things I find slightly annoying is that you can’t just include a filename in your XML file and have the content pipeline pick up the texture or sound automatically. However, if you’re already writing your own custom writer/reader pair, there’s no reason why we can’t include that functionality ourself.
For our example we’re going to create a class that loads one of our Armor classes that we made in a previous example, and a Texture2D class which is provided by the framework. And since there won’t be much fun in a game unless it has a player, that’s exactly what we’ll create. A Player class.
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
namespace Engine
{
public class Player : GameObject, IDeepCloneable
{
/// <summary>
/// Gets or sets the Armor of the player
/// </summary>
[ContentSerializerIgnore]
public Armor Armor { get; set; }
/// <summary>
/// Gets or sets the Texture of the player
/// </summary>
[ContentSerializerIgnore]
public Texture2D Texture { get; set; }
/// <summary>
/// Gets or sets the name of the Armor asset
/// </summary>
public string ArmorAsset { get; set; }
/// <summary>
/// Gets or sets the name of the Texture asset
/// </summary>
public string TextureAsset { get; set; }
/// <summary>
/// Creates a deep clone of the current player
/// </summary>
/// <returns>A copy of the current player</returns>
public virtual object Clone()
{
Player p = new Player();
// Base class
p.Name = this.Name;
// Player class
p.Armor = this.Armor.Clone() as Armor;
p.ArmorAsset = this.ArmorAsset;
p.Texture = this.Texture;
p.TextureAsset = this.TextureAsset;
return p;
}
public void Draw(SpriteBatch sb, GameTime gameTime)
{
// This poor guy won’t ever move anywhere :(
Rectangle positionSize = new Rectangle(
0, // X position in pixels
0, // Y position in pixels
this.Texture.Width, // Size of X axis
this.Texture.Height // Size of Y axis
);
sb.Draw(this.Texture, positionSize, Color.White);
}
}
}
Now let’s take a look at what’s going on here. Our class inherits from GameObject and the IDeepCloneable, just like our previous classes did. Further it has two string properties, one Armor class property and one Texture2D property. The strings will be used to store the filenames in the XML file so we can load it in the custom content reader. Very much like how we did it in the last example.
The Clone() method is a standard implementation of how to deep clone an object, although notice that I don’t actually clone the texture. Technically this isn’t a true deep clone. Why am I doing it this way? Well, if you do a clone of texture you get an entirely new instance of the texture. This will take up more memory. While this isn’t an issue for a tiny game like this one, it could be an issue if you have hundreds of players/enemies/bullets, etc. Because the texture object itself are unlikely to be modified directly (you’d probably swap if with a different one, if you need to show something different about the player) we can reference the same texture for all our objects. This way instead of having 100s of texture objects, we have only one.
Lastly, we have our Draw method. You’re likely to want to implement the position in a property of its own, and update it in an Update() method, but for simplicity I just made it static here. It’ll draw the texture at the top left position of the gameplay screen, in the height and width of the loaded texture.
We can now declare our class in XML. Here’s what I have:
<XnaContent>
<Asset Type="Engine.Player">
<Name>Frode</Name>
<ArmorAsset>Armor1</ArmorAsset>
<TextureAsset>PlayerGreen</TextureAsset>
</Asset>
</XnaContent>
Because we set Armor and Texture to [ContentSerializerIgnore], we don’t specify those in our XML file. Rather, we use the ArmorAsset and TextureAsset to store the filenames of the files we want to load. Later in our custom reader, those are the values we’ll look at to do just that.
To make this work, you need to have an Armor1.xml and PlayerGreen.png (or another supported image format) in your content project.
Our custom writer is not much different from the previous posts I wrote. You should be able to write it yourself, but here’s mine as a reference:
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 PlayerContentWriter : ContentTypeWriter<Player>
{
protected override void Write(ContentWriter output, Player value)
{
output.Write(value.Name);
output.Write(value.ArmorAsset);
output.Write(value.TextureAsset);
}
public override string GetRuntimeReader(TargetPlatform targetPlatform)
{
return typeof(PlayerContentReader).AssemblyQualifiedName;
}
}
}
It does the same as the previous writers. It takes the values from the value parameter and serializes it in the output parameter. XNA then saves that in an XNB file format on the hard drive.
The final piece of the puzzle is to create our custom reader. Essentially what we’re going to do here is to load all the serialized bits of the XNB file. Then use the strings of ArmorAsset and TextureAsset to load the Armor and Texture before we return the new object.
{
protected override Player Read(ContentReader input, Player existingInstance)
{
Player player = existingInstance;
if (player == null)
{
player = new Player();
}
player.Name = input.ReadString();
player.ArmorAsset = input.ReadString();
player.Armor = input.ContentManager.Load<Armor>(player.ArmorAsset).Clone() as Armor;
player.TextureAsset = input.ReadString();
player.Texture = input.ContentManager.Load<Texture2D>(player.TextureAsset);
return player;
}
}
Not very complicated, huh? Once the ArmorAsset string has been loaded, we use it to load a new unique instance of the Armor class. Then once the TextureAsset string has been loaded, we use it to load the a new (or currently existing) instance of the Texture2D class. And that’s all there is to it.
To test this, I made some changes to my Game1.cs file. First I added a public property to the class like so:
{
/// <summary>
/// This is the main type for your game
/// </summary>
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Player player;
// file continues with constructor and stuff down here
Then I added another line to my LoadContent() method:
{
// 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");
}
And finally, in my Draw() method I added a line to actually draw the Player:
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
player.Draw(spriteBatch, gameTime);
spriteBatch.End();
base.Draw(gameTime);
}
Assuming you have all references and other fun stuff setup right, you should get a player drawn on your screen when you launch the game.

Ok, maybe I should have picked a bigger picture. But that green 16×16 pixel guy in the corner there, is the PlayerGreen.png file from my project.
The nice thing about this method is that you can stick all your content loading code into one place, seperate from the actual class. I personally like it better this way.
In my next part I’ll show how we can reduce the amount of lines of code we need to write and read when we inherit from our Player class.
Leave a Reply