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




Related