Sega Genesis Programming Part 11: Scenes and Dialogs



It all started with a cash register

This is probably the longest Genesis programming article I've written and it's all because I wanted to add a simple piece of scenery. I decided the store looked a little bare and adding a cash register to the counter would spruce things up. I designed a hideously ugly register and started on the code to add it.

The register

There's the register now, mocking me for all the work it caused. Sure it looks like it's mocking the player sprite but really it's me on the receiving end. And not because the player sprite is meant to be an avatar of me, it's not, I'm much taller.

In creating the register it occurred to me that I need to stop hard-coding all the scenery. At some point, maybe even soon, our little sprite will be able to leave the store and wander around a semi-authentic 1989 shopping mall. To do that we need some kind of general way to store & draw locations in the mall. I opted for the term "scene" but "location", "place", and "who cares already" would be fine too.

This led me down a wormhole of coding & refactoring...

Scene definition

Scenes are a data structure that represent everything needed to draw a location. This includes:

NPCs are not in this definition because I haven't decided how to handle moving them between stores. Any NPCs in a scene definition will always be loaded there. An alternate approach is applying rules based on the game state to determine which NPCs are in which location. I think I prefer the second option even though it's more work. An argument could also be made that objects and background music could change based on game state. I'm not even close to thinking about that yet.

You might notice the scene is called "SceneVB". This is because I'm tentatively calling the first store "Video Buffet". It doesn't appear there's ever been a real store called that, for good reason I suppose.

Here's the full definition for our first store:

SceneVB:

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

 ; tiles

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

 dc.w $0000 ; one tileset

 ; tileset 0

 dc.l StoreTilesStart ; start of scene tiles

 dc.l StoreTilesEnd ; end of scene tiles

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

 ; palettes

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

 dc.w $0004 ; four palettes

 dc.l PaletteStoreA ; start of first palette to load

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

 ; scenery

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

 dc.w $000A ; scenery count

 ; floor

 dc.l PatternFloorStart ; location of pattern to load

 ; pccvhnnnnnnnnnnn

 ; 0000000000000001 

 dc.w $0001 ; base pattern

 dc.w $001F ; repeat 32 times

 dc.l VDP_VRAM_WRITE_B ; initial drawing location

 ; top shelf

 dc.l PatternShelvesHStart ; location of pattern to load

 ; pccvhnnnnnnnnnnn

 ; 0000000000000101 

 dc.w $0005 ; base pattern

 dc.w $0000 ; no repeat

 ; row 1, column 0 = 128 = 0080

 dc.l VDP_VRAM_WRITE_B+$00800000 ; initial drawing location

 ; middle shelf

 dc.l PatternShelvesMStart ; location of pattern to load

 ; pccvhnnnnnnnnnnn

 ; 0010000000000101 

 dc.w $2005 ; base pattern

 dc.w $0000 ; no repeat

 ; row 2, column 0 = 256 = 0100

 dc.l VDP_VRAM_WRITE_B+$01000000 ; initial drawing location

 ; low shelf

 dc.l PatternShelvesLStart ; location of pattern to load

 ; pccvhnnnnnnnnnnn

 ; 0100000000000101 

 dc.w $4005 ; base pattern

 dc.w $0000 ; no repeat

 ; row 3, column 0 = 256 = 0180

 dc.l VDP_VRAM_WRITE_B+$01800000 ; initial drawing location

 ; frame top

 dc.l PatternFrameTopStart ; location of pattern to load

 ; pccvhnnnnnnnnnnn

 ; 1000000000000000 

 dc.w $8000 ; base pattern

 dc.w $0000 ; no repeat

 ; row 3, column 0 = 256 = 0180

 dc.l VDP_VRAM_WRITE_A ; initial drawing location

 ; frame side

 dc.l PatternFrameSideStart ; location of pattern to load

 ; pccvhnnnnnnnnnnn

 ; 1000000000000000 

 dc.w $8000 ; base pattern

 dc.w $0016 ; repeat 23 times

 ; row 1, column 0 = 128 = 0080

 dc.l VDP_VRAM_WRITE_A+$00800000 ; initial drawing location

 ; store front

 dc.l PatternStoreFrontStart ; location of first pattern to load

 ; pccvhnnnnnnnnnnn

 ; 1000000000001100 

 dc.w $800C ; base pattern

 dc.w $0000 ; no repeat

 ; row 24, column 0 = 3072 = C00

 dc.l VDP_VRAM_WRITE_A+$0C000000 ; initial drawing location

 ; counter - low front

 dc.l PatternCounterLowStart ; location of first pattern to load

 ; pccvhnnnnnnnnnnn

 ; 0010000000100001 

 dc.w $2021 ; base pattern

 dc.w $0000 ; no repeat

 ; row 10, column 7 = 51C

 dc.l VDP_VRAM_WRITE_A+$051C0000 ; initial drawing location

 ; counter - low side

 dc.l PatternCounterLowSideStart ; location of first pattern to load

 ; pccvhnnnnnnnnnnn

 ; 0010000000110001 

 dc.w $2031 ; base pattern

 dc.w $0000 ; no repeat

 ; row 3, column 12 = 432 = 1B0

 dc.l VDP_VRAM_WRITE_B+$01B00000 ; initial drawing location

 ; counter - top

 dc.l PatternCounterHighStart ; location of first pattern to load

 ; pccvhnnnnnnnnnnn

 ; 1010000000110001 

 dc.w $A031 ; base pattern

 dc.w $0000 ; no repeat

 ; row 7, column 28 = 39C

 dc.l VDP_VRAM_WRITE_A+$039C0000 ; initial drawing location

 ; register

 dc.l PatternRegisterStart ; location of first pattern to load

 ; pccvhnnnnnnnnnnn

 ; 1110000000111011 

 dc.w $E03B ; base pattern

 dc.w $0000 ; no repeat

 ; row 6, column 44 = 330

 dc.l VDP_VRAM_WRITE_A+$03300000 ; initial drawing location

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

 ; objects

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

 dc.w OBJ_LIST_LENGTH-1 ; object count

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

 ; word0=Object ID (0-65535)

 ; word1[0-8]=x0 (0-511)

 ; word1[9-15]=width (0-127)

 ; word2[0-8]=y0 (0-512)

 ; word2[9-15]=height (0-127)

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

 dc.w OBJ_SCENE_VB_8BIT

 ; x0=136 width=106 = 1101010 010001000 = D488

 dc.w $D488

 ; y0=136 height=26 = 0011010 010001000 = 3488

 dc.w $3488

 dc.w OBJ_SCENE_VB_8BIT

 ; x0=242 width=79 = 1001111 011110010 = 9EF2

 dc.w $9EF2

 ; y0=136 height=26 = 0011010 010001000 = 3488

 dc.w $3488

 dc.w OBJ_SCENE_VB_8BIT

 ; x0=351 width=90 = 1011010 101011111 = B55F

 dc.w $B55F

 ; y0=136 height=26 = 0011010 010001000 = 3488

 dc.w $3488

 dc.w OBJ_SCENE_VB_MAGS

 ; x0=240 width=114 = 1110010 011110000 = E4F0

 dc.w $E4F0

 ; y0=224 height=32 = 0100000 011100000 = 40E0

 dc.w $40E0

 dc.w OBJ_SCENE_VB_COUNTER

 ; x0=240 width=80 = 1010000 011110000 = A0F0

 dc.w $A0F0

 ; y0=200 height=16 = 0010000 011001000 = 20C8

 dc.w $20C8

 dc.w OBJ_SCENE_VB_REGISTER

 ; x0=320 width=16 = 0010000 101000000 = 2140

 dc.w $2140

 ; y0=162 height=32 = 0100000 010100010 = 40A2

 dc.w $40A2

 dc.w OBJ_NOTHING

 dc.w $0000

 dc.w $0000

 dc.w OBJ_NOTHING

 dc.w $0000

 dc.w $0000

 dc.w OBJ_NOTHING

 dc.w $0000

 dc.w $0000

 dc.w OBJ_NOTHING

 dc.w $0000

 dc.w $0000

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

 ; collision data

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

 dc.w DEFAULT_COLLISION_DATA_SIZE ; collision data size

 dc.l MapStoreCollision ; location of collision data

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

 ; bgm

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

 dc.l BGM_Test ; location of background music

Loading the scene

With the definition out of the way we need a routine to load it. First off there's a little bit of planning required to determine where in memory things should be loaded. It's been ~20 years since I've done any programming that required manual memory management. It's not really as annoying as it sounds, maybe I'll regret this statement in some future article. For the moment let's reserve some blocks in the VDP memory for scene tiles, font tiles, and sprite tiles:

SCENE_VDP=VDP_VRAM_WRITE   ; write location for scene tiles

FONT_VDP=SCENE_VDP+$12000000  ; write location for font tiles

SPRITE_VDP=SCENE_VDP+$20000000  ; write location for sprite tiles

The routine to load the scene largely pulls together code that was scattered throughout various places into one location:

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

; LoadScene

; Parameters

; a6 = starting address of scene to load

; other registers used

; a0 & a1 are used to call other subroutines

; assume that d0-d7 are used either by this subroutine or others it calls

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

LoadScene:

 move.w #$2700,sr  ; disable interrupts

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

; load tiles

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

 move.w (a6)+,d7 ; number of tilesets to load

 move.l #SCENE_VDP,d6 ; use d6 to track write location

LoadSceneLoadTilesLoop:

 move.l (a6)+,d1 ; start address of tileset

 move.l (a6)+,d0 ; end address of tileset

 sub.l d1,d0 ; subtract the start address to get length

 move.l d0,d2 ; copy original value to increment write location later

 divu.w #$0004,d0 ; divide by 4 to setup call to LoadTiles

 movea.l d1,a0 ; set address of first tile to load

 move.l d6,d1 ; set initial write location

 bsr.w LoadTiles ; branch to LoadTiles subroutine

 ; increment write location for next tileset

 swap d2 ; d2 has the size of the last tileset, swap it to upper word

 add.l d2,d6 ; add to d6 to increment write location

 dbra d7,LoadSceneLoadTilesLoop ; loop until all data is loaded

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

; load palettes

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

 ; setup call to LoadPalettes

 move.w (a6)+,d0 ; number of palettes to load

 movea.l (a6)+,a0 ; start address of palettes

 move.l #VDP_CRAM_WRITE,d1 ; initial write address

 bsr.w LoadPalettes ; branch to LoadPalettes subroutine

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

; draw the scenery

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

 move.w (a6)+,d7 ; number of tilesets to load

LoadSceneDrawSceneryLoop:

 movea.l (a6)+,a0 ; start address of pattern

 move.w (a6)+,d0 ; base pattern

 move.w (a6)+,d1 ; repeat

 movea.l (a6)+,a1 ; initial drawing location

 bsr.w DrawTileset  ; branch to DrawTileset subroutine

 dbra d7,LoadSceneDrawSceneryLoop ; loop until all data is loaded

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

; load objects

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

 move.w (a6)+,d7 ; number of objects to load

 lea MEM_OBJECT_LIST_OBJS,a0 ; address of object data

LoadSceneLoadObjectsLoop:

 move.w (a6)+,(a0)+ ; word0 (object ID)

 move.w (a6)+,(a0)+ ; word1 (x+width)

 move.w (a6)+,(a0)+ ; word2 (y+height)

 dbra d7,LoadSceneLoadObjectsLoop ; loop until all data is loaded

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

; load collision data

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

 move.w (a6)+,d7 ; size of collision data

 movea.l (a6)+,a0 ; start address of collision data

 lea MEM_COLLISION_DATA,a1 ; store destination memory location

LoadSceneLoadMapCollisionLoop:

 move.l (a0)+,(a1)+

 dbra d7,LoadSceneLoadMapCollisionLoop

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

; setup map

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

 move.w #INIT_MAP_POSITION_X,(MEM_MAP_POSITION_X) ; set map x-position

 move.w #INIT_MAP_POSITION_Y,(MEM_MAP_POSITION_Y) ; set map y-position

 move.w #$FFFF,(MEM_FLAG_MAP_POSITION_CHANGED) ; flag to reset the scroll

 bsr.w SetMapScroll ; set the map position

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

; load & start the background music

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

 movea.l (a6)+,a0 ; address of the BGM

 bsr Echo_PlayBGM

 move.w #$2000,sr  ; re-enable interrupts

 rts

You may have noticed that patterns now have a "repeat" option. This was originally added to support drawing the floor but was also handy for drawing the side frame. This means that DrawTileset needed a little refactoring:

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

; DrawTileset

; draws a set of tiles

; Parameters

; a0 = starting address of tileset

; a1 = initial VRAM write address

; d0 = base pattern (ID+palette+high/low) of first tile in the tileset

; d1 = number of times to repeat the pattern

; other registers used

; a2 = used to reference current VRAM write address

; d2 = number of rows in the tileset

; d3 = number of columns in the tileset

; d4 = used to store the VDP pattern

; d5 = loop counter

; d6 = loop counter

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

DrawTileset:

 move.w (a0)+,d2 ; store the number of rows

 move.w (a0)+,d3 ; store the number of columns

DrawTilesetOuterLoop:

 movea.w a0,a2 ; reset a2 to starting address

 move.w d2,d5 ; reset d5 to store the number of rows to loop over

DrawTilesetRowLoop:

 move.w d3,d6 ; reset d6 to store the number of columns to loop over

 move.l a1,(VDP_CONTROL) ; set VDP address

DrawTilesetColumnLoop:

 move.w (a2)+,d4 ; store the next tile index in d4

 add.w d0,d4 ; add base tile ID to tile index

 ; draw the tile

 move.w d4,(VDP_DATA) ; copy the pattern to VPD

 dbf d6,DrawTilesetColumnLoop ; decrement value of d6 (column) and loop

 adda.l #ROW_HEIGHT,a1 ; move to the next row

 dbf d5,DrawTilesetRowLoop ; decrement value of d5 (row) and loop

 dbf d1,DrawTilesetOuterLoop ; decrement value of d1 (repeat) and loop

 rts

Loading a new scene in the main game code is now a really short bit-o-work:

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

; load the initial scene

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

 lea SceneVB,a6 ; address of the initial scene

 bsr.w LoadScene ; branch to LoadScene subroutine

Despite all this new code we're really just in the exact place we left off only with an ugly cash register. Let's add something new...

Dialog open/close animation

Our little sprite needs some ability to interact with their environment. We stubbed out the basic mechanics of pressing the A button to look at stuff in part 9 so now we need a dialog to display some text.

We're going to build an animated dialog that expands and collapses. This is pretty much standard fare in most RPGs. Let's start with a few new constants:

DIALOG_PATTERN_SIZE=$00C4 ; size of the dialog pattern

DIALOG_BASE_TILE=$8090 ; base file for dialogs

DIALOG_ROWCOL=$09900000 ; row 19 column 16=(128*19)+16=2448=990

DIALOG_FRAME_COUNT=$000B ; number of animation frames for dialogs

DIALOG_UPDATE_FREQUENCY=$0002 ; how often to update dialog animation

LF=$0A ; '\n' - used to break text into multiple lines

ETX=$03 ; end of text

These constants define the size of the dialog, the base tile in the dialog tileset, where to draw the dialog, how many frames of animation in the expand/collapse, and how often to update the animation.

This is probably not optimal but I decided to create a pattern for each dialog animation frame:

PatternDialogFrame0:

 dc.w $0003 ; 4 rows

 dc.w $0017 ; 24 columns

 ; row 00

 dc.w $5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F

 dc.w $60,$64,$64,$66

 dc.w $5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F

 ; row 01

 dc.w $5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F

 dc.w $62,$00,$00,$63

 dc.w $5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F

 ; row 02

 dc.w $5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F

 dc.w $62,$00,$00,$63

 dc.w $5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F

 ; row 03

 dc.w $5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F

 dc.w $61,$65,$65,$67

 dc.w $5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F,$5F

[...]

PatternDialogFrameA:

 dc.w $0003 ; 4 rows

 dc.w $0017 ; 24 columns

 ; row 00

 dc.w $60,$64,$64,$64,$64,$64,$64,$64,$64,$64,$64,$64

 dc.w $64,$64,$64,$64,$64,$64,$64,$64,$64,$64,$64,$66

 ; row 01

 dc.w $62,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00

 dc.w $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$63

 ; row 02

 dc.w $62,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00

 dc.w $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$63

 ; row 03

 dc.w $61,$65,$65,$65,$65,$65,$65,$65,$65,$65,$65,$65

 dc.w $65,$65,$65,$65,$65,$65,$65,$65,$65,$65,$65,$67

In the main loop we need to make a small adjustment to see if a dialog is either drawing or open. If it's drawing we need to wait it out, if it's not drawing we need to see if it's open to then figure out if a button was pressed to close it.

MainGameLoop:

[...]

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

; determine if the player is opening or interacting with a dialog

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

TestDialog:

 move.l (MEM_GAME_STATE),d7 ; copy current game state to d7

 btst.l #STATE_FLAG_DIALOG,d7 ; test game state

 beq.w TestExploring ; dialog not displaying, move to next test

TestDialogUpdateFrequency:

 cmpi.w #DIALOG_UPDATE_FREQUENCY,(MEM_FRAME_COUNTER); is it time to update?

 blt.s TestDialogOpen ; branch if it's not time to move

 move.w #$0000,(MEM_FRAME_COUNTER) ; reset counter to 0

 bsr.w ProcessDialog ; dialog is set, jump to process dialog sub-routine

 bra.w MainGameLoop ; return to start of game loop

TestDialogOpen:

 ; if the dialog is open then update frequency is ignored

 move.l (MEM_DIALOG_FLAGS),d7 ; copy current game state to d7

 btst.l #DIALOG_FLAG_TEXT_OPEN,d7 ; test if the dialog is open

 beq.w MainGameLoop ; dialog is not open, move to next test

 ; else branch to process dialog

 bsr.w ProcessDialog ; dialog is set, jump to process dialog sub-routine

 bra.w MainGameLoop ; return to start of game loop

We now need to update the ProcessDialog routine written back in part 9 to draw the dialog pattern based on the animation frame:

ProcessDialog:

 move.w #$2700,sr ; disable interrupts while managing dialogs

 move.l (MEM_DIALOG_FLAGS),d7 ; copy current dialog state to d7

ProcessDialogTestTextBuilt:

 btst.l #DIALOG_FLAG_TEXT_BUILT,d7 ; test game state

 bne.s ProcessDialogTestOpening ; text is built, move to next test

[...]

ProcessDialogTestOpening:

 btst.l #DIALOG_FLAG_TEXT_OPENING,d7 ; test if the dialog is opening

 beq.s ProcessDialogTextDrawing ; dialog is not opening, go to next test

 ; dialog opening animation

 move.w d7,d6 ; copy low word with frame number

 mulu.w #DIALOG_PATTERN_SIZE,d6 ; multiply by size of dialog patterns

 movea.l #PatternDialogStart,a0 ; point a0 to start of dialog patterns

 adda.l d6,a0 ; increment to current frame

 move.w #DIALOG_BASE_TILE,d0 ; base pattern

 move.w #$0000,d1 ; repeat

 movea.l #(VDP_VRAM_WRITE_A+DIALOG_ROWCOL),a1 ; initial drawing location

 bsr.w DrawTileset  ; branch to DrawTileset subroutine

 addq #$1,d7 ; increment frame number

 cmpi.w #DIALOG_FRAME_COUNT,d7 ; are we at the last frame?

 ble.w ExitProcessDialog ; not at the last frame, exit

 bclr.l #DIALOG_FLAG_TEXT_OPENING,d7 ; done opening, clear flag

 bset.l #DIALOG_FLAG_TEXT_DRAWING,d7 ; change state to text drawing

 ; reset the drawing location for the dialog text

 move.l #(VDP_VRAM_WRITE_A+DIALOG_ROWCOL),(MEM_DIALOG_VPD) ; base address

 add.l #$00820000,(MEM_DIALOG_VPD) ; add 132 to move 1 row and column

 bra.w ExitProcessDialog ; exit

ProcessDialogTextDrawing:

[...covered in the next section...]

ProcessDialogTestOpen:

 btst.l #DIALOG_FLAG_TEXT_OPEN,d7 ; test if the dialog is open

 beq.s ProcessDialogTestClosing ; dialog is not open, move to next test

 ; wait until a button is pressed to clear the dialog

 move.b (MEM_CONTROL_PRESSED),d6 ; copy pressed buttons to d6

 cmpi.w #$0000,d6 ; are any buttons pressed?

 beq.s ExitProcessDialog ; no buttons are pressed, exit

 bset.l #DIALOG_FLAG_TEXT_CLOSING,d7 ; set closing flag

 bclr.l #DIALOG_FLAG_TEXT_OPEN,d7 ; closing, clear open flag

ProcessDialogTestClosing:

 btst.l #DIALOG_FLAG_TEXT_CLOSING,d7 ; test if the dialog is opening

 beq.s ProcessDialogClearFlags ; dialog is not closing, exit

 ; dialog closing animation

 move.w d7,d6 ; copy low word with frame number

 subq #$1,d6 ; decrement frame number

 mulu.w #DIALOG_PATTERN_SIZE,d6 ; multiply by size of dialog patterns

 movea.l #PatternDialogClear,a0 ; point a0 to start of dialog patterns

 adda.l d6,a0 ; decrement to current frame

 move.w #DIALOG_BASE_TILE,d0 ; base pattern

 move.w #$0000,d1 ; repeat

 movea.l #(VDP_VRAM_WRITE_A+DIALOG_ROWCOL),a1 ; initial drawing location

 bsr.w DrawTileset  ; branch to DrawTileset subroutine

 subq #$1,d7 ; decrement frame number

 cmpi.w #$0000,d7 ; are we at the last frame?

 bgt.s ExitProcessDialog ; not at the last frame, exit

ProcessDialogClearFlags: ; clear flags when done

 andi.l #$00000000,d7 ; clear all dialog flags

 ; clear dialog bit on game state

 move.l (MEM_GAME_STATE),d6 ; copy current game state to d6

 bclr.l #STATE_FLAG_DIALOG,d6 ; clear the dialog bit

 move.l d6,(MEM_GAME_STATE) ; copy it back

ExitProcessDialog:

 move.l d7,(MEM_DIALOG_FLAGS) ; save any changes made to the game state

 move.w #$2000,sr  ; re-enable interrupts

 rts

So after all this work we just have an empty dialog with a decent-looking fade-in/fade-out transition.

Empty dialog

I thought about taking a break and posting an article after getting this little bit working. Instead I fought my laziness and implemented text as well.

Drawing text

Before we can draw text we'll need a font. I played around with a couple original font ideas before I remembered that I had a perfectly nice looking font already created for a Phantasy Star III ROM hack. The only consistently positive feedback I receive on my PSIII ROM hacks is for the font. Too bad it's not mine.

The font I'm using is the Deathwake font from here - http://www.zee-3.com/pickfordbros/archive/bitmapfonts.php. Their site says "Feel free to use any of these fonts for whatever purpose you see fit." so I will. It has a really nice style to it and amazingly only uses a 6x6 grid for each character:

Font

The 6x6 part is important because with 8x8 tiles it means I don't have to do any work on spacing. The characters are all aligned bottom-right creating a 1px pad on the left and top edges.

There are some characters included that likely won't ever be used like { } or `. I'm keeping those around as placeholders in case I need symbols in the dialogs at some later point. Like if I feel like putting a character that looks like a musical note in some text I'd overwrite { or whatever.

Now we need to define some dialog text. Since I'm not very creative, the text is a really bland:

DialogTextNothing:

 dc.b "There's nothing very"

 dc.b LF

 dc.b "interesting here."

 dc.b ETX

 align 2

DialogText8Bit:

 dc.b "The shelves are lined"

 dc.b LF

 dc.b "with 8-bit games."

 dc.b ETX

 align 2

DialogTextMags:

 dc.b "Below the counter are"

 dc.b LF

 dc.b "gaming magazines."

 dc.b ETX

 align 2

DialogTextCounter:

 dc.b "The counter is empty."

 dc.b ETX

 align 2

DialogTextRegister:

 dc.b "The register stares"

 dc.b LF

 dc.b "back at you blankly."

 dc.b ETX

 align 2

DialogTextDani:

 dc.b "Dani: 'Isn't it time"

 dc.b LF

 dc.b "to close already?'"

 dc.b ETX

 align 2

Now we need a quick 'n dirty method to figure out what text to draw based on what the player is looking at. This is nowhere near elegant nor something I plan to build on, it's just a giant switch statement:

; this demo iteration is just doing a brute force lookup by object id

; future versions of this demo should add rules based on the game state

BuildDialogText: ; note - d7 is used by the caller

 move.w (MEM_ACTION_TARGET_OBJID),d6 ; copy action target to d6

 cmpi.w #OBJ_NOTHING,d6

 bne.s .1

 lea DialogTextNothing,a0

 bra.s ExitBuildDialogText

.1

 cmpi.w #OBJ_SCENE_VB_8BIT,d6

 bne.s .2

 lea DialogText8Bit,a0

 bra.s ExitBuildDialogText

.2

 cmpi.w #OBJ_SCENE_VB_MAGS,d6

 bne.s .3

 lea DialogTextMags,a0

 bra.s ExitBuildDialogText

.3

 cmpi.w #OBJ_SCENE_VB_COUNTER,d6

 bne.s .4

 lea DialogTextCounter,a0

 bra.s ExitBuildDialogText

.4

 cmpi.w #OBJ_SCENE_VB_REGISTER,d6

 bne.s .5

 lea DialogTextRegister,a0

 bra.s ExitBuildDialogText 

.5

 cmpi.w #OBJ_NPC_DANI,d6

 bne.s .6

 lea DialogTextDani,a0

 bra.s ExitBuildDialogText 

.6

 lea DialogTextNothing,a0

ExitBuildDialogText:

 move.l a0,MEM_DIALOG_TEXT ; copy address to MEM_DIALOG_TEXT

 rts

Drawing text is just like drawing any other pattern except we need to subtract 32 (20 hex) from the ASCII values to get the tile ID. Alternatively, the base tile in the pattern could be -32 but somehow that seems more complicated:

ProcessDialog:

[...]

ProcessDialogTextDrawing:

 btst.l #DIALOG_FLAG_TEXT_DRAWING,d7 ; test if the dialog is opening

 beq.s ProcessDialogTestOpen ; text is not drawing, go to next test

 move.l (MEM_DIALOG_TEXT),a0 ; point a0 to the current character

 move.b (a0),d6 ; copy current character to d6

 cmpi.b #ETX,d6 ; is this the end of the text?

 beq.s ProcessDialogTextEnd ; end of text

 cmpi.b #LF,d6 ; is this a new line character

 bne.s .1 ; not a new character

 move.l #(VDP_VRAM_WRITE_A+DIALOG_ROWCOL),(MEM_DIALOG_VPD) ; base address

 add.l #$01020000,(MEM_DIALOG_VPD) ; add 258 to move 2 rows and column

 bra.s .2 ; go to increment text step

.1

 ; update d6 to point to the tile ID

 sub.w #$20,d6 ; subtract 32 to get the character index

 add.w #DIALOG_BASE_TILE,d6 ; add the base tile

 move.l (MEM_DIALOG_VPD),(VDP_CONTROL) ; set VDP address

 move.w d6,(VDP_DATA) ; copy the character to VPD

 ; draw the next character

 add.l #$00020000,(MEM_DIALOG_VPD) ; move to the next column

.2

 add.l #$0001,(MEM_DIALOG_TEXT) ; move to the next character

 bra.w ExitProcessDialog

ProcessDialogTextEnd:

 bclr.l #DIALOG_FLAG_TEXT_DRAWING,d7 ; done drawing, clear flag

 bset.l #DIALOG_FLAG_TEXT_OPEN,d7 ; change state to open when done

ProcessDialogTestOpen:

[...]

We now have an animated dialog that draws text:

Dialog with text

Now our little sprite can look around at stuff, the brute force text lookup even works too:

Dialog looking at nothing
Dialog looking at the counter
Dialog looking at games

Dialog looking at the register
Dialog looking at the magazine rack
Dialog looking at an NPC

I know this doesn't seem terribly exciting but I'm very happy to get this working. I'm not too far from having the basic plumbing in place to allow a player to freely explore the entire mall. I suppose scene transitions are going to be the next heap of work to figure out.

New problems & other stuff that needs fixin'

Download

Download the latest source code on GitHub




Tweet