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:
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:

Thanks! Very clean code, way cleaner than mine btw
This is very nice a great example to point me in the right direction for my 3d map level processor thanks. Also if you don’t mind me asking what program/s did you use to create that class digram?
Thanks, I’m glad it was helpful.
I used Visual Studio to create the class diagram.
You can create a class diagram by going to the Project menu, selecting Add New Item, and selecting Class Diagram from the list.
Then just drag the classes that you want from the Solution Explorer window onto the design surface, or create new classes directly in the diagram by dragging items from the Toolbox onto the design surface.
This looks very interesting would like to look at this more in depth. Is it possible to look at the solution and source for this project ?