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

;  http://bigevilcorporation.co.uk/2012/04/24/sega-megadrive-6-scary-monsters-and-nice-sprites/

;-------------------------------------------------------------------------------

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




Tweet