Showing posts with label Content Pipeline. Show all posts
Showing posts with label Content Pipeline. Show all posts

Sunday, November 27, 2011

The XML Content Importer

A while ago, I wrote a quick rundown of XNA's Content Pipeline, the classes needed to extend it and how they work together. However for the most part, one wouldn't need to do all that since the built in XML Importer is so powerful. One can load almost any class through the content pipeline using only XML.
We'll go few some examples to illustrate this.
If you want to follow along with the code, go ahead and make a new XNA Game Project. I named mine XMLPipelineTutorial.
Right click on the Content Project, select Add New Item... and select XML file. I named mine SimpleString.xml because that's what it will load. The following XML should appear:

<?xml version="1.0" encoding="utf-8" ?>
<XnaContent>
  <!-- TODO: replace this Asset with your own XML asset data. -->
  <Asset Type="System.String"></Asset>
</XnaContent>
The Node named Asset has an attribute called Type which is set to "System.String". This is what will be returned when we call the Content Manager's Load method.
What we put inside the Asset tags is the actual content
I've changed the XML to look like this:

<?xml version="1.0" encoding="utf-8" ?>
<XnaContent>
  <Asset Type="System.String">
    Hello World!
  </Asset>
</XnaContent>
Now in Game1.cs I changed the LoadContent() method so it looks like this:
protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);

    string xmlContent = Content.Load<string>("SimpleString");
    Console.WriteLine(xmlContent);
}
The line Console.WriteLine(xmlContent) is a little trick I use for debugging games when I don't want to bother loading a font and drawing it on the screen. The output appears on the 'Output' window which you can find in Visual Studio via Debug -> Windows -> Output.
Easy enough. We can load arrays of things:

<?xml version="1.0" encoding="utf-8" ?>
<XnaContent>
  <!-- TODO: replace this Asset with your own XML asset data. -->
  <Asset Type="System.Int32[]">
    4 5 1 6 9 4 10 100032
  </Asset>
</XnaContent>
I named this one IntegerArray.xml.
We can load this just as easily.

protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);

    string xmlContent = Content.Load<string>("SimpleString");
    Console.WriteLine(xmlContent);

    int[] xmlContent2 = Content.Load<int[]>("IntegerArray");
    for (int i = 0; i < xmlContent2.Length; i++)
    {
        Console.WriteLine(xmlContent2[i].ToString());
    }
}
This is all very well and good but doesn't really show off the flexibility. Let's import a Dictionary of Vectors keyed by Rectangles just for kicks.

<?xml version="1.0" encoding="utf-8" ?>
<XnaContent>
  <!-- TODO: replace this Asset with your own XML asset data. -->
  <Asset Type="System.Collections.Generic.Dictionary[Microsoft.Xna.Framework.Rectangle, 
         Microsoft.Xna.Framework.Vector2]">
    <Item>
      <Key>0 12 24 5</Key>
      <Value>5 5.4</Value>
    </Item>
    <Item>
      <Key>12 3 6 2</Key>
      <Value>0 0</Value>
    </Item>
  </Asset>
</XnaContent>
Each KeyValuePair in the Dictionary is put in between Item nodes. We could had done this for the simple array but we don't have to. Each Key and Value is inside a node of the same name. The XML importer uses reflection to figure out what's what (which is why it's so flexible and powerful).
The Keys, being rectangles are just a list of int's representing X, Y, Width and Height respectively and the Values being Vectors are written as two float's X and Y, respectively. For some reason the pipeline expects these to be in this format and won't let me put values between Width/Height tags or X/Y tags. I suppose for convenience.
Now to load and test it out.
protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);

    Dictionary<Rectangle, Vector2> xmlContent = 
        Content.Load<Dictionary<Rectangle, Vector2>>("VectorRectangleDictionary");

    foreach (Rectangle rectangle in xmlContent.Keys)
    {
        Console.WriteLine(rectangle.ToString());
    }

    foreach (Vector2 vector in xmlContent.Values)
    {
        Console.WriteLine(vector.ToString());
    }
}
And all is as it should be.
But these are all classes from preexisting libraries, one might point out, what if I want to load my own class?
Not a problem! Since the XML importers uses reflection, we can load any class we want through it.
To do this though we must add a Game Library Project to keep any classes we want to load. Then add a reference to that project in the content project.
We need the Library Project because the content project doesn't know anything about the XMLPipelineTutorial namespace so it won't be able to find any types you try to give it and trying to add a reference to it would cause a circular dependency.
Remember to add a reference to the Game Library not only in the content project but also the main game project.
Here's a simple class:
public class GameItem
{
    public string Name = null;
    public double Value;
}
And here's an XML file for loading it:
<?xml version="1.0" encoding="utf-8" ?>
<XnaContent>
  <Asset Type="XMLTutorialData.GameItem">
    <Name>Sword</Name>
    <Value>350</Value>
  </Asset>
</XnaContent>
Make sure the namespace in the Type attribute match yours.
Now we can load it just like any other asset
protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);

    GameItem item = Content.Load<GameItem>("Item");

    Console.WriteLine(item.Name + " " + item.Value.ToString());
}
And you should see, "Sword 350" in the output window.
There is a subtlety here that's worth pointing out. To illustrate I've defined another GameItem object and loaded it with the same file. Here's the new LoadContent method. See if you can predict the output before running.
protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);

    GameItem itemOne = Content.Load<GameItem>("Item");
    GameItem itemTwo = Content.Load<GameItem>("Item");

    Console.WriteLine(itemOne.Name + " " + itemOne.Value.ToString());
    Console.WriteLine(itemOne.Name + " " + itemOne.Value.ToString());

    //Changed the name of itemOne...
    itemOne.Name = "Boot";

    //Print out the name of itemTwo.
    Console.WriteLine(itemTwo.Name);
}
What the?! Changing itemOne's name changed itemTwo's name as well! This is because GameItem is a reference object. What we have are two references to the same object. Go ahead and try this with a value type like a single integer or a Vector2 object and you won't get the same behavior.

The importer will by default only deserialize public fields and properties. If you have private fields you'd like to add you must mark them with a [ContentSerializer] attribute and if you have public fields or properties you'd like ignored by the serializer, you must use the [ContentSerializerIgnor] attribute (excluding readonly fields and properties).
So let's change the GameItem class so not everything is public.
public class GameItem
{
    [ContentSerializer]
    string _name = null;
    [ContentSerializer]
    double _value;

    public string Name
    {
        get { return _name; }
    }

    public double Value
    {
        get { return _value; }
    }
}
The Name and Value properties are readonly so they don't require an attribute to ignore them.
Here's the new Item.xml where the node names match the new field names
<?xml version="1.0" encoding="utf-8" ?>
<XnaContent>
  <Asset Type="XMLTutorialData.GameItem">
    <_name>Sword</_name>
    <_value>350</_value>
  </Asset>
</XnaContent>
And this works exactly the same as before except the GameItem class is better encapsulated.
That's all for now. The built in XML importer has a lot of features and is quite flexible. There's a lot more to explore and perhaps I'll go into more in future articles.

Update: I cam across this question in StackOverflow where someone's demonstrated a nice way to print out how exactly the intermediate serializer expects to see your .xml formatted for some specific object. Definitely worth checking out.

Sunday, June 26, 2011

Content Pipeline Tutorial

In this article we'll be building a content pipeline extension.
The content pipeline is a great system for importing and loading outside art assets into your game. The files are converted into a binary (.xnb) file format at compile time that the XNA framework can then deserialize at run time.
The basic process as illustrated in the diagram goes something like this: the file we wish to load is imported into the content pipeline and the importer returns an some object we can work with like a string or an XmlDocument, then the imported type is passed off to the content processor which turns it into an object of the desired type. For example, a .png image is imported and then some doubtlessly complicated logic goes through the data within the file and translates it to a Texture2DContent object (which is turned into a Texture2D object at run time). The object is then passed off to the Content Type Writer which serializes it into the binary .xnb format. All of this happens during compile-time. Then finally at run-time the Content Reader deserializes the .xnb file to our loaded type within the Load method call.

First thing's first. Let's create and set up the projects we'll need. We'll need a total of three projects: An XNA Game project, a Game Library project to hold the types we'll be loading in as well as our content readers, and a Content Pipeline Extension library which will have the importer, processor and writer. Go ahead and create a new solution and then add the other projects.
That finished we now need to add the required references. In the Game project add a reference to the Game Library project. The Content Pipeline Extension project also needs a reference to the Game Library project so it can hold references to the types we'll be loading. Lastly the Content project where you'll have your asset files needs a reference to the Content Pipeline Extension library.
Now let's make our type to be loaded in. We'll make a simple tile map class that will display a grid of textures.
We'll be using the .xml format to import and process our assets. This is mostly to make everything nice and easy since xml is so nice to work with but we could import the data in any form we like.
XNA does already have an importer for the xml format which is very nice and flexible but one can have the control over how data is processed.

The following is the code for our TileMap class which is to go into our Game Library project (I named mine ContentPipelineGameLibrary). It's a very simple class. It uses a single sprite sheet for the tile textures. This is for ease in the serialization and deserialization. I'm using this one with two textures: grass and dirt.
Go ahead and add this code,

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;

namespace ContentPipelineTutorialGameLibrary
{
    public class TileMap
    {
        string _assetName = "";
        Texture2D _spriteSheet;
        int[,] _textureGrid;
        int _tileWidth = 32;
        int _tileHeight = 32;
        int _tilesPerRow =2;

        Rectangle _drawRectangle = new Rectangle();
        Rectangle _sourceRectangle = new Rectangle();

        public int TilesPerRow
        {
            get { return _tilesPerRow; }
            set { _tilesPerRow = value; }
        }
        public int MapWidth
        {
            get { return _textureGrid.GetLength(1); }
        }
        public int MapHeight
        {
            get { return _textureGrid.GetLength(0); }
        }
        public string AssetName
        {
            get { return _assetName; }
            set { _assetName = value; }
        }
    
        public TileMap(int width, int height)
        {
            _textureGrid = new int[height, width];
            _drawRectangle.Width = _sourceRectangle.Width = _tileWidth;
            _drawRectangle.Height = _sourceRectangle.Height = _tileHeight;
        }

        public void Draw(SpriteBatch spriteBatch)
        {
            for (int x = 0; x < _textureGrid.GetLength(1); x++)
            {
                for (int y = 0; y < _textureGrid.GetLength(0); y++)
                {
                    _drawRectangle.X = x * _tileWidth;
                    _drawRectangle.Y = y * _tileHeight;
                    _sourceRectangle.X =
                        (_textureGrid[y, x] % _tilesPerRow) * _tileWidth;
                    _sourceRectangle.Y =
                        (_textureGrid[y, x] / _tilesPerRow) * _tileHeight;
                    spriteBatch.Draw(
                        _spriteSheet,
                        _drawRectangle,
                        _sourceRectangle,
                        Color.White);
                }
            }
        }
        public void SetTextures(Texture2D spriteSheet, string assetName)
        {
            _spriteSheet = spriteSheet;
        }
        public int GetIndex(int x, int y)
        {
            return _textureGrid[y, x];
        }
        public void SetIndex(int x, int y, int value)
        {
            _textureGrid[y, x] = value;
        }
    }
}

Pretty straight forward stuff. The _tilePerRow field is so we don't have to limit ourselves too much on how we format our sprite sheets. The AssetName setter could prove problematic later on if we want to switch out sprite sheets during run-time but we can tweak things later.

Before we get to any content extension classes, let's set up how we're going to format the data in our file. Like I mentioned before I went with xml since it's so easy to work with but you can just as easily use almost anything (using classes that already have an importer/processor is tricky and I don't recommend it).
Right click on your content project. Select Add -> New Item... and select XML Document in the dialogue. Name it whatever you want, I named mine "TileMap01". The file is automatically given a .xml extension so right click on it and select Rename then change the extension to whatever (I chose ".tilemap"). You will get a warning but pay no heed; we got this.
Now double click the newly created file, delete whatever's inside and add this:

<TileMap Width="4" Height="4">
  <Texture AssetName="TileSheet"/>
  <Layout>
    0 0 0 1
    0 1 0 1
    0 0 0 0
    1 0 1 1
  </Layout>
</TileMap>

Now on to the importer. Right click on your Content Extension Library and select Add -> New Item..., then select Content Importer.
Now replace the automatically generated code with 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 System.Xml;

namespace ContentPipelineTutorialExtensionLibrary
{   
    [ContentImporter(".tilemap", DisplayName = "TileMap Importer", 
        DefaultProcessor = "TileMapProcessor")]
    public class TileMapImporter : ContentImporter<XmlDocument>
    {
        public override XmlDocument Import(
            string filename, ContentImporterContext context)
        {
            XmlDocument document = new XmlDocument();
            document.Load(filename);
            return document;
        }
    }
}

Done. All xml Importers will start this way. Make sure the attribute that declares the extension matches yours.
Now add another new item to the Content Extension Library project, select Content Processor and replace the code with 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 System.Xml;
using ContentPipelineTutorialGameLibrary;
using System.IO;

namespace ContentPipelineTutorialExtensionLibrary
{
    [ContentProcessor(DisplayName = "TileMap Processor")]
    public class TileMapProcessor : ContentProcessor<XmlDocument, TileMap>
    {
        public override TileMap Process(
            XmlDocument input, ContentProcessorContext context)
        {
            
            XmlNodeList inputNodeList = input.GetElementsByTagName("TileMap");
            TileMap output = new TileMap(
                int.Parse(inputNodeList[0].Attributes["Width"].Value),
                int.Parse(inputNodeList[0].Attributes["Height"].Value));
            XmlNodeList nodeList = inputNodeList[0].ChildNodes;

            string[] layout;
            string[] rowEntries;

            foreach (XmlNode node in nodeList)
            {
                if (node.Name == "Layout")
                {
                    layout = node.InnerText.Trim().Split('\n');
                    for (int row = 0; row < layout.Length; row++)
                    {
                        rowEntries = layout[row].Trim().Split(' ');
                        for (int rowEntry = 0; rowEntry < rowEntries.Length; rowEntry++)
                        {
                            output.SetIndex(
                                rowEntry, row, int.Parse(rowEntries[rowEntry]));
                        }
                    }
                }
                else if (node.Name == "Texture")
                {
                    output.AssetName = node.Attributes["AssetName"].Value;
                }
            }
            
            return output;
        }
    }
}

So what we're doing here is reading through our xml, setting the texture asset name and setting the map's grid index. Notice we're NOT setting the texture yet. This is because the Texture2D class is not built during compile-time, it's a run-time object so we'll be setting the actual texture in the Content Reader which is called at run-time (when content.Load is called).
That's it for processing our data. Now let's make a Content Writer for the binary serialization.
Like before, add a new item and select Content Writer. The code:


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;
using ContentPipelineTutorialGameLibrary;

namespace ContentPipelineTutorialExtensionLibrary
{
    [ContentTypeWriter]
    public class TileMapWriter : ContentTypeWriter<TileMap>
    {
        protected override void Write(ContentWriter output, TileMap value)
        {
            output.Write(value.MapWidth);
            output.Write(value.MapHeight);
            output.Write(value.AssetName);
            for (int x = 0; x < value.MapWidth; x++)
            {
                for (int y = 0; y < value.MapHeight; y++)
                {
                    output.Write(value.GetIndex(x, y));
                }
            }
        }

        public override string GetRuntimeReader(TargetPlatform targetPlatform)
        {
            return 
                "ContentPipelineTutorialGameLibrary.TileMapReader, ContentPipelineTutorialGameLibrary";
        }
    }
}

In the GetRuntimeReader method, we have the name of our Content Type Reader which we haven't created yet but will be located in the Game Library as opposed to the Content Pipeline Extension library.
And like before, make sure the names match yours (or, rather, what you will name your Content Reader).


Now we need to actually make the reader. Add a new item to the Game Library and select Content Type Reader.
The code:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

namespace ContentPipelineTutorialGameLibrary
{
    public class TileMapReader : ContentTypeReader<TileMap>
    {
        protected override TileMap Read(ContentReader input, TileMap existingInstance)
        {
            TileMap tileMap = new TileMap(input.ReadInt32(), input.ReadInt32());
            string assetName=input.ReadString();
            tileMap.SetTextures(
                input.ContentManager.Load<Texture2D>(assetName), assetName);
            for (int x = 0; x < tileMap.MapWidth; x++)
            {
                for (int y = 0; y < tileMap.MapHeight; y++)
                {
                    tileMap.SetIndex(x, y, input.ReadInt32());
                }
            }
            return tileMap;
        }
    }
}

This is where we set the texture by from the deserialized asset name.
This also might be a good time to explore the functions available in both the reader and writer. There's pretty much a method for serializing anything you want.
We're not quite done yet. In your content project, left click the tile map file and in the Properties window under Build Action select Compile. Then, under Content Importer and Content Processor select the TileMap Importer and TileMap Processor, respectively.
If they don't appear in the drop-down, try compiling the solution and check again.

Let's load the map and draw it!
Change your Game1.cs class to look like this:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using ContentPipelineTutorialGameLibrary;

namespace ContentPipelineTutorial
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        TileMap tileMap;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        protected override void Initialize()
        {        
            base.Initialize();
        }   
        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);

            tileMap = Content.Load<TileMap>("TileMap01");
        }      
        protected override void UnloadContent()
        {
      
        }   
        protected override void Update(GameTime gameTime)
        {          
            base.Update(gameTime);
        }
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            spriteBatch.Begin();
            tileMap.Draw(spriteBatch);
            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}

Now run the solution and you should see something like this:


Okay, so that's pretty small so feel free to change things around a bit :)
That's it for this tutorial. I am happy to answer any questions regarding this tutorial as best I can.

source code