Automatic XNB serialization and *Content classes

Originally posted to Shawn Hargreaves Blog on MSDN, Thursday, July 2, 2009

Automatic XNB serialization works best when the same classes are available at content build time and at runtime.

Pedantic correction: if you are making an Xbox game, they can't actually be the SAME classes. You must compile the code twice, once for use on Windows during the content build, then again for runtime use on Xbox. This is fine as long as the shared types live in an assembly that has the same name, version, and public key on both platforms.

Sometimes, though, it just isn't possible to use the same class in both places. For instance the Content Pipeline represents a CompiledEffect as an array of bytecode, but at runtime this same data is loaded into an Effect that sends its shader code through the driver to the GPU. Automatic XNB serialization does not work well for types like this. Fortunately, the things which change between build time and runtime are mostly low-level graphics types, for which the framework already provides the necessary ContentTypeWriter and ContentTypeReader, so you rarely need to bother writing these yourself.

A perniciously irritating situation arises if you have a custom data type that you want to share between build time and runtime, but which aggregates a low level type that is not the same in both places.

Aside: I love the word "pernicious" - it makes me happy whenever I find an excuse to use it  :-)

Let's say we are making a custom sprite class that contains a texture plus a rectangle indicating where it should be drawn on the screen. Should be simple and straightforward, neh? But what type should we use for the texture field? This needs to be a Texture2DContent at build time, but then becomes a Texture2D at runtime.

What to do?

 

Proxy Content Types

The most general purpose, flexible, yet longwinded and even perniciously (yay! my favorite word!) verbose solution is to make two versions of our Sprite class, for instance:

    public class Sprite
    {
        public Rectangle Rectangle;
        public Texture2D Texture;
    }

    [ContentSerializerRuntimeType("SharedDataTypes.Sprite, SharedDataTypes")]
    public class SpriteContent
    {
        public Rectangle Rectangle;
        public Texture2DContent Texture;
    }

We must factor these into separate assemblies, as described in the "Creating New Data Types" section of this article. Armed with these classes, we can create a SpriteContent object using the built-in XmlImporter to deserialize this XML:

    <?xml version="1.0" encoding="utf-8" ?>
    <XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
      <Asset Type="SharedDataTypes.SpriteContent">
        <Rectangle>32 32 256 128</Rectangle>
        <Texture>
          <Mipmaps>
            <Mipmap Type="Graphics:PixelBitmapContent[Microsoft.Xna.Framework.Graphics.Color]">
              <Width>2</Width>
              <Height>2</Height>
              <Pixels>
                <Row>FFFF0000 FF00FF00</Row>
                <Row>FF0000FF FF000000</Row>
              </Pixels>
            </Mipmap>
          </Mipmaps>
        </Texture>
      </Asset>
    </XnaContent>

Ok, it's silly to define texture data in XML like this. But hey, it's just an example. You get the idea, right?

Resulting data flow:

 

Dynamic Types

Ok, so it sucks having to make two versions of our custom data type. There must be a better way, neh? One option is to go all loosey goosey and use dynamic typing, changing the texture field to type object:

    public class Sprite
    {
        public Rectangle Rectangle;
        public object Texture;
    }

This way we can store either a Texture2DContent or a Texture2D in the same field, so we can use the same Sprite class at build time and runtime (with assemblies as described under "Sharing Data Types Between Build And Runtime" in this article).

We must tweak our source XML to match the new type:

    <?xml version="1.0" encoding="utf-8" ?>
    <XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
      <Asset Type="SharedDataTypes.Sprite">
        <Rectangle>32 32 256 128</Rectangle>
        <Texture Type="Graphics:Texture2DContent">
          <Mipmaps>
            <Mipmap Type="Graphics:PixelBitmapContent[Microsoft.Xna.Framework.Graphics.Color]">
              <Width>2</Width>
              <Height>2</Height>
              <Pixels>
                <Row>FFFF0000 FF00FF00</Row>
                <Row>FF0000FF FF000000</Row>
              </Pixels>
            </Mipmap>
          </Mipmaps>
        </Texture>
      </Asset>
    </XnaContent>

Note how the <Asset Type> attribute has changed from SpriteContent to Sprite, and we added a Type attribute to the <Texture> element. This was not needed when using a proxy content type, because IntermediateSerializer already knew this field was of type TextureContent, but now our field is of type object, the XML must explicitly specify what type of object it wants to create.

The downside is we must now add pernicious (huzzah! there's that word again!) and ugly casts to any code that uses the Texture field, for instance to draw the sprite:

    spriteBatch.Draw((Texture2D)sprite.Texture, sprite.Rectangle, Color.White);

 

Generics

If we define our Sprite class like so:

    public class Sprite<T>
    {
        public Rectangle Rectangle;
        public T Texture;
    }

We can specialize this generic definition to make the same Texture field be either a Texture2DContent or a Texture2D.

We must tweak our source XML to match the new type:

    <?xml version="1.0" encoding="utf-8" ?>
    <XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
      <Asset Type="SharedDataTypes.Sprite[Graphics:Texture2DContent]">
        <Rectangle>32 32 256 128</Rectangle>
        <Texture>
          <Mipmaps>
            <Mipmap Type="Graphics:PixelBitmapContent[Microsoft.Xna.Framework.Graphics.Color]">
              <Width>2</Width>
              <Height>2</Height>
              <Pixels>
                <Row>FFFF0000 FF00FF00</Row>
                <Row>FF0000FF FF000000</Row>
              </Pixels>
            </Mipmap>
          </Mipmaps>
        </Texture>
      </Asset>
    </XnaContent>

Note how there is no longer any need for a Type attribute on the <Texture> element, while the <Asset Type> attribute must now specify what type the generic should be specialized on.

A subtle thing happens here. At build time we are dealing with a Sprite<Texture2DContent>, but at runtime that same data becomes a Sprite<Texture2D>:

    sprite = Content.Load<Sprite<Texture2D>>("sprite");

This works because the automatic XNB serializer is smart enough to understand generics. When it sees a generic type Foo<T>, it looks not only to see whether Foo has a different runtime type, but also to see what the runtime equivalent of T is. If Foo is the same at build time and runtime, but T changes to S, it will automatically update the generic to become Foo<S>.

I think this last solution is my favorite. It doesn't offer much excuse for repeating the word "pernicious", but it makes me happy just the same.

Blog index   -   Back to my homepage