Take the 2-minute tour ×
Game Development Stack Exchange is a question and answer site for professional and independent game developers. It's 100% free, no registration required.

I'm attempting to get my head around the XNA content pipeline, and how I can use it to simplify my game code. Specifically, I want to define sprites, sprite sheets, and animations as assets and have them automatically available to me in game code.

At the end of the day, this is what I want to be able to do:

  • add a new file with .sprite extension to my content project
  • call contentManager.Load<Sprite>("NameOfSprite"); and get back a Sprite instance

My Sprite class looks like this:

public class Sprite
{
    public Texture2D Texture
    {
        get { ... }
    }

    public Rectangle? SourceRectangle
    {
        get { ... }
    }

    public Vector2 Origin
    {
        get { ... }
    }
}

The purpose of the Origin property is not important - it's specific to my game. The point is, I want it to be "packaged up" alongside the texture information.

I've defined a SpriteData class as:

public sealed class SpriteData
{
    public string TextureFilename
    {
        get;
        set;
    }

    public Rectangle? SourceRectangle
    {
        get;
        set;
    }

    public Vector2 Origin
    {
        get;
        set;
    }
}

And I've added a test.sprite file to my content project as:

<?xml version="1.0" encoding="utf-8" ?>

<XnaContent xmlns:data="MyNamespace">
  <Asset Type="data:SpriteData">
    <TextureFilename>Sprites\Test.jpg</TextureFilename>
    <SourceRectangle>0 5 20 25</SourceRectangle>
    <Origin>30 2</Origin>
  </Asset>
</XnaContent>

I have defined a SpriteImporter as:

[ContentImporter(".sprite", DisplayName = "Sprite Importer", DefaultProcessor = "SpriteProcessor")]
public class SpriteImporter : ContentImporter<SpriteData>
{
    public override SpriteData Import(string filename, ContentImporterContext context)
    {
        using (var xmlReader = XmlReader.Create(filename))
        {
            return IntermediateSerializer.Deserialize<SpriteData>(xmlReader, filename);
        }
    }
}

And a SpriteProcessor as:

[ContentProcessor(DisplayName = "Sprite Processor")]
public class SpriteProcessor : ContentProcessor<SpriteData, SpriteData>
{
    public override SpriteData Process(SpriteData input, ContentProcessorContext context)
    {
        var spritePath = Path.GetDirectoryName(context.OutputFilename);
        var textureReference = new ExternalReference<Texture2DContent>(input.TextureFilename);

        // make sure the texture is built (not sure what else to do here)
        context.BuildAsset<Texture2DContent, Texture2DContent>(textureReference, string.Empty);

        return input;
    }
}

Now I'm stuck. If I don't implement a content writer+reader combination then I won't get a Sprite out of the content manager - I'll get a SpriteData instead, which is not what I want. But my attempt to implement a SpriteWriter came up short. I have no idea how to get the texture information from the processor to the writer. Do I need to change the type from SpriteData to something else? Is there a sample anywhere that demonstrates how to do this end-to-end?

share|improve this question

4 Answers 4

up vote 3 down vote accepted

OK, I've found a way to do this. Whether it's the best way, I'm unsure. Any input appreciated. The key was to have my processor spit out an intermediate type, known only to my content pipeline. Here is my processor (my importer remains as is):

[ContentProcessor(DisplayName = "Sprite Processor")]
public class SpriteProcessor : ContentProcessor<SpriteData, SpriteContent>
{
    public override SpriteContent Process(SpriteData input, ContentProcessorContext context)
    {
        var spritePath = Path.GetDirectoryName(context.OutputFilename);
        var textureReference = new ExternalReference<Texture2DContent>(input.TextureFilename);

        // make sure the texture is built
        var builtTexture = context.BuildAsset<Texture2DContent, Texture2DContent>(textureReference, string.Empty);

        var spriteContent = new SpriteContent
        {
            Texture = builtTexture,
            SourceRectangle = input.SourceRectangle,
            Origin = input.Origin
        };

        return spriteContent;
    }
}

Note the new SpriteContent type, which is very simple:

public sealed class SpriteContent
{
    public ExternalReference<Texture2DContent> Texture
    {
        get;
        set;
    }

    public Rectangle? SourceRectangle
    {
        get;
        set;
    }

    public Vector2 Origin
    {
        get;
        set;
    }
}

With this in place, I changed my SpriteWriter as follows:

[ContentTypeWriter]
public sealed class SpriteWriter : ContentTypeWriter<SpriteContent>
{
    protected override void Write(ContentWriter output, SpriteContent value)
    {
        output.WriteExternalReference(value.Texture);
        output.Write(value.SourceRectangle.HasValue);

        if (value.SourceRectangle.HasValue)
        {
            output.Write(value.SourceRectangle.Value.X);
            output.Write(value.SourceRectangle.Value.Y);
            output.Write(value.SourceRectangle.Value.Width);
            output.Write(value.SourceRectangle.Value.Height);
        }

        output.Write(value.Origin);
    }

    public override string GetRuntimeReader(TargetPlatform targetPlatform)
    {
        return typeof(SpriteReader).AssemblyQualifiedName;
    }
}

I'm guessing this could be simplified to use built-in types, but wasn't sure how because they're all internal. Lastly, by SpriteReader looks like this:

public sealed class SpriteReader : ContentTypeReader<Sprite>
{
    protected override Sprite Read(ContentReader input, Sprite existingInstance)
    {
        var texture = input.ReadExternalReference<Texture2D>();
        Rectangle? sourceRectangle = null;

        if (input.ReadBoolean())
        {
            sourceRectangle = new Rectangle(input.ReadInt32(), input.ReadInt32(), input.ReadInt32(), input.ReadInt32());
        }

        var origin = input.ReadVector2();

        return new Sprite(texture, sourceRectangle, origin);
    }
}

With that in place, this works:

var testSprite = this.Content.Load<Sprite>(@".\Sprites\Test");

So that takes care of sprites. Now I need to tackle sprite sheets and animations, but I'll leave that for next weekend.

share|improve this answer

For a demonstration of how to create a ContentTypeWriter and a corresponding ContentTypeReader for your Sprite, take a look at the excellent answer to ContentSerializerRuntimeType required in content pipeline?.

To load a SpriteData as a Sprite, you'll be interested in the ContentSerializerRuntimeType attribute. This attribute specifies into which class to load serialized data. I also wrote a blog with an end-to-end example, though it's a more complicated scenario than what you're wanting to do.

share|improve this answer
    
The answer you linked to is one I've already seen. However, it does not include textures in the content type, so didn't really help. Checking out your blog entry now...thanks. –  me-- Jul 28 '12 at 16:53
    
Had a look at your blog and code sample. Maybe I'm being stupid but I still don't see how this answers my question. The code in your example does not "pass through" the texture - it exchanges it for something else entirely. In my case, I need to supplement the texture with other information, keeping the texture data intact. Not getting how to do that. –  me-- Jul 28 '12 at 17:15
    
Found a way to get this working - please see my answer. I'd be greatful for any input to improve my solution. –  me-- Jul 28 '12 at 17:49
    
I don't have enough reputation to comment on your answer directly, but your implementation uses the pipeline as best I understand it. Using the Texture2DContent to build the texture asset was the solution I arrived at, as well. Doesn't mean it's the best way, but it's the way I would have done it. –  bozalina Jul 28 '12 at 18:48

I see that you've posted your own solution for how you handled the particular question, but I want to post what I do with my SpriteSheet assets in case it helps you at all. Note that my answer does not define a ContentTypeWriter or ContentTypeReader because I did not need to use any special writing or reading rules. ContentSerializer knows how to serialize/deserialze at content build time / run time because I've used native types. I've supplied classes for the serialized .xnb file generated at content build time and the deserialized final product used at run time.

These are the four classes involved:

SpriteSheetData          (deserialized instance of XML data)
SpriteSheetProcessor     (input SpriteData, output SpriteContent)
SpriteSheetContent       (serialized at content build, deserialized at run time to Sprite)
SpriteSheet              (instance used by game)

Here's how my classes look (condensed for clarity):

First, define the deserialized instance of the SpriteSheetData:

public class SpriteSheetData
{
     public String FileName;
     public int HorizontalFrameCount;
     public int VerticalFrameCount;
     public int FrameWidth;
     public int FrameHeight;
     public int MillisecondsPerFrame;
}

Second, process the SpriteSheetData into a SpriteSheetContent:

[ContentProcessor(DisplayName = "ContentPipelineExtension.SpriteSheetProcessor")]
public class SpriteSheetProcessor : ContentProcessor<TInput, TOutput>
{
    public override TOutput Process(TInput input, ContentProcessorContext context)
    {
        String fileName = input.FileName;
        ExternalReference<TextureContent> textureReference = new ExternalReference<TextureContent>(fileName);

        TextureContent texture = context.BuildAndLoadAsset<TextureContent, TextureContent>(textureReference, "TextureProcessor");
        BitmapContent bitmap = texture.Faces[0][0];

        TOutput SpriteSheet = new TOutput();
        SpriteSheet.Texture.Mipmaps.Add(bitmap);
        SpriteSheet.FrameCount = new Point(input.HorizontalFrameCount, input.VerticalFrameCount);
        SpriteSheet.FrameSize = new Point(input.FrameWidth, input.FrameHeight);
        SpriteSheet.MillisecondsPerFrame = input.MillisecondsPerFrame;

        return SpriteSheet;
    }
}

Third, define the SpriteSheetContent with an attribute to identify its run time type (an instance of this class is generated at content build time):

[ContentSerializerRuntimeType("ContentPipelineExtensionLibrary.SpriteSheet, ContentPipelineExtensionLibrary")]
public class SpriteSheetContent
{
    public Texture2DContent Texture = new Texture2DContent();

    public Point FrameCount = new Point();

    public Point FrameSize = new Point();

    public int MillisecondsPerFrame = new int();
}

And finally, define the run time type with attributes to determine serialization at run time (an instance of this class is deserialized from .xnb file at run time):

public class SpriteSheet
{
    [ContentSerializer]
    public Texture2D Texture;

    [ContentSerializer]
    public Point FrameCount;

    [ContentSerializer]
    public Point FrameSize;

    [ContentSerializer]
    public int MillisecondsPerFrame;
}
share|improve this answer
    
Thanks for this (+1). It's different to how I intend doing sprite sheets, but useful to see other people's implementations. I intend dynamically taking individual textures and combining them into a single texture during the build process. That gives me the benefits of sprite sheets in terms of performance and ease of use in code, but alleviates me of the need to manually maintain them. –  me-- Aug 1 '12 at 9:26
    
In that case, make sure you check out create.msdn.com/en-US/education/catalog/sample/sprite_sheet –  Justin Skiles Aug 1 '12 at 11:45
    
yes, I had seen that. It still didn't make things click for me, mainly due to the way its design differs to my code base. I now have everything work, though, so it's all good. –  me-- Aug 6 '12 at 17:05

I think a better approach is to import the images as texture2D assets through your ide. After that you do something like:

Texture2D tex = Content.Load<Texture2D>(tex_string_name);

Then reference them by string in your Sprite, Animation, etc., classes.

share|improve this answer
    
This approach might work for you, but it doesn't for me, and it's not what I'm asking. I already do it this way and am looking to improve my code base. –  me-- Jul 28 '12 at 16:20

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.