Software Sprites With a Tile Based Display

Software Sprites With a Tile Based Display

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.

Leave a Reply