Sprite Sheet Sample
This sample shows how to implement sprite sheets, combining many separate sprite images into a single larger texture that will be more efficient for the graphics card.Sample Overview
Graphics cards are slow to get going, but then extremely fast after they start. If you ask the graphics card to draw one single triangle, it will take a relatively long time for it to wake up and notice your request, but then it will draw the triangle in hardly any time at all. If you ask it to draw 100 triangles, it will take exactly the same amount of time to get going, and then draw all 100 triangles in not much longer time than it took for just one. To get the best performance, you want to give the graphics card large batches of triangles rather than feeding them to it one at a time.
The SpriteBatch class in the XNA Framework does exactly this. If you draw more than one sprite in a row using the same texture, it will batch them up, then give them all to the graphics card in one event. This works only when the sprites use the same texture, however. If you draw two sprites using texture A, then two using texture B, SpriteBatch will say, "Hey graphics card, here are two sprites. Now stop, change your texture. OK, now here are two more sprites." It would be faster if it could give all four sprites to the graphics card in a single batch, but it cannot do this because the texture is not the same.
What if you made a single bigger texture that contained the image from texture A and also the image from texture B, arranged next to each other? You could then use the overload of SpriteBatch.Draw that lets you specify a source rectangle parameter, telling the card which part of this bigger texture to display on the screen. The results will look the same as before, but now all four sprites use the same texture, so SpriteBatch can render them more efficiently.
Manually combining sprite images into larger sheets works well if you have just a few sprites, but it quickly becomes a burden as your game grows larger. When you have hundreds of sprites, it can be laborious having to manually pack them all into a single sprite sheet texture, and then remember where you put each image so you know what source rectangle to pass to SpriteBatch.Draw in your game code.
This sample automates the process of creating sprite sheets by using a custom content processor. You provide an XML file listing any number of individual bitmap files, one per sprite. The processor reads all these bitmaps, packs them into a single larger texture, and saves this new texture along with information recording what source rectangle should be used for each sprite. You can then look up your sprites by name rather than having to remember the specific coordinates for each image.
Sample Controls
This sample uses the following keyboard and gamepad controls.
Action | Keyboard control | Gamepad control | Windows Phone |
---|---|---|---|
Exit the sample. | ESC or ALT+F4 | BACK | BACK |
How the Sample Works
Sprite sheets are created from XML files that list any number of individual sprite bitmaps, for instance:
<XnaContent> <Asset Type="System.String[]"> <Item>cat.tga</Item> <Item>glow1.png</Item> <Item>glow2.png</Item> </Asset> </XnaContent>
To create a new sprite sheet, add an XML file in this format to your Content folder. Make sure the Content Importer property is set to XML Content - XNA Framework, and change the Content Processor setting to SpriteSheetProcessor. This will build all the sprite bitmaps listed in your XML file into a sprite sheet .xnb file, which you can load into your game by calling Content.Load<SpriteSheet>(...) .
See the SpriteSheetGame.Draw method for an example of how to use the SpriteSheet object. You can look up individual sprites either by name (as used for the cat) or by index (as used for the glow animation). You can perform math on the index values to cycle through multiframe sprite animations.
To reuse this code in your own game, add the SpriteSheetPipeline and SpriteSheetRuntime projects to your solution.
To make the pipeline project available for building your content
- Right-click the Content | References item in your content project.
- Click Add Reference.
- Click the Projects tab, and then select the SpriteSheetPipeline project.
To make the SpriteSheet class available to your game
- Right-click the References item in your main game project.
- Click Add Reference.
- Click the Projects tab, and select the SpriteSheetRuntime project
The SpriteSheetProcessor
The SpriteSheetProcessor class runs during the content build process. It is responsible for converting an array of strings that contain texture file names into a SpriteSheetContent object. The Content Pipeline XNB serializer then writes this SpriteSheetContent data into a binary .xnb file. At run time, the .xnb data is deserialized into a SpriteSheet object that can be used by your game. Note how the SpriteSheetContent class is decorated with a ContentSerializerRuntimeType attribute. This tells the serializer what type the content should be loaded into when you call ContentManager.Load. This attribute is needed because the design time SpriteSheetContent class is not the same as the runtime SpriteSheet class. The two types are similar, having the same fields in the same order, but not exactly the same, so this attribute is required to tell the serializer about their relationship. If you move the runtime SpriteSheet class into a different namespace or assembly, you must also update this attribute to match.
See http://msdn2.microsoft.com/en-us/library/bb447756.aspx for more information about the architecture of the Content Pipeline.
The hard work of this processor is implemented by the SpritePacker helper class. This class arranges many small sprite bitmaps onto a single larger sheet. The algorithm works as follows:
- Sort sprites by size, so the biggest ones will be arranged first. If you leave the smaller sprites until the end, it makes it more likely they could fit into holes left between earlier big ones.
-
Call PositionSprite for each sprite in turn, so you can decide where to put each one. This starts at the top left of the sheet, and checks whether it overlaps with any previously arranged sprite, as follows:
- If there is a collision, move to the right, looking for a free space.
- If it reaches the right edge of the sheet, wrap back to the left, and move down one row.
- Repeat until a free location is found.
When the packing process is complete, the processor calls ContentBuildLogger.LogImportantMessage, which reports how large a sheet it created and what percentage of this sheet contains sprite data versus unused space. Visual Studio displays this information in the output pane after building the content. Keep in mind that it is often impossible to find a perfect fit, so there will usually be some wasted space.
Texture Filtering
When you use sprite sheets, pixels from the edge of one sprite may bleed onto others. This often happens when you are scaling or rotating the sprites. This occurs because the graphics card applies filtering, averaging out the values of adjacent pixels to reduce aliasing. If one sprite is arranged next to another of a radically different color, this filtering may accidentally pick up some amount of that other color, which will cause a nasty looking border along the edge of your sprite.
The sprite sheet processor presented in this sample automatically avoids filtering problems by including one pixel of padding around the edge of each sprite while they are being arranged onto the sheet. The SpritePacker.CopySpritesToOutput method fills this border area with a copy of the colors from the edge of the sprite. Things will look acceptable even if the graphics card filters values from slightly outside the sprite itself.
Extending the Sample
This sample always outputs the sprite sheet in 32-bit Color format. To save space, you could change the SpriteSheetProcessor to convert the bitmap data into some other format, perhaps using DXT1 or DXT5 compression. You could even use a content processor parameter to make the compression optional for each sprite sheet.
To create a processor parameter, add the following property to the SpriteSheetProcessor class.
C# |
---|
[DefaultValue(false)] public bool Compress { get { return compress; } set { compress = value; } } bool compress; |
To implement the compression, add the following code at the end of SpriteSheetProcessor.Process, immediately before the return statement.
C# |
---|
if (compress) { spriteSheet.Texture.ConvertBitmapType(typeof(Dxt5BitmapContent)); } |
DXT compression requires texture sizes to be multiples of four. We need to make sure the generated sprite sheet will always be a valid size. The packing algorithm always produces sheets with a valid width. However, the height could be anything, so we must round it up to the next larger multiple of four. Add this line to SpritePacker.PackSprites immediately before the "Sort the sprites back into index order" comment:
C# |
---|
outputHeight = (outputHeight + 3) & ~3; |
After rebuilding the processor project, you can select any XML content file that is set to use the sprite sheet processor. Then open up the Visual Studio properties pane, expand the + sign next to the Content Processor property, and change the newly created Compress setting.