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.
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.
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. 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:
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:
Now our little sprite can look around at stuff, the brute force text lookup even works too:
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