HuguesJohnson.com: 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

Here's the binary & code for it, I've done virtually no debugging or source code cleanup:

PaletteTileCodeGen.zip

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: http://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




Tweet