Earlier today, I was talking about the Sega Mega Drive and how it sometimes used software sprites (but mostly its predecessor, the Sega Master System). This made me think of the technique for a while and how I haven’t seen it detailed in any length for a while. It was not that common, but still useful to make games that would defeat the sprite-limit in the hardware, or if the system had no sprite hardware at all in a given graphical mode.
First, let’s define a sprite. It’s going to be a simple one, a 8×8 bullet. Let’s make it that it only has 2 colors, meaning it only needs 1-bit per pixel (we’ll get to why this example is important later):
00111100 01000010 10011001 10100101 10100101 10011001 01000010 00111100
Let’s define a tiled screen made of several 8×8 tiles. For clarity sake, let’s just make a 4×4 one and name them using hexadecimal letters:
0 1 2 3 0 |0x0|0x1|0x2|0x3| 1 |0x4|0x5|0x6|0x7| 2 |0x8|0x9|0xA|0xB| 3 |0xC|0xD|0xE|0xF|
So, this screen has a resolution of 32×32 pixels. If you wanted this sprite to appear in a specific location, let’s say pixel position (21,19) for the top bit of the sprite, then you first would divide the number by 8 (by shifting the data right 3 bits):
tile_pos_x = pos_x >> 3; tile_pos_y = pos_y >> 3;
But this would just give, at best, the base location of our sprite, in this case, tile (2,1) or tile 0x6 in the previous scheme. but here’s the nice part. let’s grab those lost three bytes with a mask and call it an offset:
off_pos_x = pos_x & 0x07; off_pos_y = pos_y & 0x07;
this gives us (5, 3) – the specific offset of the sprite. So, to accommodate this change, we shift the bitmask of the sprite the given amount of data to the right, and at the same time move it downwards the specific amount:
for (int i = off_pos_y; i < 8; i++) tile_data0x6[i] = sprite[i - off_pos_y] >> off_pos_x;
This results in something like :
00000000 00000000 00000000 00000001 00000010 00000100 00000101 00000101
Well, now if you think about it, you just have to do the same for the next tile – in our case 0x7, except you want to move in the opposite direction:
for (int i = off_pos_y; i < 8; i++) tile_data0x7[i] = sprite[i - off_pos_y] << (8-off_pos_x);
So, we get:
00000000 00000000 00000000 11100000 00010000 11001000 00101000 00101000
It’s now logical that the bottom tiles (0xA and 0xB) are just a variant of this concept, displaying the missing bottom. Here’s a slightly organized pseudo-code:
for (int i = off_pos_y; i < 8; i++) { tile_data0x6[i] = sprite[i - off_pos_y] >> off_pos_x; tile_data0x7[i] = sprite[i - off_pos_y] << (8 - off_pos_x); } for (int i = 0; i < 8 - offpos_y; i++) { tile_data0x6[i] = sprite[off_pos_y + i] >> off_pos_x; tile_data0x7[i] = sprite[off_pos_y + i] << (8 - off_pos_x); }
This shapes up to this
(0x6) (0x7) 00000000 00000000 00000000 00000000 00000000 00000000 00000001 11100000 00000010 00010000 00000100 11001000 00000101 00101000 00000101 00101000 (0xA) (0xB) 00000100 11001000 00000010 00010000 00000001 11100000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
What is means is that you can use 4 tiles to draw a single, 8×8 sprite anywhere on the screen. You’ll normally want to update it every frame, so the basic limit to this technique is, other than the CPU power, the amount of VRAM writes you can do to the hardware during a v-blank (updating only the altered tiles, of course). You also have to remember when several sprites take the same tile pattern, what can be solved with a simple sorting test and then ORing the final sprite results as the new tile pattern.
Now, about the whole color issue. Most consoles store the tile data in planar layers of bits. So a 2-bit sprite (meaning a 4 color sprite) would look like this:
0 - 00111100 1 - 01000010 2 - 10011001 3 - 10100101 4 - 10100101 5 - 10011001 6 - 01000010 7 - 00111100 8 - 00111100 9 - 01111110 A - 11111111 B - 11100111 C - 11100111 D - 11111111 E - 01111110 F - 00111100
This means that you’d grab line 0 and line 8 and then join them together bit-by-bit, then line 1 and 9 and so on – so in the end this is a representation of:
00 00 11 11 11 11 00 00 = 00333300 00 11 01 01 01 01 11 00 = 03111130 11 01 01 11 11 01 01 11 = 31133113 11 01 11 00 00 11 01 11 = 31300313 11 01 11 00 00 11 01 11 = 31300313 11 01 01 11 11 01 01 11 = 31133113 00 11 01 01 01 01 11 00 = 03111130 00 00 11 11 11 11 00 00 = 00333300
While this was mostly to simplify the hardware, it helps that you can still perform the same type of shifts – you just do it on a per-plane manner instead of having to take into account the bit-depth of the whole sprite.