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
With another modification to the pattern definition we can loop through each of the four planes.
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:
; [archived link]
; (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.
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