Sega Genesis Programming Part 2: Tiles and Sprites


Where we left off

In the first article in this series, and yes now that there are two it is a series, we covered the basics of building a Genesis game in Linux and loading palettes. In this section let's try loading a tile, a sprite, and using the controller to move the sprite around.

I'm sort of pleasantly surprised to make it this far. I expected myself to get bored and/or frustrated after a couple hours. Quite the opposite, although occasionally frustrating, I've found this therapeutic. I finished graduate school 11 years ago and haven't tackled something that really made me think in a different way since then. Assembly is very simple yet very complex. The real motivation though is the dream of seeing something I create run on an old piece of video game hardware. I've wanted to do that since my first Intellivision around age 8-10. It took me way too long to follow-through on that.

Anyway, back to the code. First things first, let's create some constants that we'll use throughout this example:


; 68k memory map
CTRL_1_DATA=$00A10003
CTRL_2_DATA=$00A10005
CTRL_X_DATA=$00A10007
CTRL_1_CONTROL=$00A10009
CTRL_2_CONTROL=$00A1000B
CTRL_X_control=$00A1000D
REG_HWVERSION=$00A10001
REG_TMS=$00A14000
PSG_INPUT=$00C00011
RAM_START=$00FF0000
VDP_DATA=$00C00000
VDP_CONTROL=$00C00004
VDP_COUNTER=$00C00008
Z80_ADDRESS_SPACE=$00A10000
Z80_BUS=$00A11100
Z80_RESET=$00A11200
; VDP access modes
VDP_CRAM_READ=$20000000
VDP_CRAM_WRITE=$C0000000
VDP_VRAM_READ=$00000000
VDP_VRAM_WRITE=$40000000
VDP_VSRAM_READ=$10000000
VDP_VSRAM_WRITE=$14000000
; buttons
BUTTON_UP_PRESSED=$01
BUTTON_DOWN_PRESSED=$02
BUTTON_LEFT_PRESSED=$04
BUTTON_RIGHT_PRESSED=$08
BUTTON_B_PRESSED=$10
BUTTON_C_PRESSED=$20
BUTTON_A_PRESSED=$40
BUTTON_START_PRESSED=$80
; program-specific values
MEM_VSYNCFLAG=$00FF00000 ; TODO - use a bit at this address instead
MEM_VSYNC_COUNTER=$00FF00002
MEM_DEBUG_MAINLOOP_COUNTER=$00FF00004
MEM_CONTROL_HELD=$00FF0006
MEM_CONTROL_PRESSED=$00FF0008
MEM_SPRITE_X=$00FF000A
MEM_SPRITE_Y=$00FF000C
MEM_SPRITE_STEP_COUNTER=$00FF000E

The majority of these are standard values for any Genesis game, the last set are specific to this demo. This demo is obviously not optimized. For example the vsync flag is a word when it could be a single bit in a word that contained multiple flags.

Tiles

The first part of this series just loaded a few palettes against an uni-color background. Booooring. Let's create a slightly less boring tile. Getting into compressed graphics is at least a few experiments away so let's create an uncompressed one, which is really just a bitmap.


GroundTileA:
 dc.l $AABBABCC
 dc.l $BAABBCCB
 dc.l $CBAABCBC
 dc.l $ACBAABAB
 dc.l $CCBBAAAB
 dc.l $CBAABCCB
 dc.l $BBABBCBA
 dc.l $BABBAABC

Now we need to load the tile into VDP memory. We'll do that by just copying it over bit-by-bit in a loop.


LoadTiles:
 move.l #$40200000,(VDP_CONTROL)
 lea GroundTileA,a0 ; load address of first tile to a0
 move.l #$27,d0  ; (8 x number of tiles to load) - 1    
LoadTilesLoop:
 move.l (a0)+,(VDP_DATA) ; move data to VDP & increment address
 dbra d0,LoadTilesLoop ; loop until all data is loaded

Low let's fill the background, again in a loop, with our tile. We'll only use one palette from the previous demo. The documentation for the VPD pattern format is inline:


;-------------------------------------------------------------------------------
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.l #$40000003,(VDP_CONTROL) ; initial drawing location
 move.l #$6FF,d3     ; how many tiles to draw (1792 total)
DrawTileLoop:
 move.w  d0,(VDP_DATA)    ; copy the pattern to VPD
 dbra    d3,DrawTileLoop    ; loop to next tile

By changing bit 14 of the pattern we can loop through all the palettes.


 ;---------------------------------------------------------------------------
 ; draw the test tile with all four palettes
 ; d0 - tile pattern
 ; d1-d3 - loop control
 ; d4 - VDP drawing location
 ;---------------------------------------------------------------------------
 move.w #$0001,d0  ; store tile pattern in d0
 move.l #$3,d1   ; looping through 4 palettes
 move.l #$40000003,d4 ; initial drawing location
DrawTileLoop:
 move.l #$03,d2   ; drawing 4 rows of tiles
 move.l d4,(VDP_CONTROL); tell VDP contol where to start drawing
DrawTileLoopA:
 move.l   #$59,d3  ; how many tiles to draw - 1
DrawTileLoopB:
 move.w  d0,(VDP_DATA) ; copy the pattern to VPD
 dbra    d3,DrawTileLoopB; loop to next tile
 dbra    d2,DrawTileLoopA; loop to next 
 add.l #$03000000,d4 ; increment the drawing location
 dbra    d1,DrawTileLoop ; loop back to the start

Tile with four palettes

With another modification to the pattern definition we can loop through each of the four planes.

Toggling the layers

That's neat but not really the main objective of this demo so let's move on to...

Controller input

If we want to move a sprite around we probably should start reading controller input. I can't claim authorship over this next snippet, something to this effect appears in every Genesis game that's published its source. This routine reads the input from the joypads and stores it in memory, it also differentiates a button being held vs just being pressed.


ReadJoypads:
 lea CTRL_1_DATA,a0  ; load address to read controller 1 data
 lea MEM_CONTROL_HELD,a1 ; load address to write results of the read
 bsr.s ReadJoypad  ; read first joypad
 ; code to read second joypad, not used for this demo 
 ; addq.w #2,a0  ; switch to second joypad data
 ; bsr.s ReadJoypad  ; read second joypad
 rts
ReadJoypad:
 move.b #0,(a0)  ; read joypad data port
 nop     ; bus synchronization
 nop     ; bus synchronization
 move.b (a0),d1  ; get joypad data - Start/A
 lsl.w #2,d1  ; shift them so they are at the 2 highest bits
 move.b #$40,(a0) ; read joypad data port again
 nop     ; bus synchronization
 nop     ; bus synchronization
 move.b (a0),d0  ; get joypad data - C/B/Dpad
 andi.b #$3F,d0  ; C/B/Dpad in low 6 bits
 andi.b #$C0,d1  ; Start/A in high 2 bits
 or.b d1,d0  ; merge values from both registers
 not.b d0   ; revert bits so '0' means not pressed, and '1' pressed
 move.b (a1),d1  ; get previous joypad state
 eor.b d0,d1  ; toggle off buttons that are being held
 move.b d0,(a1)+ ; 'joypad held' functionality 
 and.b d0,d1
 move.b d1,(a1)+ ; 'joypad pressed' functionality
 rts

In this demo let's read the joypads at every vsync event. I suppose there are reasons why someone would do this in the main loop or elsewhere, I'll probably find out the hard way later.


VSync:
 move.w #$FFFF,(MEM_VSYNCFLAG)  ; flag that vsync is in-progress
 add.w   #$0001,(MEM_VSYNC_COUNTER) ; increment counter 
 bsr.w ReadJoypads     ; read controllers
 ; do other stuff here
VSyncExit:
 move.w #$0000,(MEM_VSYNCFLAG)  ; flag that vsync is complete
 rte

Moving the sprite

Let's create a 16x16 sprite and to do this lets' go with four sets of 8x8 tiles. Sorry but I'm not very artistic and this will definitely be a problem later.


BoxSprite:
 dc.l $00000000
 dc.l $01111110
 dc.l $01000010
 dc.l $01000010
 dc.l $01000010
 dc.l $01000010
 dc.l $01111110
 dc.l $00000000
 dc.l $00000000
 dc.l $02222220
 dc.l $02000020
 dc.l $02000020
 dc.l $02222220
 dc.l $02000020
 dc.l $02222220
 dc.l $00000000
 dc.l $00000000
 dc.l $03333330
 dc.l $03300030
 dc.l $03030030
 dc.l $03003030
 dc.l $03000330
 dc.l $03333330
 dc.l $00000000
 dc.l $00000000
 dc.l $04444440
 dc.l $04000440
 dc.l $04004040
 dc.l $04040040
 dc.l $04400040
 dc.l $04444440
 dc.l $00000000

Next we need to create a sprite definition. This took me a few tries to get right and in the full source code there are even more notes than in this code snippet. This definition, which is later loaded into the sprite table, contains things like starting position & size.


BoxSpriteDefinition:
 ;---------------------------------------------------------------------------
 ; index + 0 = vertical coordinate
 ; ---- --yy yyyy yyyy
 ; 0000 0000 1000 0000 -> 128
 ;---------------------------------------------------------------------------
 dc.w  $0080
 ;---------------------------------------------------------------------------
 ; index + 2 = size
 ; ---- hhvv
 ; 0000 0101 -> width 1 cell, height 1 cell
 ;---------------------------------------------------------------------------
 dc.b  $05
 ;---------------------------------------------------------------------------
 ; index + 3 = link field
 ; -lll llll
 ; 0000 0000
 ;---------------------------------------------------------------------------
 dc.b  $00
 ;---------------------------------------------------------------------------
 ; index + 4 = priority, palette, flip, pattern
 ; pccv hnnn nnnn nnnn
 ; 0010 0000 0000 0010
 ;---------------------------------------------------------------------------
 dc.w  $2002
 ;---------------------------------------------------------------------------
 ; index + 6 = horizontal coordinate
 ; ---- --yy yyyy yyyy
 ; 0000 0000 1000 0000 -> 128
 ;---------------------------------------------------------------------------
 dc.w  $0080

Next we need to load the sprite into the VDP, which looks a lot like loading a tile.


LoadSprite:
 lea     BoxSpriteDefinition,a0  ; store address of sprite definition
 move.w  #$01,d0      ; 1 sprite
 move.l #$60000003,(VDP_CONTROL) ; set write location in VDP
LoadSpriteLoop:
 move.l    (a0)+,(VDP_DATA)
 move.l    (a0)+,(VDP_DATA)
 dbra    d0,LoadSpriteLoop
; sprite initial position 
 move.w #$0080,(MEM_SPRITE_X) ; x=128
 move.w #$0080,(MEM_SPRITE_Y) ; y=128

Now we need to move the sprite based on the controller input. This was started based off another tutorial (see link in code comments) and I added code to (a) test the direction being pressed (2) implemented a step counter so the sprite isn't moving faster than we can see (3) test for collision with the edge of the screen.


;-------------------------------------------------------------------------------
; MoveSprite
; move the sprite around based on the controller input
; d3 - placeholder for controller input so operations aren't against memory
; d4 - used to store the sprite table destination
; d5 - used to store the sprite id
; d6 - used to increment/decrement sprite x,y
; d7 - used to perform various operations
; some of this code is based on the tutuorial at:
;  https://bigevilcorporation.co.uk/2012/04/24/sega-megadrive-6-scary-monsters-and-nice-sprites/
; (although I've since re-written it)
;-------------------------------------------------------------------------------
MoveSprite:
 move.b (MEM_CONTROL_HELD),d3 ; copy value of button held to d3
 move.b d3,d7     ; copy button held value to d7 for eor
 eor.w #$0000,d7    ; test if it is zero
 bne.w TestDirection   ; branch if a direction is being held
NoDirection:
 move.w #$0000,(MEM_SPRITE_STEP_COUNTER) ; nothing pressed, reset counter
 bsr.w ExitMoveSprite      ; exit
TestDirection:
 add.w   #$0001,(MEM_SPRITE_STEP_COUNTER) ; increment counter
 move.w (MEM_SPRITE_STEP_COUNTER),d7 ; copy counter to d7
 cmpi.w #$012C,d7      ; move once every 5 vsyncs
 bls.w ExitMoveSprite     ; exit if it's not time to move
 move.w #$0000,(MEM_SPRITE_STEP_COUNTER); reset the step counter
TestUp:
 move.b d3,d7      ; copy button held value to d7 for andi
 andi.w #BUTTON_UP_PRESSED,d7  ; test if the right button is held
 beq.s TestDown     ; branch if not
MoveUp:
 move.w (MEM_SPRITE_Y),d6   ; move current y position to d6
 cmpi.w #$0080,d6     ; is the sprite at the top edge?
 ble.w TestRight     ; if at the top edge don't move
 sub.w #$0001,d6     ; decrement
 move.w d6,(MEM_SPRITE_Y)   ; save it back
 move.w #$00,d5          ; copy sprite id to d5
 move.w d5,d4      ; copy sprite ID to d4
    mulu.w #$08,d4      ; mult by 8 to get sprite array offset
    swap d4                          ; move to upper word
 add.l #$60000003,d4     ; add to sprite table address
 move.l d4,(VDP_CONTROL)   ; set write location in VDP
 move.w d6,(VDP_DATA)               ; copy the new y-coordinate
 beq.s TestRight     ; up & down can't be pressed together
TestDown:
 move.b d3,d7      ; copy button held value to d7 for andi
 andi.w #BUTTON_DOWN_PRESSED,d7  ; test if the right button is held
 beq.s TestRight     ; branch if not
MoveDown:
 move.w (MEM_SPRITE_Y),d6   ; move current y position to d6
 cmpi.w #$0150,d6     ; is the sprite at the bottom edge?
 bge.w TestRight     ; if at the bottom edge don't move
 add.w #$0001,d6     ; increment
 move.w d6,(MEM_SPRITE_Y)   ; save it back
 move.w #$00,d5          ; copy sprite id to d5
 move.w d5,d4      ; copy sprite ID to d4
    mulu.w #$08,d4      ; mult by 8 to get sprite array offset
    swap d4                          ; move to upper word
 add.l #$60000003,d4     ; add to sprite table address
 move.l d4,(VDP_CONTROL)   ; set write location in VDP
 move.w d6,(VDP_DATA)               ; copy the new y-coordinate
TestRight:
 move.b d3,d7      ; copy button held value to d7 for andi
 andi.w #BUTTON_RIGHT_PRESSED,d7 ; test if the right button is held
 beq.s TestLeft     ; branch if not
MoveRight:
 move.w (MEM_SPRITE_X),d6   ; move current x position to d6
 cmpi.w #$01B0,d6     ; is the sprite at the right edge?
 bge.w ExitMoveSprite    ; if at the right edge don't move
 add.w #$0001,d6     ; increment
 move.w d6,(MEM_SPRITE_X)   ; save it back
 move.w #$00,d5          ; copy sprite id to d5
 move.w d5,d4      ; copy sprite ID to d4
    mulu.w #$08,d4      ; mult by 8 to get sprite array offset
    add.b #$06,d4                     ; x-coordinate is at index 6
    swap d4                          ; move to upper word
 add.l #$60000003,d4     ; add to sprite table address
 move.l d4,(VDP_CONTROL)   ; set write location in VDP
 move.w d6,(VDP_DATA)               ; copy the new x-coordinate
 beq.s TestRight     ; up & down can't be pressed together
TestLeft:
 move.b d3,d7      ; copy button held value to d7 for andi
 andi.w #BUTTON_LEFT_PRESSED,d7  ; test if the right button is held
 beq.s ExitMoveSprite    ; branch if not
MoveLeft:
 move.w (MEM_SPRITE_X),d6   ; move current x position to d6
 cmpi.w #$0080,d6     ; is the sprite at the left edge?
 ble.w ExitMoveSprite    ; if at the left edge don't move
 sub.w #$0001,d6     ; decrement
 move.w d6,(MEM_SPRITE_X)   ; save it back
 move.w #$00,d5          ; copy sprite id to d5
 move.w d5,d4      ; copy sprite ID to d4
    mulu.w #$08,d4      ; mult by 8 to get sprite array offset
    add.b #$06,d4                     ; x-coordinate is at index 6
    swap d4                          ; move to upper word
 add.l #$60000003,d4     ; add to sprite table address
 move.l d4,(VDP_CONTROL)   ; set write location in VDP
 move.w d6,(VDP_DATA)               ; copy the new x-coordinate
ExitMoveSprite:
 rts

Here's a picture of the not artistic sprite against a wintry background.

Moving sprite

Main game loop

Last up, let's create a main game loop that waits for vsync to complete then moves the sprite around.

Check it out, there's some duplicate code I need to clean-up. Well, let's just pretend I left it in on purpose to illustrate an "example" of how to check the controller input in the main loop instead.


MainGameLoop:
 bsr.w WaitVSync
 add.w   #$0001,(MEM_DEBUG_MAINLOOP_COUNTER) ;debug code
 move.b (MEM_CONTROL_HELD),d7 ; copy value of button held to d7
 eor.w #$0000,d7  ; test if it is zero
 beq.s MainGameLoopEnd ; branch if no directional button is being held
 bsr.w MoveSprite
MainGameLoopEnd:
 bra.s MainGameLoop 
WaitVSync:
 move.w (MEM_VSYNCFLAG),d0 ; check if the vsync flag is set
 ; do other stuff here
 dbra d0,WaitVSync;  ; wait for vsync to complete
 rts

Now that I see this in web form, that dbra d0,WaitVSync is probably not the most efficient way to check if the vsync flag is zero. I guess in the next example I write that will be the first thing to clean-up.

Download

Download the latest source code on GitHub




Related