Sega Genesis Programming Part 13: Action Table


 

Multi-page dialogs

Let's skip the commentary and get right to work this time. One small change we need to make is enabling multi-page dialogs. This should have been done back when dialogs were first created but whatever. Let's start by adding a new dialog state flag and a new constant character:

DIALOG_FLAG_TEXT_BUILT=$1A ; set when text to display is built

DIALOG_FLAG_TEXT_OPENING=$1B ; dialog is opening

DIALOG_FLAG_TEXT_DRAWING=$1C ; dialog text is drawing

DIALOG_FLAG_TEXT_OPEN=$1D ; dialog is open

DIALOG_FLAG_TEXT_NEW_PAGE=$1E ; dialog text has another page

DIALOG_FLAG_TEXT_CLOSING=$1F ; dialog is closing

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

FF=$0C ; form feed - used to break text into pages

ETX=$03 ; end of text

Let's re-purpose the ^ character to act as a visual indicator there are more pages. We also need some dialog that goes on for more than one page:

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

dc.b LF

dc.b "to close already? ^"

dc.b FF

dc.b "Get that guy out of"

dc.b LF

dc.b "here so we can leave.'"

dc.b ETX

align 2

Now the code to process dialogs needs to look for the FF character and draw the next page of text when the player hits a button:

[...]

ProcessDialogTextDrawing:

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

 beq.w 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 #FF,d6 ; is there another page of text?

 bne.s .1 ; not the new page character, continue to next test

 bset.l #DIALOG_FLAG_TEXT_NEW_PAGE,d7 ; flag there is another page

 bra.s ProcessDialogTextEnd

.1

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

 bne.s .2 ; 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 .3 ; go to increment text step

.2

 ; 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

.3

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

 bra.w ExitProcessDialog

ProcessDialogTextEnd:

[...]

ProcessDialogTestOpen:

 ; 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.w ExitProcessDialog ; no buttons are pressed, exit

 ; start button shouldn't close the dialog

 andi.w #BUTTON_START_PRESSED,d6 ; test if the start button is held

 bne.w ExitProcessDialog ; exit if start button is held

 btst.l #DIALOG_FLAG_TEXT_NEW_PAGE,d7 ; test if there is another page

 beq.s .4 ; branch if new text page is not set

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

 ; moving to a new page of text

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

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

 ; 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

 ; clear out the dialog

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

 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

 ; reset flags to force text to start re-drawing

 bset.l #DIALOG_FLAG_TEXT_DRAWING,d7 ; set drawing flag

 bclr.l #DIALOG_FLAG_TEXT_OPEN,d7 ; clear open flag

 bclr.l #DIALOG_FLAG_TEXT_NEW_PAGE,d7 ; clear new page flag

 bra.s ExitProcessDialog ; exit

.4

 bset.l #DIALOG_FLAG_TEXT_CLOSING,d7 ; set closing flag

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

ProcessDialogTestClosing:

[...]

Now we have a page of text with a little upside-down triangle thingy:

Dialog page 1

After pressing a button the next page displays. In theory this could support an endless number of pages. With limited screen real estate for text it just might have to.

Dialog page 2

That wasn't a whole lot of work so let's move on to something more painful.

Action table

For this to resemble a real demo we need a system to process actions. Something that figures out "if the player does [X] with thing [Y] then do [Z]". This can't be too complex because there are memory and CPU limits to consider. So I'm going to start by trying a simple table approach.

I don't have a full story for this game idea figured out yet. I'm trying to not get too far ahead of myself. I have a rough outline though and already know it will take place over a number of days. In each day the player will have access to stores (scenes) in the mall. The events that happen will be based on the day of the story.

What I built to manage this is a table that maps a day/scene/action combination to the address of the code to handle it. I could have gone with just day+scene but (a) there was enough room to include the action in the lookup formula and (b) the first thing that would happen in the day/scene code was checking the action anyway.

The action table will have a base address like $B0000 or $F0000. It doesn't matter so long as it's greater than a word and less than the maximum size of a Genesis cartridge. The table lookup values are then constrained to being a word in size. That creates a limit of 16,384 possible day/scene/action combinations. That won't be a problem because I'm having a hard enough time writing up a plot that has more than 10.

The table search order is [DAY]->[SCENE]->[ACTION]. That makes the formula: (Day#*SceneCount*ActionCount*4)+(Scene#*ActionCount*4)+(Action#*4).The *4 part is because the table entries are addresses to code which are long values.

Here's a picture that might make the design easier to digest:

Action table

Hopefully this helps explain it a little. I barely understand how this works myself, it mostly came to me in a vision. The code to implement the table and lookup values goes like:

ROM_ADDR_ACTION_TABLE=$B0000

[...]

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

; action table

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

 org ROM_ADDR_ACTION_TABLE

ActionTable:

 dc.l Day00Scene00Action00

[...]

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

; compute offset for action table lookups

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

BuildActionTableOffset:

 move.w MEM_DAY,d7

 mulu.w #SCENE_COUNT_X_ACTION_COUNT_X4,d7

 move.w d7,(MEM_ACTION_TABLE_OFFSET)

 move.w MEM_ACTIVE_SCENE_ID,d7

 mulu.w #ACTION_COUNT_X4,d7

 add.w d7,(MEM_ACTION_TABLE_OFFSET)

 move.w MEM_ACTION_ID,d7

 mulu.w #$4,d7

 add.w d7,(MEM_ACTION_TABLE_OFFSET)

 rts

[...]

TestAButtonPressed: ; test if the player pressed the A button

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

 andi.w #BUTTON_A_PRESSED,d6 ; test if the A button was pressed

 beq.w MainGameLoopUpdateSprites ; A button is not pressed

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

 ; build event table address and branch to code

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

 ; clear MEM_CONTROL_PRESSED to prevent state from flipping in loop

 move.w #$0000,(MEM_CONTROL_PRESSED)

 bsr.w BuildNPCObjectList ; update the location of NPCs

 bsr.w FindActionTarget ; find the target of the player's action

 cmpi.w #OBJ_NOTHING,(MEM_ACTION_TARGET_OBJID) ; is the target nothing?

 beq.s NoActionTarget ; branch if no target

 bsr.w BuildActionTableOffset ; build action table offset

 lea ActionTable,a5 ; point to action table

 adda.w (MEM_ACTION_TABLE_OFFSET),a5 ; move to offset location

 move.l (a5),a6 ; a5 has the address of the subroutine to jump to

 jsr (a6) ; jump to location of code to process this event

 bra.w MainGameLoop ; return to start of game loop

[...]

Here's a screenshot of me debugging the code if that's remotely interesting to anyone:

Action table test

This seems like a really small bit of work but in reality it's going to make scripting out the rest of the demo much easier.

Let's give this a quick test by changing an NPC's dialog after you talk to them. As you probably know, one rule of RPGs is you should keep talking to every NPC until they start to repeat themselves:

DialogTextDaniScene0Day0Flag0:

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

 dc.b LF

 dc.b "to close already? ^"

 dc.b FF

 dc.b "Get that guy out of"

 dc.b LF

 dc.b "here so we can leave.'"

 dc.b ETX

 align 2

DialogTextDaniScene0Day0Flag1:

 dc.b "Dani:`Maybe you should"

 dc.b LF

 dc.b "actually talk to him.^"

 dc.b FF

 dc.b "You know, do your job."

 dc.b ETX

 align 2

[...]

Day00Scene00Action00:

 move.l (MEM_DAY_EVENT_FLAGS),d7 ; copy event flags for the day to d7

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

 cmpi.w #OBJ_NPC_DANI,d6 ; test target

 bne.s .2 ; branch to next NPC

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

 ; handle dialog with NPC Dani

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

 btst.l #$1,d7 ; test if flag 1 is set

 bne .1 ; branch if not

 bset.l #$1,d7 ; set flag 1

 move.l d7,(MEM_DAY_EVENT_FLAGS) ; save updated event flags for the day

 lea DialogTextDaniScene0Day0Flag0,a6 ; load dialog text

 bra.s .4 ; branch to setup dialog display

.1

 lea DialogTextDaniScene0Day0Flag1,a6 ; load dialog text

 bra.s .4 ; branch to setup dialog display

.2

 cmpi.w #OBJ_NPC_MALE_SHOPPER0,d6 ; test target

 bne.s .3 ; branch to display default text

 lea DialogTextMaleShopper0,a6

 bra.s .4 ; branch to setup dialog display

.3

 ; default

 bsr.w ShowDefaultText

 bra.s ExitDay00Scene00Action00

.4

 move.l a6,MEM_DIALOG_TEXT ; copy address to MEM_DIALOG_TEXT

 bsr.w SetDialogOpening ; set the dialog opening flags

ExitDay00Scene00Action00:

 rts

And now the text changes after the first time you talk to your sister, how exciting:

Alternate text

In time I may regret the choice of search order. For example, if there are 10 days in the story it seems wrong to have 10 different flows to handle looking at a piece of static scenery. So this crazy action table idea might work decently for interacting with NPCs but not for looking at scenery.

The "right" flow might be closer to [ACTION]->[ACTION_ITEM]->[ACTION_TARGET] which then checks for day or scene specific rules and otherwise returns a default result. Since action targets could be items or NPCs that number gets big really fast. Let's say in theory this demo grows to a game with 10 days, 20 scenes, and 6 actions. Then the action table has something like ~1200 entries. That's a lot. But if there are 6 actions, 20 items that can be used, and 40 potential targets we're at nearly 5000 entries. Both of those numbers are incomprehensible to me at this point so expect this demo to evolve into something with ~5 days, ~20 scenes, and ~4 actions.

Now that I think about it a bit more - it will be pain to not have default text for objects. So let's add that real quick:

ROM_ADDR_DEFAULT_TEXT_TABLE=$AF000

[...]

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

; object default text table

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

 org ROM_ADDR_DEFAULT_TEXT_TABLE

DefaultTextTable:

 ; nothing

 dc.l DialogTextNothing

 ; OBJ_SCENE_VB_8BIT

 dc.l DialogText8Bit

 ; OBJ_SCENE_VB_HARDWARE

 dc.l DialogTextHardware

 ; OBJ_SCENE_VB_16BIT

 dc.l DialogText16Bit

 ; OBJ_SCENE_VB_MAGS

 dc.l DialogTextMags

 ; OBJ_SCENE_VB_COUNTER

 dc.l DialogTextCounter

 ; OBJ_SCENE_VB_REGISTER

 dc.l DialogTextRegister

[...]

ShowDefaultText:

 move.w (MEM_ACTION_TARGET_OBJID),d7 ; move target object ID to d7

 andi.w #$0FFF,d7 ; clear the base value

 mulu.w #$4,d7 ; multiply by 4 to get the offset

 lea DefaultTextTable,a6 ; point to default text table

 adda.l d7,a6 ; add offset

 move.l (a6),(MEM_DIALOG_TEXT) ; copy value at a6 to MEM_DIALOG_TEXT

 ; set flags so the dialog displays next time through the main loop

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

  bset.l #DIALOG_FLAG_TEXT_OPENING,d7 ; change state to opening

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

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

 bset.l #STATE_FLAG_DIALOG,d7 ; set the dialog bit

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

 rts

[...]

SetDialogOpening:

 ; sets flags to display the dialog

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

 bset.l #DIALOG_FLAG_TEXT_OPENING,d7 ; change state to opening

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

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

 bset.l #STATE_FLAG_DIALOG,d7 ; set the dialog bit

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

 rts

What's next?

Let's go back to the "plan" from the last article:

1) Dialog between characters - meaning an NPC says something to you and you respond. Maybe you respond with more dialog, maybe you give them an item. That leads to...

2) Inventory management - along with basic stuff like taking and giving items.

3) Game event tracking - we need a way to say "if [X] happened and you talk to character [Y] then do [Z]", I'll probably over-complicate this. I won't call this 100% complete but it's functional enough.

4) Scripted sprite movements - if the ultimate goal is to get a customer to leave a store then there needs to be an animation where they walk away.

5) Adding & removing NPCs from the current scene - the one NPC is hard-coded, there needs to be a way to move NPCs in and out of scenes. The removing part hasn't been needed yet but it's small and will be addressed with #4.

6) To make this look like a real demo I should really create a title screen and ending message.

7) Dozens of things I didn't think of that will all be painful to work out.

So if our little sprite is going to have to show items to NPCs then next up we'll need an inventory system and menus with commands like "take" and whatnot. We also need a way for the player to select items in a dialog or menu. It probably makes sense to build that first. It sounds like some combination of #1 and #2 will be next.

Download

Download the latest source code on GitHub




Tweet