Sometimes I feel like I'm just talking to myself here, but documenting things as I'm learning them is certainly easier, and if seeing me go through this process helps someone else out, then all the better.
When I ditched the Mode 3 sprite system, it reminded me of the "There is no spoon" scene from The Matrix. The sprites[] array is only there as a temporary holding spot for the information needed for blitting, and that under the hood it all just boils down to blitting stuff into RAM tiles each frame, and keeping track of what was in VRAM before the blit so it can be restored after the pixels are output. "There is no sprite."
So this got me thinking that the blits for a character don't have to be in a tiled grid pattern, they can be disjointed, or on top of each other with transparency, and in the back of my mind I remembered the
Partitioned Sprites that Lee had written about many years ago on our wiki. I had never tried that before, and it seemed like a lot of work to implement in the old system, but with blits, it should be really easy, and I would get the benefit of being able to blit things that are different sizes and shapes that share the same origin point, so I no longer needed to translate each individual object that I needed to draw based on how big it is, or which way it is facing, because tile maps always have the origin point set in the top left corner of the megasprite.
So I came up with my own map format, and I've been calling them BlitMaps to differentiate them from the TileMaps from before, and I set about converting all of my tile maps to blit maps, paying attention to minimizing the bounding boxes (and thus RAM tiles needed) as described in Lee's document, and thinking about how I can reuse the same blit for multiple megablits. For instance, now I only have a single head tile that I can use for all of the poses, and I can just blit that at whatever offset it should be drawn at for that pose.
Here is an example of how I carved up the first walking pose:

- tilemap_converted_to_blits.png (12.67 KiB) Viewed 685 times
and here is what its BlitMap looks like with the origin point set to 16 pixels above where the front leg starts:
Code: Select all
// This is the starting tile number for the BlitMaps
#define S (20)
const int8_t blitmap_walk0[] PROGMEM =
{
3, // Number of blits (if the high bit is set, all blits in the map should be flipped on the X axis)
S, // tileIndex
-2, // X offset of blit
0, // Y offset of blit
S + 1,
0,
8,
S + 2,
6,
7,
};
and I just continued to carve up each of my poses (testing as I went along, because it's easy to mess up!) until everything was now stored as a BlitMap. The function for blitting these BlitMaps is pretty simple, though I wanted to bake x-mirroring into the BlitMap, to save on having to have duplicate tiles that are just mirrored images of each other, and so I don't have to account for mirroring things in the code later. For that I hijacked the top bit of the first element in the BlitMap, the length field since you would run out of CPU if you tried to blit 255 things. If I need y-mirroring later I can just use the second highest bit for that.
And then I wrote the function to churn through a BlitMap:
Code: Select all
void BlitMyBlitMap(uint8_t spriteFlags, const int8_t *blitmap, uint8_t map_x, uint8_t map_y, bool doblit)
{
uint8_t xflip_len = (uint8_t)pgm_read_byte(&blitmap[0]);
int8_t len = xflip_len & 0x7F;
if (xflip_len & 0x80) { // If the high bit of len is set, flip the entire sprite on X
if (spriteFlags & SPRITE_FLIP_X)
spriteFlags &= ~SPRITE_FLIP_X;
else
spriteFlags |= SPRITE_FLIP_X;
}
int8_t xdir;
int8_t xoff;
if (spriteFlags & SPRITE_FLIP_X) {
xdir = -1;
xoff = TILE_WIDTH * (PLAYER_TILE_WIDTH - 1);
} else {
xdir = 1;
xoff = 0;
}
int8_t ydir;
int8_t yoff;
if (spriteFlags & SPRITE_FLIP_Y) {
ydir = -1;
yoff = TILE_HEIGHT * (PLAYER_TILE_HEIGHT - 1);
} else {
ydir = 1;
yoff = 0;
}
for (int8_t i = 0, pos = 1; i < len; ++i, pos += 3)
MyBlitSprite(spriteFlags,
(uint8_t)pgm_read_byte(&blitmap[pos]),
map_x + xoff + xdir * (int8_t)pgm_read_byte(&blitmap[pos + 1]),
map_y + yoff + ydir * (int8_t)pgm_read_byte(&blitmap[pos + 2]),
doblit);
}
PLAYER_TILE_WIDTH and PLAYER_TILE_HEIGHT are #defines of 1 and 2 respectively, and doblit is a boolean that tells the MyBlitSprite() function if we are going to blit the sprite (true) or if we just want to reserve the RAM tiles without doing the blit (false).
This ended up reducing my flash usage by having way fewer sprite tiles, it reduced the CPU time needed for blitting, because there are fewer blits per "megablit", and it reduced the number of RAM tiles needed to display the same sized character unaligned on average. So a triple win!
It also made me think about what can happen next now that I'm using these BlitMaps...
I only have a single unicorn head tile now, what if I add two parameters to the BlitMyBlitMap() function: replaceTile and withTile. Then as it blits the map, any time it sees the head tile (#20) I can replace it with a different head, maybe one where the horn is glowing red because you're using magic, or the tongue is sticking out because you just ate a lollipop, or the eyes blink because you are idle. What if I instead allocate a user RAM tile for this, and now at runtime I can change the way the head looks with special effects, or if it takes damage I can tweak individual pixels.
Or maybe certain powerups can change the look (add wings that get blitted on top in any pose?) Or maybe it can carry objects around the level and have them float nearby with its magic. All I would need to do would be add an extra call to the blitter, I wouldn't need to mess around with trying to find the proper sprite number so I get the z-order correct, and then have to deal with the sprite numbers for everything else getting messed up. Nope, there are no sprites, it's all just blits now, and I can do them whenever I need to, and let the flickering algorithm works its magic drawing the rest of the entities. Exciting times!