Sprites are freely moving small graphics that normally make up the "objects" in the game world (players, enemies, items, etc.). They're also sometimes used for more mundane stuff like the cursor in a menu.
- How sprites are stored
- Setting up the VDP
- Building the sprite table
- Step 1: clearing the table
- Step 2: inserting a sprite
- Step 3: upload table to video memory
- Large sprites
- Sprite limits
- Beware of sprite cache
How sprites are stored
We need to learn how sprite graphics are stored first.
Sprites are made out of tiles. Their size can be anywhere from 1×1 to 4×4 tiles (i.e. 8×8 to 32×32 pixels). Width and height can be set separately, i.e. the sprite doesn't have to be square, it can be rectangular.
Tiles are arranged first vertically then horizontally:
Setting up the VDP
We're going to refer to the labels from the article about setting up the VDP.
First the VDP needs to know where we're going to put the sprite table.
It can be anywhere in VRAM, only restriction is that the address must be
either a multiple of
$200 (if 256px wide screen) or
$400 (if 320px wide). In case of doubt, go with the latter.
If you're using the setup code from the VDP
setup page, then the sprite table will be at
we may as well use that and move on:
SPRITE_ADDR: equ $F000
If you care more about it: the register that indicates the address is
VDPREG_SPRITE). Bits 15-9 of the sprite table address are
put into bits 6-0 of this register, or in other words, we need to shift
the address by 9 bits to the right.
This convenient macro will help us with that:
SetSpriteAddr macro addr move.w #VDPREG_SPRITE|((addr)>>9), (VdpCtrl) endm
Then somewhere in your initialization code (possibly replacing the write to the sprite register from the original code):
Building the sprite table
The easiest way to show sprites on screen is to rebuild the whole table from scratch in RAM every frame and copy it to video memory once all sprites have been added.
You will need this somewhere in RAM:
- A buffer for the sprite table (8 bytes × maximum number of sprites, or if you're unsure, just reserve 640 bytes which is the largest it can get)
- Number of sprites (a byte will do), to keep track of how many we have inserted so far
Every frame you would be doing this:
- Clear the sprite table
- Add every sprite to the table
- Upload sprite table to video memory
Step 1: clearing the table
When the frame starts the first thing you need to do is to "clear" the sprite table (our copy in RAM, that is, not the one in video memory).
This one is actually simple:
- Set number of sprites to 0
- Fill the first 8 bytes of the table with 0
The last point is important when showing no sprites.
ClearSprites: clr.b (NumSprites) ; Reset sprite count clr.l (SpriteTable+0) ; Clear first entry clr.l (SpriteTable+4) rts
Step 2: inserting a sprite
Now we need to do this step for every sprite we want to add. First some checks:
- Make sure the sprite is visible (discard it if too far from the screen)
- Make sure you didn't reach the sprites on screen limit (discard it if you did)
Now figure out the pointer of the sprite entry to be added. It goes like this (you should probably replace that multiply with a bit shift):
start of sprite table + (number of sprites × 8)
And now we can proceed to insert the sprite. We need to write 8 bytes as follows (yeah, the order of the fields is kind of awkward), explanation of each value follows below:
- 2 bytes for Y coordinate
- 1 byte for sprite size
- 1 byte for next sprite number
- 2 bytes for tile number + flags
- 2 bytes for X coordinate
X and Y coordinates
The position of the top-left corner of the sprite on screen. You need to add 128 to both to get the value to write in the table (i.e. the top left corner of the screen is at 128;128).
Note that not all the bits are used and hence the sprites will wrap around if they're too far from the screen. You should skip sprites that are clearly not visible to avoid issues (and also reduces the risk of running into one of the sprite limits).
Tile number and flags
The "base" tile number for the sprite, from 0 to 2047. This is the number for the first tile, the sprite will use consecutive tiles starting from this one. On top of this, the tile number can be OR'd with some flags to change the appearance of the sprite.
Bits 12-11 can be used to flip the sprite:
$0000to not flip
$0800to flip horizontally
$1000to flip vertically
$1800to flip both ways (180° flip)
Bits 14-13 pick the sprite palette:
$0000for palette 0
$2000for palette 1
$4000for palette 2
$6000for palette 3
Bit 15 sets the layer priority:
$0000for low priority
$8000for high priority
Here are some convenient constants for use in 68000 assembly:
NOFLIP: equ $0000 ; Don't flip (default) HFLIP: equ $0800 ; Flip horizontally VFLIP: equ $1000 ; Flip vertically HVFLIP: equ $1800 ; Flip both ways PAL0: equ $0000 ; Use palette 0 (default) PAL1: equ $2000 ; Use palette 1 PAL2: equ $4000 ; Use palette 2 PAL3: equ $6000 ; Use palette 3 LOPRI: equ $0000 ; Low priority (default) HIPRI: equ $8000 ; High priority
HIPRI|PAL3|ScoreTileId can be used to select
the tile ID for the score (whatever number
with high priority and palette 3. Since no flipping flags have been
specified, the sprite is not flipped.
The size of the sprite. Bits 3-2 are the width of the sprite, bits 1-0 are the height of the sprite. Take the size in tiles then substract one, i.e.
%00= 1 tile wide/high
%01= 2 tiles wide/high
%10= 3 tiles wide/high
%11= 4 tiles wide/high
If you're writing in 68000 assembly then these constants will be useful:
SPR_1x1: equ %0000 SPR_2x1: equ %0100 SPR_3x1: equ %1000 SPR_4x1: equ %1100 SPR_1x2: equ %0001 SPR_2x2: equ %0101 SPR_3x2: equ %1001 SPR_4x2: equ %1101 SPR_1x3: equ %0010 SPR_2x3: equ %0110 SPR_3x3: equ %1010 SPR_4x3: equ %1110 SPR_1x4: equ %0011 SPR_2x4: equ %0111 SPR_3x4: equ %1011 SPR_4x4: equ %1111
Next sprite number
The most awkward part.
Sprites don't have to come in a row, the video hardware can scan them in any order (except the first sprite, which must be sprite 0). It's usually not worth it, however. This field indicates what's the number of the next sprite (and 0 when the sprite list is over).
Usually the easiest way to handle this is to do this when inserting a new sprite (you can swap #1 and #2, but make sure #3 comes last):
- Write 0 in the link of the new sprite.
- Write the current number of sprites in the link of the previous sprite (if there's any).
- Increment number of sprites by 1 now.
To give an idea of how the code would look like... As you can see it's quite a bunch of code, so of course the best idea is to wrap this in a subroutine and let everything else piggyback on it.
; d0 = X coordinate ; d1 = Y coordinate ; d2 = tile + flags ; d3 = sprite size AddSprite: ; Don't bother if off-screen cmp.w #SCREEN_W, d0 ; Too far right? bge.s @Skip cmp.w #-32, d0 ; Too far left? ble.s @Skip cmp.w #SCREEN_H, d1 ; Too far down? bge.s @Skip cmp.w #-32, d1 ; Too far up? ble.s @Skip ; Get pointer to sprite table lea (SpriteTable), a0 ; Check sprite count move.b (NumSprites), d4 ; If 1st sprite, then skip beq.s @First ; most of this cmp.b #MAX_SPRITES, d4 ; If too many sprites, then bhs.s @Skip ; don't draw this sprite ; Get pointer to new entry moveq #0, d5 move.b d4, d5 lsl.w #3, d5 lea (a0,d5.w), a0 ; Update the link of the last sprite ; to point to the one we're inserting move.b d4, -5(a0) @First: ; Coordinates are offset by 128 add.w #128, d0 add.w #128, d1 ; Store the entry move.w d1, (a0)+ ; Y coordinate move.b d3, (a0)+ ; Sprite size move.b #0, (a0)+ ; Link move.w d2, (a0)+ ; Tile + flags move.w d0, (a0)+ ; X coordinate ; Update sprite count addq.b #1, d4 move.b d4, (NumSprites) @Skip: rts
Step 3: upload table to video memory
Once you're done adding all the sprites, it's time to copy the table to video memory.
First determine the length:
- If there are no sprites, it's 8 bytes
- Otherwise, it's number of sprites × 8 bytes
You need to always upload at least the first entry, since it's always used. If you're showing no sprites, we have to push it away and cut the table short there (this is why we fill that entry with 0 when we clear the table). The first point above ensures this.
Anyway: now copy the table to video memory (where you had set it with the relevant video register). Use a loop or DMA or whatever. Make sure to do this during vblank though (like any large writes you write to video memory).
Uploading the table the easy way
Ideally you should have already a way to copy the table quickly to VRAM (e.g. DMA transfers), but if you're just getting started and need something quick you could copy the table manually with a simple loop.
We're gonna use the
SetVramAddr macro from the page about
writing to video memory.
Now we copy it manually using a loop (remember this works by writing
VdpData). If there are sprites we copy all
the entries as-is, while if the aren't sprites we overwrite the first
entry (in VRAM) with zeroes (that'll push everything away).
UpdateSprites: lea (SpriteTable), a0 lea (VdpData), a1 ; Tell VDP where we'll write SetVramAddr SPRITE_ADDR ; Check how many sprites are there (note ; the moveq to extend to a larger size) moveq #0, d0 move.b (NumSprites), d0 beq.s @Empty ; Copy every sprite into VRAM ; Every entry is 8 bytes (two longwords) ; so we just do two long writes for each ; sprite to make it simpler subq.w #1, d0 @Loop: move.l (a0)+, (a1) move.l (a0)+, (a1) dbf d0, @Loop rts ; If we get here, the table has no sprites ; Fill first entry with zeroes (which happens ; to be d0's value, so we reuse that) @Empty: move.l d0, (a1) move.l d0, (a1) rts
Sprites can be up to 32px large, but often you see larger sprites in games. How?
While the Mega Drive can't show larger sprites per-se, you can split a large graphic into several smaller sprites (you can even reuse graphics to save memory). For example, the graphic below is split into four smaller sprites (marked by the red boxes).
There's a limit to how many sprites the video hardware can handle. Moreover, the sprite limit is directly proportional to the screen resolution (specifically, the width).
For 320px wide resolutions:
- Up to 80 sprites on screen
- Up to 20 sprites per line
- Up to 320 sprite pixels per line
For 256px wide resolutions:
- Up to 64 sprites on screen
- Up to 16 sprites per line
- Up to 256 sprite pixels per line
Beware of sprite cache
This is a warning for those who try to be clever by messing with the sprite table address (if you just set it once or always rewrite the table after changing it, this is not for you).
You can change the sprite table address at any time, but beware that there's a sprite cache. This cache holds the Y coordinate as well as sprite sizes and their order, but not the X coordinate and tile number + flags. The cache is flushed whenever you write to the sprite table, no matter how long it takes.
If you write a whole new table, this isn't a problem. But if you change the sprite address without writing new sprites, the cached values will be used and you'll end up with half the data from the old table and half the data from the new table.
Note that this can be exploited. Castlevania Bloodlines exploits this for a reflection effect by changing the sprite table in the middle of the screen (giving the sprites new X coordinate and tile number). And in Dragon's Castle this is exploited to allow underwater sprites to use a different palette without having to rewrite the whole table.