SpriteBatch sorting part 2
Continuing from yesterday...
Let's consider a practical example of how to sort sprites for good performance. Imagine I am making a game called Super Dromedary Racer Extreme, which requires a top down view of a piece of desert. My graphical elements are:
- The ground is a tilemap containing various arrangements of sand dunes.
- I have several types of object in the world: palm trees, flags
marking the start and finish lines, spectators, a couple of passing
kangaroos, and of course the dromedaries themselves.
- I want the dromedaries to kick up huge clouds of dust, which I'm
going to implement as a large number of alpha blended particle sprites
that gradually increase in size and rise upwards as they fade away.
The ground obviously needs to be drawn first, so that it will appear
below all the other objects. When I loop over my tile map, I will be
drawing the different sand dune tiles in a basically random order, but
there is a useful trick that can avoid the need for me to bother
sorting these sprites. Rather than using a different texture for each
type of tile, I can arrange all the possible tile designs into
different areas of a single larger texture. In my SpriteBatch.Draw call
I can then use the sourceRectangle parameter to specify which part of
that large texture should be used for each tile. Since all my ground
sprites are now using the same texture, I can draw them without sorting
and still get perfect batching, using the fastest possible immediate
sort mode:
spriteBatch.Begin(SpriteBlendMode.Opaque, SpriteSortMode.Immediate, SaveStateMode.None);
// draw all the ground tiles
spriteBatch.End();
Now to draw my game objects. Ideally I would like to be able to do this with some code along the lines of:
foreach (GameEntity dude in gameEntities)
{
spriteBatch.Draw(dude.Texture, dude.Position, Color.White);
if (dude.HasDustTrail)
{
foreach (DustParticle particle in dude.DustTrail)
{
spriteBatch.Draw(dustTexture, particle.Position, null,
Color.White, 0, Vector2.Zero, 1,
SpriteEffects.None, particle.LayerDepth);
}
}
}
But there are problems with this approach. Because my game entities
are not necessarily sorted by their texture, it will cause poor batch
performance. Worse, it won't even render the correct thing! Because my
alpha blended dust particles are overlapping each other (and could even
be merging with particles from a different dust cloud if several
dromedaries are racing near each other) these will need to be sorted by
depth to get a correct result. It seems impossible to get this right: I
need to sort by texture to get good performance, but also by depth to
get correct alpha blending, and I can't sort by two different
things at the same time! Whatever is a poor graphics coder to do?
I could draw all the entity sprites first using
SpriteSortMode.Texture, then end that SpriteBatch and start a new one
using SpriteSortMode.BackToFront for drawing the dust particles. But
then I would have to loop over all my entity objects twice, which seems
wasteful.
The solution is actually very simple:
spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Texture, SaveStateMode.None);
dustSprites.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.BackToFront, SaveStateMode.None);
foreach (GameEntity dude in gameEntities)
{
spriteBatch.Draw(dude.Texture, dude.Position, Color.White);
if (dude.HasDustTrail)
{
foreach (DustParticle particle in dude.DustTrail)
{
dustSprites.Draw(dustTexture, particle.Position, null,
Color.White, 0, Vector2.Zero, 1,
SpriteEffects.None, particle.LayerDepth);
}
}
}
spriteBatch.End();
dustSprites.End();
This relies on the fact that as long as you aren't using
SpriteSortMode.Immediate, SpriteBatch doesn't actually do any drawing
until you call the End method. That means you can begin several
overlapping batches with different settings, draw sprites in any order
using any combination of the batch instances, and control what order
the entire contents of each batch get drawn by the ordering of your End
calls.
The code is simple, the sorting gives correct alpha blending results, and the batching gives good performance. Hurrah!
Of course, this only works because all the dust particles are using
the same texture. If we had several different kinds of dust, sorting
these by depth would leave the textures in a random order and thus
cause poor batching performance. In that case we could use the same
trick as for the ground tiles: arrange all the possible dust particles
onto a single larger texture, so we can sort the particles however we
like and still avoid having to switch textures too often.