A short while ago MonoGame released a Pipeline tool to replace the XNA content pipeline process.

MonoGame Pipeline

The Pipeline tool is reasonably straightforward to use for standard content (textures, sounds, etc) once you get used to it. At some point though, you may want to write your own custom importers, processors, readers and writers for your own or 3rd party content.

In this tutorial we'll create a simple content importer, processor, reader and writer for Bitmap Fonts created with the BMFont tool and use it with MonoGame's Pipeline tool.

Overview

The tutorial is broken into into the following steps. If you're already familiar with any of these steps feel free to skip ahead.

  1. Create a new project to hold your content importer, processor and writer
  2. Create the content importer
  3. Create the content processor
  4. Create the content type writer
  5. Create the content type reader
  6. Reference the DLL from the Pipeline tool
  7. Read the content into your game

Part 1: Creating a new project for the importer, processor and writer

I recommend keeping your content importer, processor and writer in a project separate from your game. This way you can build an external DLL and reference it from the pipeline tool and separate the dependencies from your game. Reading content will often require 3rd party libraries that your game doesn't need. Once the content is processed the game can often read them independently of the 3rd party libraries.

First, add a standard Class Library project to your solution.

Add class library

Next, reference the MonoGame.Framework.Content.Pipeline.Portable package using NuGet.

Reference NuGet Package

Note: at the time of writing, the NuGet package is in pre-release only package. Make sure you choose Include Prerelease at the top of the Manage NuGet Packages window.

Let's write some code.

Part 2: Create the content importer

The first class you'll need is a content importer. The purpose of the importer is to read the data in it's raw format, usually as it was saved or exported from another tool. In this example, we're reading an XML file exported from the BMFont tool.

For more information about the details of creating content importers, see the MSDN documentation.

[ContentImporter(".fnt", DefaultProcessor = "BitmapFontProcessor", 
    DisplayName = "BMFont Importer - MonoGame.Extended")]
public class BitmapFontImporter : ContentImporter<FontFile>
{
    public override FontFile Import(string filename, ContentImporterContext context)
    {
        context.Logger.LogMessage("Importing XML file: {0}", filename);

        using (var streamReader = new StreamReader(filename))
        {
            var deserializer = new XmlSerializer(typeof(FontFile));
            return (FontFile)deserializer.Deserialize(streamReader);
        }
    }
}

You'll notice a few things about the above code. First, it's generically typed on the FontFile class. I haven't provided that class here, but you can find the original code on pastebin or you can also find my modified copy in MonoGame.Extended.

Second, the class is decorated with the ContentImporter attribute. This is how you specify the type of files the importer can handle, which content processor it uses (in the next part) and the display name as it will appear in MonoGame's Pipeline tool.

The rest is just standard XML deserialization.

Part 3: Create the content processor

The content processor is used to get the data ready for your game. What you do here is really up to you, but typically the goal is to make it more efficient to load into your game. You can strip the data of any bits you don't need and pre-calculate anything you don't need to do at runtime.

[ContentProcessor(DisplayName = "BMFont Processor - MonoGame.Extended")]
public class BitmapFontProcessor : ContentProcessor<FontFile, FileFileData>
{
    public override FileFileData Process(FontFile input, ContentProcessorContext context)
    {
        try
        {
            context.Logger.LogMessage("Processing BMFont");
            var json = JsonConvert.SerializeObject(input);
            var output = new FileFileData(json);

            foreach (var fontPage in input.Pages)
            {
                var assetName = Path.GetFileNameWithoutExtension(fontPage.File);
                output.TextureAssets.Add(assetName);
            }

            return output;

        }
        catch (Exception ex)
        {
            context.Logger.LogMessage("Error {0}", ex);
            throw;
        }
    }
}

In my case I decided to extract the names of the textures used for the bitmap font, ready for loading by the ContentManager. Then I just serialize the data as a single string of JSON. Sure, it's not as efficent as it could be but it's still more efficient than a big wad of XML and it's only a few lines of code.

A nice property of content processors is that they can be improved incrementally. I might come back later and make it more efficent. For the purpose of this tutorial though, I wanted to keep it small.

Part 4: Create the content type writer

The content type writer's job is to finally output the data as a binary file. This is ultimately what ends up in those XNB files shipped with your game. The content type writer is paired closely with the content type reader, whatever you write out will be read back by the reader.

[ContentTypeWriter]
public class BitmapFontWriter : ContentTypeWriter<FileFileData>
{
    protected override void Write(ContentWriter output, FileFileData value)
    {
        output.Write(value.TextureAssets.Count);

        foreach (var textureAsset in value.TextureAssets)
            output.Write(textureAsset);
        
        output.Write(value.Json);
    }

    public override string GetRuntimeType(TargetPlatform targetPlatform)
    {
        return typeof(BitmapFont).AssemblyQualifiedName;
    }

    public override string GetRuntimeReader(TargetPlatform targetPlatform)
    {
        return "MonoGame.Extended.BitmapFonts.BitmapFontReader, MonoGame.Extended";
    }
}

The GetRuntimeType and GetRuntimeReader are not very well documented but I believe they are used to describe the data type expected to come out of this data and which reader to use when loading it.

We have to use the fully qualified names of the classes here and the GetRuntimeReader needs to use a string otherwise we'd end up with a circular reference.

At this point we are done with the content pipeline project. We could reference it from the Pipeline tool and start processing our .fnt files. But before we move onto that, let's write the content type reader to go with the writer.

Part 5: Create the content type reader

The content type reader is the exact opposite of the content type writer. It is responsible for reading the data into your game when you request it using the Content.Load<T> method.

Note: this class doesn't live in the same DLL as the content type writer. It can either live in a separate DLL referenced by your game (as is the case with MonoGame.Extended) or it could live directly in the game code itself.

public class BitmapFontReader : ContentTypeReader<BitmapFont>
{
    protected override BitmapFont Read(ContentReader input, BitmapFont existingInstance)
    {
        var textureAssetCount = input.ReadInt32();
        var assets = new List<string>();

        for (var i = 0; i < textureAssetCount; i++)
        {
            var assetName = input.ReadString();
            assets.Add(assetName);
        }

        var json = input.ReadString();
        var fontFile = JsonConvert.DeserializeObject<FontFile>(json);
        var texture = input.ContentManager.Load<Texture2D>(assets.First());
        return new BitmapFont(texture, fontFile);
    }
}

In this implementation I'm only supporting 1 texture per bitmap font file, although technically BMFont's can have many pages of textures. That's an improvement for the next iteration.

Part 6: Reference the DLL from the Pipeline tool

Finally, we are ready to reference our new DLL from the Pipeline tool. Currently (MonoGame v3.4) it's not as intuitive as I would have liked.

Pipeline References

  1. Click on Content at the root of the tree.
  2. Click on References in the properties pane.
  3. Click on the the ellipses [...] button.
  4. Type in the path to your DLL, relative to the Content.mgcb file.

After you've added a reference to your DLL you can open the Content.mgcb file in a text editor and see the references like this:

#-------------------------------- References --------------------------------#

/reference:..\..\MonoGame.Extended.Content.Pipeline\bin\Debug\MonoGame.Extended.Content.Pipeline.dll

Of course, if you prefer not to use the GUI tool you can edit this file manually.

While we are in the Pipeline tool, let's build our content.

MonoGame Pipeline

If you've added your content files to the tree you should be able to select your new importer and processor from the properties pane. Rebuild your content and check for any errors.

Part 7: Load the content into your game

If everything has went well, there's only one thing left to do.

_bitmapFont = Content.Load<BitmapFont>("courier-new-32");

Happy coding!