Sega Genesis Programming Part 3: Sprite Animation


Where we left off

In part 1 of my attempt to learn Genesis programming I started with simply loading palettes. In part 2 I loaded up some tiles and created a very basic sprite. This article/tutorial will build on part 2 by making the background tiles a little more interesting and creating an animated sprite.

I originally planned to tackle background music next but that's turning out to be a bigger effort than expected, and I expected it to be huge. There's source code to a few different Genesis sound drivers floating around the internet but they all come from decompiled games. I'm going to assume those are not legal to copy. When I take this one on again I'll start by seeing if there are some public domain sound drivers other developers have posted. If not, then it's going to be, well, interesting trying to figure that out.

Note: After writing this I found the Echo project which I'll try out in a future article.

So rather than flounder for a while I picked-up where I left off. The first thing I needed to figure out was a setting for this demo. I'm not sure where this Genesis programming experiment will go. I have about a dozen ideas but I'm trying to stay flexible. Most of my game programming experiments have died when I had a grand idea that was really big to implement and I lost steam halfway though. I'm combating it by not going after anything specific too early.

I need a setting though. If I create an animated sprite it has to be moving around somewhere. In the first two articles I was going for something outdoorsy because it was a good excuse to test four different palettes. I'm going to move this demo inside because the outdoors implies a big open environment and I'm not anywhere near ready to tackle that. I'm also going to set it in a time when the Sega Genesis was new. By writing this in assembly I'm partially recreating the experience of an early Genesis developer so why not set the demo then too.

Let's combine those ideas and go with shopping mall in 1989. Again, I have no idea where this will all end up but that setting opens up room for a ton of possibilities.

The first step in creating a mall is designing ugly mall store carpet. Let's go with a 2x2 pattern of carpet with a grayish, slightly stained, appearance. That requires four tiles:


FloorTiles:
dc.l $42424212
dc.l $42444242
dc.l $12424242
dc.l $42424242
dc.l $42424242
dc.l $42424242
dc.l $44424244
dc.l $42424242
dc.l $42424242
dc.l $42124242
dc.l $42424242
dc.l $42424242
dc.l $42424242
dc.l $44424242
dc.l $42422242
dc.l $42424242
dc.l $42424242
dc.l $42434242
dc.l $42424244
dc.l $42424242
dc.l $42424242
dc.l $42424142
dc.l $42424242
dc.l $42424242
dc.l $42424242
dc.l $44424242
dc.l $42424212
dc.l $42424242
dc.l $12424442
dc.l $42424242
dc.l $42424243
dc.l $42424242

Next the routine to load the background tiles needs to be modified so it draws them in the 2x2 pattern. It needs to draw one row alternating between tiles 0 and 2, the next row alternating between tiles 1 and 3, then looping back 0 and 2, and so on.


FillBackground:
;-------------------------------------------------------------------------------
; Pattern notes
; bits 0-10 = pattern number
; bit 11 = horizontal reverse bit (1=reverse)
; bit 12 = vertical reverse bit (1=reverse)
; bit 13 = palette low bit
; bit 14 = palette high bit
; bit 15 = priority
;-------------------------------------------------------------------------------
move.w #$0001,d0 ; store tile pattern in d0
move.b #%0011,d3 ; eor value used to toggle tiles
move.l #$40000003,(VDP_CONTROL) ; initial drawing location
move.w #$001F,d1 ; 32 rows
DrawTileRowLoop:
move.w #$003F,d2 ; 64 columns per row
DrawTileColLoop:
move.w d0,(VDP_DATA) ; copy the pattern to VPD
eor.b d3,d0 ; flip between patterns
dbra d2,DrawTileColLoop ; loop to next tile
eor.b #%0010,d0 ; flip between tiles 1 and 3
eor.b #%0100,d3 ; flip the flip pattern
dbra d1,DrawTileRowLoop ; loop to next tile

Now that we have some horrible mall carpet we need "dude working in a store in 1989". Since I'm lazy I'm using a model that looks a lot like the sprites from Phantasy Star II. I should change to use something from the public domain in the next iteration.

I'm so lazy that I don't want to code the tiles for this dude by hand so I wrote a tile & palette editor that generates 68k assembly code.

Palette and tile code generator

Note: I've since scraped this tool in favor of the ones here - https://github.com/huguesjohnson/RetailClerk89/tree/master/src/build-tools

I realize there are dozens of perfectly good tile & palette editors out there, including ones I've written, but I wanted something that produced source code rather than modified a ROM.

After working with this tool for a while I produced animations for all four directions with two animation frames per direction. I won't post all the code here (it's available for download at the end), it all pretty much looks like:


SpritePlayerDownFrameZero:
;column 0 - row 0 
dc.l $00000111
dc.l $00001222
dc.l $00012222
dc.l $00122323
dc.l $00122333
dc.l $00123313
dc.l $00123313
dc.l $00011333
;column 0 - row 1
dc.l $00001333
dc.l $00014133
dc.l $00144C44
dc.l $014444C4
dc.l $0144144C
dc.l $0144144C
dc.l $0144144C
dc.l $0144144C
[...]

Although there's no struct keyword in assembly we can create a structure for storing information about sprites. We need this later on so sub-routines that manipulate sprites can be passed a starting address for the structure instead of setting data registers. There are only 7 data registers to work with in the Genesis so they're bad for passing around complex structures.

Here's the structure for the player sprite:


MEM_PLAYER_SPRITE_ID=$00FF000A ; sprite table id
MEM_PLAYER_SPRITE_X=$00FF000C ; world x position of the player
MEM_PLAYER_SPRITE_Y=$00FF000E ; world y position of the player
MEM_PLAYER_SPRITE_PATTERN_INDEX=$00FF0010 ; index of pattern in VDP
MEM_PLAYER_SPRITE_DIRECTION=$00FF0012 ; which direction the player faces
MEM_PLAYER_SPRITE_FRAME=$00FF0014 ; animation frame of player sprite
MEM_PLAYER_SPRITE_STEP_COUNTER=$00FF0016 ; used to determine when to move

I realize I could save memory by combining some of these into single words with high and low bytes. Right now I'm far more interested in ease of debugging. This can be refactored later. The Genesis sprite table already contains x and y values but those represent where the sprite is on the screen not in the virtual world. There's no scrolling in this demo yet but there has to be at some point so I need to track the virtual sprite location somewhere.

Anyway, this creates a structure of:


; aX = SPRITE_ID
; aX + 2 = SPRITE_X
; aX + 4 = SPRITE_Y
; aX + 6 = SPRITE_PATTERN_INDEX
; aX + 8 = SPRITE_DIRECTION
; aX + A = SPRITE_FRAME
; aX + C = SPRITE_STEP_COUNTER

The sub-routines to move the sprite will rely on a starting address and follow this structure.

At some point I need to note that when I started this endeavor I based my code on this article: https://bigevilcorporation.co.uk/2012/05/05/sega-megadrive-8-animated-sprites/

It's a great tutorial and my initial code to animate the sprite was based on it. I changed the approach a bit though and my final product is different now.

We're going to need a couple sub-routines to animate our mall employee. The first is one to set a sprite's pattern based on the direction and animation frame:


SetSpritePattern:
;---------------------------------------------------------------------------
; a6 = SPRITE_ID
;---------------------------------------------------------------------------
; setup
movea.l a6,a5 ; store address in a5 because it is manipulated
move.w (a5),d5 ; copy sprite id to d5
mulu.w #$08,d5 ; multiply sprite ID by 8 to get sprite array offset
; change the sprite pattern
add.b #$04,d5 ; palette and pattern is at index 4 in the sprite definition
swap d5 ; move to upper word
add.l #VRAM_SPRITE_TABLE,d5 ; add to sprite table address
; set the pattern, pattern=(base pattern number)+(direction*24)+(frame*8)
adda.l  #$6,a5 ; move to a6+6 -> SPRITE_PATTERN_INDEX
move.w (a5),d6 ; start with base pattern in d6
adda.l #$2,a5 ; move to a6+8 -> SPRITE_DIRECTION
move.w (a5),d7 ; copy direction to d7
mulu.w #$18,d7 ; multiply direction * 24
add.w d7,d6 ; add result to d1
adda.l #$2,a5 ; move to a6+A -> SPRITE_FRAME
move.w (a5),d7 ; copy frame to d7
mulu.w #$08,d7 ; multiply frame * 8 since there are 8 tiles per frame
add.w d7,d6 ; add result to d6
add.w #$2000,d6 ; add palette and other sprite info
move.l d5,(VDP_CONTROL) ; set write location in VDP
move.w d6,(VDP_DATA) ; write the new pattern
ExitSetSpritePattern:
rts

This may seem backwards but before we move a sprite we need a method to stop a sprite, which really just resets their animation to the standing still frame.


StopSprite:
;---------------------------------------------------------------------------
; a6 = SPRITE_ID
;---------------------------------------------------------------------------
; setup
movea.l a6,a5 ; store address in a5 because it is manipulated
move.w (a5),d5 ; copy sprite id to d5
adda.l  #$A,a5 ; move to a5+A -> SPRITE_FRAME
move.w #$0000,(a5) ; reset animation frame
bsr.w SetSpritePattern ; branch to move SetSpritePattern
EndStopSprite:
rts

In the last article we read the controller input and moved the sprite based on it. That was hardwired to the player sprite. We need to split that apart so the controller input sets the direction of the player and a separate method moves them. This is so NPC sprites can be moved through the same method.

First let's map the input to the player sprite and decide whether they should start moving:


DIRECTION_DOWN=$0000
DIRECTION_UP=$0001
DIRECTION_LEFT=$0002
DIRECTION_RIGHT=$0003
SPRITE_MOVE_FREQUENCY=$012C
;-------------------------------------------------------------------------------
; MovePlayer
; moves the player sprite if a d-pad direction is being pressed
; calls MoveSprite to actually move the sprite
; d7 is used to test controller input
; a6 is used to store the start address of the sprite info if there is movement
;-------------------------------------------------------------------------------
MovePlayer:
move.b (MEM_CONTROL_HELD),d7 ; copy button held value to d7 for eor
eor.w #$0000,d7 ; test if it is zero
bne.w SetPlayerDirection ; branch if a direction is being held
PlayerNotMoving:
bra.w StopPlayerSprite ; reset the sprite frame
SetPlayerDirection:
; map key press to direction
TestUpHeld:
move.b (MEM_CONTROL_HELD),d7 ; copy button held value to d7 for andi
andi.w #BUTTON_UP_PRESSED,d7 ; test if the up button is held
beq.s TestDownHeld ; branch if not
move.w #DIRECTION_UP,(MEM_PLAYER_SPRITE_DIRECTION) ; set direction
bra.s MovePlayerSprite ; move the player sprite
TestDownHeld:
move.b (MEM_CONTROL_HELD),d7 ; copy button held value to d7 for andi
andi.w #BUTTON_DOWN_PRESSED,d7 ; test if the down button is held
beq.s TestLeftHeld ; branch if not
move.w #DIRECTION_DOWN,(MEM_PLAYER_SPRITE_DIRECTION) ; set direction
bra.s MovePlayerSprite ; move the player sprite
TestLeftHeld:
move.b (MEM_CONTROL_HELD),d7 ; copy button held value to d7 for andi
andi.w #BUTTON_LEFT_PRESSED,d7 ; test if the left button is held
beq.s TestRightHeld ; branch if not
move.w #DIRECTION_LEFT,(MEM_PLAYER_SPRITE_DIRECTION) ; set direction
bra.s MovePlayerSprite ; move the player sprite
TestRightHeld:
move.b (MEM_CONTROL_HELD),d7 ; copy button held value to d7 for andi
andi.w #BUTTON_RIGHT_PRESSED,d7 ; test if the right button is held
beq.s StopPlayerSprite ; non-directional button held
move.w #DIRECTION_RIGHT,(MEM_PLAYER_SPRITE_DIRECTION) ; set direction
MovePlayerSprite:
add.w #$0001,(MEM_PLAYER_SPRITE_STEP_COUNTER) ; increment counter
move.w (MEM_PLAYER_SPRITE_STEP_COUNTER),d7 ; copy counter to d7
cmpi.w #SPRITE_MOVE_FREQUENCY,d7 ; is it time to move?
bls.w EndMovePlayer ; exit if it's not time to move
move.w #$0000,(MEM_PLAYER_SPRITE_STEP_COUNTER); reset the step counter
lea (MEM_PLAYER_SPRITE_ID),a6 ; setup call to MoveSprite
bsr.w MoveSprite ; branch to move MoveSprite 
bra.s EndMovePlayer ; exit
StopPlayerSprite:
lea (MEM_PLAYER_SPRITE_ID),a6 ; setup call to StopSprite
bsr.w StopSprite ; reset the sprite frame
EndMovePlayer:
rts

There's probably some trickery I can pull to shorten that method a bit. My coding approach is always "make it work then worry about making it better, or don't".

Now we need the method to move a sprite while cycling through the animation frames, this could also probably be shortened:


MoveSprite:
;---------------------------------------------------------------------------
; a6 = SPRITE_ID
;---------------------------------------------------------------------------
movea.l a6,a5 ; store address in a5 because it is manipulated
adda.l #$8,a5 ; move to a5+8 -> SPRITE_DIRECTION
move.w (a5),d7 ; store direction in a7
TestUp:
cmpi.w #DIRECTION_UP,d7 ; test for up
bne.w TestDown ; branch if not
MoveUp:
suba.l #$4,a5 ; move back to a5+4 -> SPRITE_Y
cmpi.w #$0081,(a5) ; is the sprite at the top edge?
bge.w MoveUpIncFrame ; not at the top edge, keep moving
bsr.w StopSprite ; at top edge, stop moving
bra.w ExitMoveSprite ; at top edge, exit
MoveUpIncFrame:
sub.w #SPRITE_STEP_PIXELS,(a5) ; decrement SPRITE_Y
adda.l #$6,a5 ; move up to a5+A -> SPRITE_FRAME
add.w #$0001,(a5) ; increment counter
move.w (a5),d7 ; copy current frame to d7
cmpi.w #SPRITE_FRAME_COUNT,d7 ; do we need to loop back to the start?
bls.w MoveUpAnimation ; if not, go to animation
MoveUpResetFrame:
move.w #$0001,(a5) ; toggle between frames 1&2 while moving
MoveUpAnimation:
; move the sprite in the sprite table
move.w (a6),d7 ; copy sprite ID to d7
mulu.w #$08,d7 ; mult by 8 to get sprite array offset
swap d7 ; move to upper word
add.l #VRAM_SPRITE_TABLE,d7 ; add to sprite table address
move.l d7,(VDP_CONTROL) ; set write location in VDP
; update the sprite animation 
suba.l #$6,a5 ; move back to a5+4 -> SPRITE_Y
move.w (a5),(VDP_DATA) ; copy the new y-coordinate
bsr.w SetSpritePattern ; set the new pattern for the sprite
bra.w ExitMoveSprite ; only 4-direction movement in this demo
TestDown:
cmpi.w #DIRECTION_DOWN,d7 ; test for down
bne.w TestRight ; branch if not
MoveDown:
suba.l #$4,a5 ; move back to a5+4 -> SPRITE_Y
cmpi.w #$0140,(a5) ; is the sprite at the bottom edge?
ble.w MoveDownIncFrame ; not at the bottom edge, keep moving
bsr.w StopSprite ; at bottom edge, stop moving
bra.w ExitMoveSprite ; at bottom edge, exit
MoveDownIncFrame:
add.w #SPRITE_STEP_PIXELS,(a5) ; increment SPRITE_Y
adda.l #$6,a5 ; move up to a5+A -> SPRITE_FRAME
add.w #$0001,(a5) ; increment counter
move.w (a5),d7 ; copy current frame to d7
cmpi.w #SPRITE_FRAME_COUNT,d7 ; do we need to loop back to the start?
bls.w MoveDownAnimation ; if not, go to animation
MoveDownResetFrame:
move.w #$0001,(a5) ; toggle between frames 1&2 while moving
MoveDownAnimation:
; move the sprite in the sprite table
move.w (a6),d7 ; copy sprite ID to d7
mulu.w #$08,d7 ; mult by 8 to get sprite array offset
swap d7 ; move to upper word
add.l #VRAM_SPRITE_TABLE,d7 ; add to sprite table address
move.l d7,(VDP_CONTROL) ; set write location in VDP
; update the sprite animation 
suba.l #$6,a5 ; move back to a5+4 -> SPRITE_Y
move.w (a5),(VDP_DATA) ; copy the new y-coordinate
bsr.w SetSpritePattern ; set the new pattern for the sprite
bra.w ExitMoveSprite ; only 4-direction movement in this demo
TestRight:
cmpi.w #DIRECTION_RIGHT,d7 ; test for right
bne.w TestLeft ; branch if not
MoveRight:
suba.l #$6,a5 ; move back to a5+2 -> SPRITE_X
cmpi.w #$01B0,(a5) ; is the sprite at the right edge?
ble.w MoveRightIncFrame ; not at the right edge, keep moving
bsr.w StopSprite ; at right edge, stop moving
bra.w ExitMoveSprite ; at right edge, exit
MoveRightIncFrame:
add.w #SPRITE_STEP_PIXELS,(a5) ; increment SPRITE_Y
adda.l #$8,a5 ; move up to a5+A -> SPRITE_FRAME
add.w #$0001,(a5) ; increment counter
move.w (a5),d7 ; copy current frame to d7
cmpi.w #SPRITE_FRAME_COUNT,d7 ; do we need to loop back to the start?
bls.w MoveRightAnimation ; if not, go to animation
MoveRightResetFrame:
move.w #$0001,(a5) ; toggle between frames 1&2 while moving
MoveRightAnimation:
; move the sprite in the sprite table
move.w (a6),d7 ; copy sprite ID to d7
mulu.w #$08,d7 ; mult by 8 to get sprite array offset
add.b #$06,d7 ; x-coordinate is at index 6
swap d7 ; move to upper word
add.l #VRAM_SPRITE_TABLE,d7 ; add to sprite table address
move.l d7,(VDP_CONTROL) ; set write location in VDP
; update the sprite animation 
suba.l #$8,a5 ; move back to a5+2 -> SPRITE_X
move.w (a5),(VDP_DATA) ; copy the new y-coordinate
bsr.w SetSpritePattern ; set the new pattern for the sprite
bra.w ExitMoveSprite ; only 4-direction movement in this demo
TestLeft:
cmpi.w #DIRECTION_LEFT,d7 ; test for left
bne.w ExitMoveSprite ; branch if not
MoveLeft:
suba.l #$6,a5 ; move back to a5+2 -> SPRITE_X
cmpi.w #$0081,(a5) ; is the sprite at the left edge?
bge.w MoveLeftIncFrame ; not at the left edge, keep moving
bsr.w StopSprite ; at left edge, stop moving
bra.w ExitMoveSprite ; at left edge, exit
MoveLeftIncFrame:
sub.w #SPRITE_STEP_PIXELS,(a5) ; decrement SPRITE_Y
adda.l #$8,a5 ; move up to a5+A -> SPRITE_FRAME
add.w #$0001,(a5) ; increment counter
move.w (a5),d7 ; copy current frame to d7
cmpi.w #SPRITE_FRAME_COUNT,d7 ; do we need to loop back to the start?
bls.w MoveLeftAnimation ; if not, go to animation
MoveLeftResetFrame:
move.w #$0001,(a5) ; toggle between frames 1&2 while moving
MoveLeftAnimation:
; move the sprite in the sprite table
move.w (a6),d7 ; copy sprite ID to d7
mulu.w #$08,d7 ; mult by 8 to get sprite array offset
add.b #$06,d7 ; x-coordinate is at index 6
swap d7 ; move to upper word
add.l #VRAM_SPRITE_TABLE,d7 ; add to sprite table address
move.l d7,(VDP_CONTROL) ; set write location in VDP
; update the sprite animation 
suba.l #$8,a5 ; move back to a5+2 -> SPRITE_X
move.w (a5),(VDP_DATA) ; copy the new y-coordinate
bsr.w SetSpritePattern ; set the new pattern for the sprite
ExitMoveSprite:
rts

The end result isn't all that bad:

Animated sprite

The only issue is the animation is really, really fast. The mall employee dude is moving his limbs faster than a hummingbird. So I guess the next bit of work will be fixing the sprite animation and probably switching to something based off public domain graphics.

After that I'll either add some scenery and collision detection, or take a stab at music. It's also possible I'll try something completely different though.

Download the latest source code on GitHub




Related