For the longest time, the Content Pipeline was a magic transmogrification device to me. I would add content to a content project and it would get mystically turned into stuff I would load in my game with ContentManager. A few months ago I decided it was time to put an end to its magical aspects and learn how it worked and how I could put it to work. I thought it would be helpful to share what I learned so I created a sample. This sample has two different custom extensions. The first is a complete custom extension from importer to reader. The second is a processor that plugs in to an existing pipeline route.
It’s important to understand the path that content travels through. The Content Pipeline runs through each item of content and instantiates instances of the appropriate class and runs the appropriate methods for it. Here are the stages:
- Importer (Build-time)
- Processor (Build-time)
- Writer (Built-time)
- Reader (Run-time)
The Content Pipeline uses MSBuild (an XML-controlled build process that also builds your XNA game, other C# projects, and every other type of project you can “compile” in Visual Studio). MSBuild looks at the .contentproj file, examines the content items, reads in data about which importer and processor they should use and what parameter values should be set for the processor, and the proceeds to call the appropriate ContentImporter<T> class’s Import method. This reads the data in to the build-time model of the data type.
You will commonly find that Content Pipeline extensions (including all of the built-in ones that XNA provides) use a build-time model of the data which is a different class than the run-time model of the data. For instance, Texture2D is the run-time model of two dimensional texture data. Its build-time counterpart is Texture2DContent. Some data is represented with several build-time models. The Model class (for 3D models), is imported as NodeContent data (by the FBX and X importers) and then transformed by the content processor into ModelContent. NodeContent represents a raw version of the 3D content, with as little transformation as possible. The process of transforming data into something that can be used in-game should be left for the ContentProcessor<TInput,TOutput>. Sometimes this means that you import the data into one data structure (e.g. NodeContent) that fairly closely represents what the data coming in from the file looks like and then transform it into another data structure (e.g. ModelContent and the subclasses like MeshContent, MaterialContent, and all the MaterialContent-derived classes) in the processor(s).
Once the processing is done, the Content Pipeline sends the output of the ContentProcessor to its relevant ContentTypeWriter<T>. There can only ever be one ContentTypeWriter for a type. If you tried to implement a ContentTypeWriter for, e.g., Texture2DContent, you will get an InvalidOperationException since one already exists in the Content Pipeline assemblies. The ContentTypeWriter must do three things to be effective. It must override the GetRuntimeReader method, the GetRuntimeType method, and the Write method. GetRuntimeReader returns the assembly qualified name of the runtime reader and GetRuntimeType returns the assembly qualified name of the runtime type. You can read more about assembly qualified names here, but basically what you need is “MyNamespace.SomeClass, MyAssemblyName”. Commonly you would use a game library to hold the runtime reader and runtime type. By default, the assembly name and the namespace will be the same. So if you had a game library called MyLib and you had classes named GameDataReader and GameData in the root namespace of the library, then GetRuntimeReader would return “MyLib.GameDataReader, MyLib”, and GetRuntimeType would return “MyLib.GameData, MyLib”. ContentManager uses this information to invoke the specified reader when it comes across content of the specified type. The Write method uses a Content Pipeline-supplied ContentWriter (which derives from BinaryWriter, adding XNA-specific Write method overloads and other methods to it) to write out the XNB file. It’s up to you to write out everything you need to write in order to be able to read things in during the game. So for arrays, List<T>s, and other collection types, for instance, you want to write out their Length/Count property first so that you can read that in at runtime and know how many items of that type you must then read in. Anything you can think of a way to read in you can write out, so for the most part the only limitations are your own creativity. However you must make sure that your corresponding reader reads everything in exactly the same way that the writer writes things out otherwise you’ll get anything from data corruption/truncation to exceptions.
As mentioned above, the ContentTypeReader<T> is not a part of the Content Pipeline even though it is a mandatory part of a Content Pipeline extension that includes a custom ContentTypeWriter<T>. Instead it’s part of your game itself or, more commonly, part of a game library that you’ve created for that content type. Indeed, ContentTypeReader<T> is found in the Microsoft.Xna.Framework assembly rather than in one of the Content Pipeline’s assemblies since it must be capable of running on any supported XNA platform, not just on PC. There is no special attribute that needs to be applied to the ContentTypeReader<T>-derived class. ContentTypeWriter<T>’s GetRuntimeReader took care of specifying the class when the XNB file was built. Instead, ContentManager, when it receives a call to the Load<T> method requesting that it load data, will open the file, determine the appropriate reader(s) for the content, and proceed to instantiate instances of those readers and pass the opened file to its Read method(s), which then reads in the content to an instance of the class and returns that instance. That is the end of content’s journey through the Content Pipeline and the ContentManager into your game.
The sample has two Content Pipeline extensions, a game library, and a test game with some content processed by the extensions.
TextListContentPipelineExtension is a full extension, reading in a text file (see TextListContentImporter.cs) into a class (see TextListContent.cs) that contains a List<string>, performing some processing on the data that has been read in depending on the values of the processor parameters that have been added (see TextListContentProcessor.cs), writing it out (see TextListContentWriter.cs and TextListContent.cs), then reading it in during the game using the TextListLib game library project’s TextListReader class (see TextListReader.cs) to read it in to TextListLib’s TextList class (see TextList.cs).
TintTextureContentPipelineExtension is a partial extension. It extends TextureProcessor by overriding its Process method to add some additional manipulation options. Rather that try to recreate TextureContent’s Process method ourselves, we just override it and (when we’re done with our custom processing) turn the resulting data over to TextureProcessor’s Process method. Everything else (import, writing, and run-time reading) is done by the built-in classes and methods that XNA provides for processing texture data for use as Texture2D, etc. The code in this is fully-functional but fragile and easy to upset. While I ended up removing it to keep the project simple and focused, I did make use of Stephen Styrchak’s XNA Content Pipeline Debugging template (which I highly recommend as it allows you to do all sort of handy things like set breakpoints that will actually work in a content pipeline extension build process). It saved me from needing to do a lot of trial and error work for this project. And it’s available in all versions of Visual Studio 2010 (Express and Pro+, alike).
Important notes. To use your Content Pipeline extension projects, you need to add a reference to them to your XNA game’s Content project. Then you need to view the properties of the assets in question and, if necessary, set them to use your importers and processors. If you follow my advice and stick the run-time reader and run-time type into a game library, then you need to add a reference to that game library to your game’s references. As always, the code is heavily commented. If you follow the files in the order they are listed above (and then view Game1.cs in Test Game at the end), it should hopefully illustrate clearly the process of creating both a full and a partial extension. Also, while I only included a Windows test game, the extensions should work equally well with Xbox 360 and Windows Phone 7 projects. The code is licensed under the terms of the Microsoft Public License. You can download it here: Content Pipeline Extension Sample (XNA 4.0).