read

I thought it might be useful to go over how I wrote the custom level processor, because after following the example in the documentation I still had some questions.

The Level Classes

I already had the classes that make up my level, because I was using them in the level editor. This is the class diagram of the level objects:

Level Class Diagram

As you can see, it’s pretty straightforward.
The classes on the left hold the Farseer collision entity data. I need to update these to add extra information, such as making the collision entities static or dynamic.
The TextureInstance class represents a single texture drawn on the screen. Note that it doesn’t hold the actual texture, just the name of the texture. The textures themselves are stored in the Level class. This is so that I’m not duplicating textures in memory if I need to draw the same image in multiple places.
The Level class just holds collections of the other classes, the Texture2D images, and the height and width of the level.

The Level XML File

Next, I designed an XML file to represent the level structure:

<?xml version="1.0" encoding="utf-8" ?>
<Level height="768" width="1024">
    <CollisionEntities>
        <PolygonCollisionEntity mass="0.5f">
            <Vertex x="100" y="100"/>
            <Vertex x="150" y="100"/>
            <Vertex x="150" y="200"/>
            <Vertex x="100" y="200"/>
        </PolygonCollisionEntity>
        <RectangleCollisionEntity x="100.0f" y="100.0f" height="100.0f" width="100.0f" mass="0.5f"/>
    </CollisionEntities>
    <TextureInstances>
        <TextureInstance x="100.0f" y="100.0f" height="100.0f" width="100.0f" name="texture"/>
    </TextureInstances>
    <Textures>
        <TextureResource name="texture" filename="C:\texture.jpg"/>
    </Textures>
</Level>

The Input Data

Following the “How To: Write a Custom Importer and Processor” article in the XNA Documentation, the first step is to write a class to hold the data that we are importing, which in the case of the level is the XML file.

using System;
using System.Collections.Generic;
using System.Text;

namespace LevelProcessor
{
    public class LevelSourceCode
    {
        public LevelSourceCode(string sourceCode)
        {
            this._sourceCode = sourceCode;
        }

        private string _sourceCode;

        public string SourceCode { get { return _sourceCode; } }

    }
}

This is basically the same as the example in the documentation, as we also only need to store the file text.

The Content Importer

The content importer is also virtually identical to the documentation, as it just needs to read the text from the XML file and store it in the LevelSourceCode object.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework.Content.Pipeline;

namespace LevelProcessor
{
    [ContentImporter(".lvl", DefaultProcessor = "XmlLevelProcessor", DisplayName = "XML Level Importer")]
    public class XmlLevelImporter : ContentImporter<LevelSourceCode>
    {
        public override LevelSourceCode Import(string filename, ContentImporterContext context)
        {
            string sourceCode = System.IO.File.ReadAllText(filename);
            return new LevelSourceCode(sourceCode);
        }

    }
}

The Content Processor

Here is where we start to differ from the documentation. What we need to do is parse the XML that we read in the previous step, and store it in an instance of the Level object.

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.IO;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework;
using PhysicsGameEngine;

namespace LevelProcessor
{
    [ContentProcessor(DisplayName = "XML Level Processor")]
    public class XmlLevelProcessor : ContentProcessor<LevelSourceCode, Level>
    {
        public override Level Process(LevelSourceCode input, ContentProcessorContext context)
        {
            Level level = new Level();

            try
            {
                // load the source xml into a memory stream
                byte[] byteArray = new byte[input.SourceCode.Length];
                System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
                byteArray = encoding.GetBytes(input.SourceCode);

                MemoryStream memoryStream = new MemoryStream(byteArray);
                memoryStream.Seek(0, SeekOrigin.Begin);

                // create an xml reader to parse the xml
                XmlReader reader = XmlReader.Create(memoryStream);

                // loop through the xml and create elements for our level
                while (reader.Read())
                {
                    switch (reader.NodeType)
                    {
                        case XmlNodeType.Element:
                            switch (reader.Name.ToUpper())
                            {
                                case "LEVEL":
                                    int levelHeight = int.Parse(reader.GetAttribute("height"));
                                    int levelWidth = int.Parse(reader.GetAttribute("width"));
                                    level.Height = levelHeight;
                                    level.Width = levelWidth;
                                    break;

                                case "POLYGONCOLLISIONENTITY":
                                    float mass = float.Parse(reader.GetAttribute("mass"));
                                    PolygonCollisionEntity poly = new PolygonCollisionEntity();
                                    poly.Mass = mass;

                                    // loop through the vertex list and get the vertices that make up the polygon
                                    List<Vector2> vertices = new List<Vector2>();

                                    while (reader.Read())
                                    {
                                        if (reader.NodeType == XmlNodeType.EndElement && reader.Name.ToUpper() == "POLYGONCOLLISIONENTITY")
                                        {
			                    break;
                                        }

                                        float xPos = float.Parse(reader.GetAttribute("x"));
                                        float yPos = float.Parse(reader.GetAttribute("y"));

                                        vertices.Add(new Vector2(xPos, yPos));
                                    }

                                    // add the vertices to the poly collision
                                    poly.Vertices = vertices.ToArray();

                                    level.PolygonCollisionEntities.Add(poly);
                                    break;

                                case "RECTANGLECOLLISIONENTITY":
                                    float rectXPos = float.Parse(reader.GetAttribute("x"));
                                    float rectYPos = float.Parse(reader.GetAttribute("y"));
                                    float height = float.Parse(reader.GetAttribute("height"));
                                    float width = float.Parse(reader.GetAttribute("width"));
                                    float rectMass = float.Parse(reader.GetAttribute("mass"));

                                    RectangleCollisionEntity rect = new RectangleCollisionEntity();
                                    rect.Height = height;
                                    rect.Mass = rectMass;
                                    rect.Position = new Vector2(rectXPos, rectYPos);
                                    rect.Width = width;

                                    level.RectangleCollisionEntities.Add(rect);
                                    break;

                                case "TEXTUREINSTANCE":
                                    float textXPos = float.Parse(reader.GetAttribute("x"));
                                    float textYPos = float.Parse(reader.GetAttribute("y"));
                                    int textureHeight = int.Parse(reader.GetAttribute("height"));
                                    int textureWidth = int.Parse(reader.GetAttribute("width"));
                                    string name = reader.GetAttribute("name");

                                    TextureInstance instance = new TextureInstance();
                                    instance.Height = textureHeight;
                                    instance.Name = name;
                                    instance.Position = new Vector2(textXPos, textYPos);
                                    instance.Width = textureWidth;

                                    level.TextureInstances.Add(instance);
                                    break;

                                case "TEXTURERESOURCE":
                                    level.TextureResources.Add(reader.GetAttribute("name"));
                                    break;
                            }

                            break;
                        case XmlNodeType.EndElement:
                            break;
                    }
                }
            }
            catch (Exception ex)
            {
                throw new InvalidContentException(ex.Message);
            }

            return level;
        }

    }
}

The Content Type Writer

In this step, we are taking the Level object that we read in the previous step, and writing it out in a binary XNB file. The binary file is much smaller than the XML file, as it doesn’t have the overhead of storing all the XML tag names, so it’s much quicker for XNA to load when you run your level.

At this stage you need to think about how to represent your level in the binary format so that you can read it out again in the next step. For example, because we can’t just iterate over the collections (because they’re not stored as collections), you need to make sure you include the number of objects to read.

You can see the structure I came up with in comments towards the top of the file:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
using PhysicsGameEngine;

namespace LevelProcessor
{
    [ContentTypeWriter]
    public class XmlLevelWriter : ContentTypeWriter
    {
        protected override void Write(ContentWriter output, Level value)
        {
            //LEVEL HEIGHT (INT)
            //LEVEL WIDTH (INT)
            //NUMBER OF POLY COLLISIONS (INT)
            //  POLY MASS (FLOAT)
            //  NUMBER OF POLY VERTICES (INT)
            //      X (FLOAT)
            //      Y (FLOAT)
            //NUMBER OF RECTANGLE COLLISIONS (INT)
            //  RECT MASS (FLOAT)
            //  X (FLOAT)
            //  Y (FLOAT)
            //  HEIGHT (FLOAT)
            //  WIDTH (FLOAT)
            //NUMBER OF TEXTURE INSTANCES (INT)
            //  NAME (STRING)
            //  X (FLOAT)
            //  Y (FLOAT)
            //  HEIGHT (INT)
            //  WIDTH (INT)

            output.Write(value.Height);
            output.Write(value.Width);

            output.Write(value.PolygonCollisionEntities.Count);
            foreach(PolygonCollisionEntity poly in value.PolygonCollisionEntities)
            {
                output.Write(poly.Mass);
                output.Write(poly.Vertices.Length);
                foreach (Vector2 vert in poly.Vertices)
                {
                    output.Write(vert.X);
                    output.Write(vert.Y);
                }
            }

            output.Write(value.RectangleCollisionEntities.Count);
            foreach (RectangleCollisionEntity rect in value.RectangleCollisionEntities)
            {
                output.Write(rect.Mass);
                output.Write(rect.Position.X);
                output.Write(rect.Position.Y);
                output.Write(rect.Height);
                output.Write(rect.Width);
            }

            output.Write(value.TextureInstances.Count);
            foreach (TextureInstance instance in value.TextureInstances)
            {
                output.Write(instance.Name);
                output.Write(instance.Position.X);
                output.Write(instance.Position.Y);
                output.Write(instance.Height);
                output.Write(instance.Width);
            }

            output.Write(value.TextureResources.Count);
            foreach (string textureName in value.TextureResources)
            {
                output.Write(textureName);
            }
        }
        public override string GetRuntimeType(TargetPlatform targetPlatform)
        {
            return typeof(Level).AssemblyQualifiedName;
        }
        public override string GetRuntimeReader(TargetPlatform targetPlatform)
        {
            return "PhysicsGameEngine.XmlLevelReader, PhysicsGameEngine, Version=1.0.0.0, Culture=neutral";
        }

    }
}

The Content Type Reader

Now we need to write the reader, which simply reads the file that we created in the previous step.

As you can see, it’s almost identical to the writer, only we are reading, and we return the instantiated level at the end:

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

namespace PhysicsGameEngine
{
    public class XmlLevelReader : ContentTypeReader
    {
        protected override Level Read(ContentReader input, Level existingInstance)
        {
            Level level = new Level();

            //LEVEL HEIGHT (INT)
            //LEVEL WIDTH (INT)
            //NUMBER OF POLY COLLISIONS (INT)
            //  POLY MASS (FLOAT)
            //  NUMBER OF POLY VERTICES (INT)
            //      X (FLOAT)
            //      Y (FLOAT)
            //NUMBER OF RECTANGLE COLLISIONS (INT)
            //  RECT MASS (FLOAT)
            //  X (FLOAT)
            //  Y (FLOAT)
            //  HEIGHT (FLOAT)
            //  WIDTH (FLOAT)
            //NUMBER OF TEXTURE INSTANCES (INT)
            //  NAME (STRING)
            //  X (FLOAT)
            //  Y (FLOAT)
            //  HEIGHT (INT)
            //  WIDTH (INT)
            //NUMBER OF TEXTURES (INT)
            //  NAME (STRING)

            level.Height = input.ReadInt32();
            level.Width = input.ReadInt32();            

            int numPolys = input.ReadInt32();
            for (int i = 0; i < numPolys; i++)
            {
                PolygonCollisionEntity poly = new PolygonCollisionEntity();
                poly.Mass = input.ReadSingle();
                int numVertices = input.ReadInt32();
                List<Vector2> vertices = new List<Vector2>();
                for (int j = 0; j < numVertices; j++)
                {
                    float xPos = input.ReadSingle();
                    float yPos = input.ReadSingle();
                    vertices.Add(new Vector2(xPos, yPos));

                }
                poly.Vertices = vertices.ToArray();
                level.PolygonCollisionEntities.Add(poly);
            }

            int numRects = input.ReadInt32();
            for (int i = 0; i < numRects; i++)
            {
                RectangleCollisionEntity rect = new RectangleCollisionEntity();
                rect.Mass = input.ReadSingle();
                float xPos = input.ReadSingle();
                float yPos = input.ReadSingle();
                rect.Position = new Vector2(xPos, yPos);
                rect.Height = input.ReadSingle();
                rect.Width = input.ReadSingle();

                level.RectangleCollisionEntities.Add(rect);
            }

            int numTextureInstances = input.ReadInt32();
            for (int i = 0; i < numTextureInstances; i++)
            {
                TextureInstance instance = new TextureInstance();
                instance.Name = input.ReadString();
                float xPos = input.ReadSingle();
                float yPos = input.ReadSingle();
                instance.Position = new Vector2(xPos, yPos);
                instance.Height = input.ReadInt32();
                instance.Width = input.ReadInt32();

                level.TextureInstances.Add(instance);
            }

            int numTextures = input.ReadInt32();
            for (int i = 0; i < numTextures; i++)
            {
                level.TextureResources.Add(input.ReadString());
            }

            return level;
        }

    }
}

And we’re done!

Now, after adding the content processor reference to the game, we can add level files to the Content project, and it should automatically identify our new level processor:

Level Content Processor

dylan


Published on

Dylan Black

Back to Home