Automatic XNB serialization and *Content classes

Automatic XNB serialization and *Content classes

Rate This
  • Comments 9

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:

  • XmlImporter reads the source XML into a SpriteContent object
  • The automatic XNB serializer writes the SpriteContent into an .xnb file
  • My game calls ContentManager.Load<Sprite>
  • It gets back an instance of the runtime Sprite class

 

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.

  • Thank you for posting about this! Figuring out this problem was driving me crazy until you shared the solution on the forums. Hopefully this blog post will save other people some stress.

  • Thats a really great solution. My only worry (which isnt exactly huge) is that you still cant constrain T - Texture2D and Texture2DContent are completely different classes, so you still rely on someone getting it right at both ends (which you do with all content loading).

    But its definitely a better solution. I guess its just my pedantic nature to try to stop silly people doing things like Sprite<Audio> or some such nonsense :)

  • I tend to use (yet) another option which looks a bit like this:

       public class Sprite

       {

           public Rectangle Rectangle;

           public string TextureName;

           public Texture2D Texture;

           public void Init(ContentManager content)

           {

               Texture = content.Load<Texture2D>(TextureName);

           }

       }

    That extra string variable does annoy me though. I think I'll experiment with the generic solution. Thanks for the post Shawn.

  • Lol Shawn, well written and great content. Thankyou.

  • Speaking of XNBs with Texture2D ...

    Is there any way to view the XNB that gets created by the SpritePacker?  

    I tried some that I found on other sites and they don't work on WP7 generated items.

    Thanks,

    Doug Mair (doug.mair@gmail.com)

  • > Is there any way to view the XNB that gets created by the SpritePacker?  

    You can load any .xnb file into an object of whatever type it contains by calling ContentManager.Load.

    .xnb data is specialized per platform, though, so if you built the content for Windows Phone, you will have to load it on Windows Phone also. If you want to load the data on Windows, you must build it for Windows.

  • @Shawn:  Is there a way to tell the content pipeline to populate a property with a content-item of a given name?  eg.

    my XML could look (say) like this:  <Texture name="SomeTextureFile"></Texture>

    And then at runtime, the content pipeline would basically do

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

  • BlueRaja: absolutely, this is a perfect use for a custom content processor.

    You might find this talk useful: www.microsoft.com/.../details.aspx

  • @Shawn: No thanks, using Roonda's method for each class is much simpler than writing a custom content processor for each class.  I was just curious if there was some simpler, undocumented method to load a property from the content pipeline.

    I recently found out there's the (undocumented) ability to specify arrays of a custom type in the XML without writing a custom content-processor (from http://glennwatson.net/?p=143), and was hoping there were more neat tricks like this.  Thanks anyways!

Page 1 of 1 (9 items)
Leave a Comment
  • Please add 7 and 7 and type the answer here:
  • Post