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

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:

Example: 4 by 3 sprite. First come all the tiles from the first column, then those of the second column, then those of the third column, then those of the fourth column.

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 $F000, so 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 VDP register $85xx (aka 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)

Then somewhere in your initialization code (possibly replacing the write to the sprite register from the original code):

    SetSpriteAddr SPRITE_ADDR

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:

Every frame you would be doing this:

  1. Clear the sprite table
  2. Add every sprite to the table
  3. 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:

The last point is important when showing no sprites.

    clr.b   (NumSprites)      ; Reset sprite count
    clr.l   (SpriteTable+0)   ; Clear first entry
    clr.l   (SpriteTable+4)

Step 2: inserting a sprite

Now we need to do this step for every sprite we want to add. First some checks:

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:

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:

Bits 14-13 pick the sprite palette:

Bit 15 sets the layer 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

For example: HIPRI|PAL3|ScoreTileId can be used to select the tile ID for the score (whatever number ScoreTileId is) with high priority and palette 3. Since no flipping flags have been specified, the sprite is not flipped.

Sprite size

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.

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):

  1. Write 0 in the link of the new sprite.
  2. Write the current number of sprites in the link of the previous sprite (if there's any).
  3. Increment number of sprites by 1 now.

Wrapping up

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

    ; 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)
    ; 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)

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:

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 everything into 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).

    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
    move.l  (a0)+, (a1)
    move.l  (a0)+, (a1)
    dbf     d0, @Loop
    ; 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)
    move.l  d0, (a1)
    move.l  d0, (a1)

Large sprites

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).

Some cat-like character. To the left, the graphic as it would be seen on screen, zoomed in. To the right, the same graphic, with red boxes showing the boundaries of the individual sprites making it up.

Sprite limits

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:

For 256px wide resolutions:

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.