Sega Genesis Programming Part 4: Echo Sound Engine


Choosing a menu style

In this installment I'm going to attempt to implement some really basic menus where the player can select stuff and then other stuff happens. I know, that's all very specific. The first step is figuring out what style of menus to implement.

There are really two different menu questions... 1) What happens when the player interacts with a person or object (A button press)? and 2) What happens when the player opens the menu (C button press)? Right now I'm going to tackle the first one unless I'm especially productive.

I'm going to start by looking at a few games that are inspirations for this crazy project. Phantasy Star II has obviously been a big inspiration so it's first on the list. In it, the player interacts with NPCs by walking up to them and pressing A. The C button opens the menu, selecting an item opens another menu and so on until the screen is covered:

Phantasy Star II menus

The talking to NPCs part is fine but menu part is a little iffy. It's neat that the player is never taken off the overworld screen but it's easy to get lost in the menus. In this example there are three top level windows with active selections. Giving or using an item opens a fourth. It's kinda confusing.

The next game in the series uses the A button to talk to NPCs and the C button now opens a full-screen menu:

Phantasy Star III menus

I prefer this style even though it could require building character portraits which is something I'll be terrible at.

Phantasy Star IV went back to a Phantasy Star II menu system but swapped the A and C buttons just to mess with people:

Phantasy Star IV menus

The original Phantasy Star was a bit different. Walking into NPCs triggered a dialog screen. The menu took up the whole screen like Phantasy Star III but used a window system like II & IV.

Phantasy Star menus

I really like having the current scene background in the menu. Phantasy Star III would look amazing if it did that instead of the monochrome background for example. This fits into the "cool idea that's a bunch of work so I probably won't do it" category.

Jumping over to the Super Nintendo, Final Fantasy IV uses a system like Phantasy Star III with talk and menu buttons:

Final Fantasy IV menus

Of course it's worked this way since the beginning of the series:

Final Fantasy menus

Earthbound isn't a huge inspiration other than the setting. At the risk of making new enemies, I found Earthbound to be a mostly aggravating game. Now some of that could be based on when I played it. My biggest complaint is all the repetitive grinding required to not get killed constantly. Phantasy Star II also suffers this problem. The difference is I played Phantasy Star II when it was new-ish and I had endless free time. I played Earthbound just a couple years ago. I bet if I played Phantasy Star II for the first time tomorrow I'd hate it.

Anyway, the menus use a window style. Talking to people isn't automatic, it's one of the options on the menu if the player is facing someone:

Earthbound menus

Going back the Genesis (err... Sega CD) for a moment. Lunar: The Silver Star is a great game with a menu that's more style than function:

Lunar: The Silver Star menus

OK, so the last game we're going to look at is on the Super Nintendo and it's one of my favorites - Shadowrun. It handles this differently than every other game here. The one part that's similar is the full screen menu:

Shadowrun menu

Interacting with characters and items is where it gets interesting. The player selects something on the screen and is presented with a menu of options. From there they choose what action to take. Examining an NPC shows some text while talking to them might show a quick response from them:

Shadowrun talk menu

More often, talking to an NPC opens a dialog where the player can ask about keywords learned throughout the game:

Shadowrun dialog

This is really what I'd like to build in the long run but I have to start smaller. Character portraits, keywords, and a more advanced script are all great but if I over-complicate this I'll never finish. So I'm going to start small and maybe build-up to something grander once I get the basics working.

There are a few options to consider. One is showing a list of possible actions when the player presses A while facing an NPC:

Menu option #1

When facing an object the options could be slightly different:

Menu option #2

Pressing C could then open the menu:

Menu option #3

Hmm... I don't like that idea. I'll build a real full-screen menu later.

Another option is showing a brief object description above the options:

Menu option #4

Here's how that would look with an NPC:

Menu option #5

I'm leaning toward the last idea. There are some pros and cons, OK just one of each I can think of right now:

Pro: One button press to access to all actions you can perform against stuff.

Con: Two button presses to interact with stuff. It could be possible to just default to look or talk based on some rules.

And last up is a slightly more advanced option to enable dialog between the player and an NPC. This is less sophisticated than Shadowrun but still gives the player a way to choose how they interact with NPCs.

Menu option #6

I am also leaning toward building this because it's a simple interface to allow choices. Let's face it, every RPG/adventure game has a bit where the player talks to someone and it goes like "Here's this horrible problem I'm facing, can you help me?" followed by a yes/no option. Selecting "yes" advances the plot while selecting "no" gives the player a chance to go do other stuff before this new quest. It seems like something I'll need to have eventually anyway. This style of dialog could also be used for "There's a shiny red button here. Do you want to press it?"

If these last two ideas are used then there are something like 3 different dialog modes:

Dialog mode Usage A/C button action B button action D-pad action
Simple text Non-interactive messages Advance dialog, close dialog when on last page Advance dialog, close dialog when on last page Advance dialog, close dialog when on last page
Text with choice Interactive dialog Advance dialog, select items when on last page Advance dialog, cancel menu when on last page Advance dialog, move between selections when on last page
Overworld menu Interact with objects & NPCs Select items Cancel menu Move between selections

This doesn't seem all that difficult to tack on to the existing demo. I will likely regret this statement as I attempt to implement it...

Showing the menu

The first step to building interactive menus is just drawing them. Even before that let's define what our action codes are. The first three map to the buttons in the 'A' button menu. The captions on the menu change based on whether it's an NPC or object so these are mapping to the menu order. This makes life easier later. Also going back to the last article, limiting the number of actions limits the size of the action table which is good. The fourth action will be used for when the player responds to a "yes/no" type question.


ACTION_LOOK=$0000 ; look at an object or NPC
ACTION_USE_TALK=$0001 ; use an object or interact with an NPC
ACTION_TAKE_GIVE=$0002 ; try to take an object or give an item to an NPC
ACTION_RESPOND=$0003 ; respond to a selection in a conversation dialog

Next are some new dialog flags that effect how the dialog behaves. Three of these are based on the style of menu being shown while the last will be used to flag that the dialog should remain open until the player makes a selection.


DIALOG_FLAG_STYLE_MENU=$17 ; overworld menu style dialog
DIALOG_FLAG_STYLE_TEXT_CHOICE=$18 ; dialog has text with selection at the end
DIALOG_FLAG_STYLE_SIMPLE_TEXT=$19 ; dialog is a simple text dialog
DIALOG_FLAG_SELECTION_WAIT=$1A ; waiting for the player to make a selection 

Now we need to create the text for the menus. The '{' character is being re-purposed to serve as an empty selector spot. Since '(', '{', '[', and '<' are all in the font it's not a big sacrifice.


NPCMenu:
 dc.b "{Look {Talk {Give",ETX
ObjectMenu:
 dc.b "{Look {Use {Take",ETX

The existing code that showed a general "look/talk" message will be replaced by a subroutine to show the selection dialog. This looks at the target object and references a lookup table to find the title of the menu.


ShowSelectionDialog:
 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
 move.w (MEM_ACTION_TARGET_OBJID),d6 ; copy action target to d6
 andi.w #OBJ_SCENE_BASE,d6 ; and against OBJ_SCENE_BASE
 beq.s .1 ; if the result is zero then this is not scenery
 lea ObjectNameTable,a6 ; point to default object text table
 bra.s .2 branch to next step
.1 ; target object is an NPC
 lea NPCNameTable,a6 ; point to default npc text table
.2
 adda.l d7,a6 ;add offset
 move.l (a6),(MEM_DIALOG_TEXT) ; copy value at a6 to MEM_DIALOG_TEXT
 ; set dialog 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
 bset.l #DIALOG_FLAG_STYLE_MENU,d7 ; set style to overworld menu
 bset.l #DIALOG_FLAG_SELECTION_WAIT,d7 ; set flag to wait for selection
 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
 move.w #$0000,(MEM_MENU_SELECTION) ; default to first menu selection
 rts

Here are the lookup tables and their values:


ObjectNameTable:
 dc.l $00000000
 ; OBJ_SCENE_VB_8BIT
 dc.l ObjectName8Bit
 ; OBJ_SCENE_VB_HARDWARE
 dc.l ObjectNameComputerHardware
 ; OBJ_SCENE_VB_16BIT
 dc.l ObjectName16Bit
 ; OBJ_SCENE_VB_MAGS
 dc.l ObjectNameMagazines
 ; OBJ_SCENE_VB_COUNTER
 dc.l ObjectNameCounter
 ; OBJ_SCENE_VB_REGISTER
 dc.l ObjectNameRegister
NPCNameTable:
 dc.l $00000000
 ; OBJ_NPC_DANI
 dc.l NPCNameDani
 ; OBJ_NPC_MALE_SHOPPER0
 dc.l NPCNameMapperShopper0
[...]
ObjectName8Bit:
 dc.b "Wall of 8-bit games",OBJMENU
ObjectNameComputerHardware:
 dc.b "Computer hardware",OBJMENU
ObjectName16Bit:
 dc.b "Rack of 16-bit games",OBJMENU
ObjectNameMagazines:
 dc.b "Magazine rack",OBJMENU
ObjectNameCounter:
 dc.b "Counter",OBJMENU
ObjectNameRegister:
 dc.b "Cash register",OBJMENU
NPCNameDani:
 dc.b "Dani (your sister)",NPCMENU
NPCNameMapperShopper0:
 dc.b "Business casual guy",NPCMENU

You're probably wondering? What's the deal with those new OBJMENU and NPCMENU constants? Those are used to tell the routine that draws the dialog that it's the end of the line and to insert a menu in the next line. That turned out to be a smaller change than I expected:


ProcessDialogTextDrawing:
[...]
TestObjMenu:
 cmpi.b #OBJMENU,d6 ; draw the object menu?
 bne.s TestNPCMenu ; not the OBJMENU character, continue to next test
 move.l #(VDP_VRAM_WRITE_A+DIALOG_ROWCOL),(MEM_DIALOG_VPD) ; base address
 add.l #$01020000,(MEM_DIALOG_VPD) ; add 258 tomove 2 rows and column
 lea ObjectMenu,a6 ; load object menu text
 move.l a6,(MEM_DIALOG_TEXT) ; copy address to MEM_DIALOG_TEXT
 bra.w ExitProcessDialog
TestNPCMenu:
 cmpi.b #NPCMENU,d6 ; draw the npc menu?
 bne.s TestLF ; not the NPCMENU character, continue to next test
 move.l #(VDP_VRAM_WRITE_A+DIALOG_ROWCOL),(MEM_DIALOG_VPD) ; base address
 add.l #$01020000,(MEM_DIALOG_VPD) ; add 258 tomove 2 rows and column
 lea NPCMenu,a6 ; load npc menu text
 move.l a6,(MEM_DIALOG_TEXT) ; copy address to MEM_DIALOG_TEXT
 bra.w ExitProcessDialog
[...]

This gets us up to drawing an empty menu with a caption.

Empty menu

This isn't interesting enough to stop so let's continue with...

Showing selections

Now for the difficult part - actually selecting menu options. We need some sort of visual indicator for which menu item is selected. I was curious how Phantasy Star II did this and it turns out they used a high layer sprite:

Phantasy Star II menu sprite

Hmm.. so a while back I created a off-screen "sprite zero" that exists only to get sprite ordering to work. Maybe I can re-purpose it to also act as a menu selector.

So after creating an 8x8 pattern for sprite zero we need some code to move it around and hide it:


MoveSelectorSprite: ; this also sets DIALOG_FLAG_SELECTION_WAIT if appropriate
 ; store sprite x in d4, sprite y in d5
 move.w #$0000,d4 ; set to 0 by default
 move.w #$0000,d5 ; set to 0 by default
 btst.l #DIALOG_FLAG_TEXT_CLOSING,d7 ; test if the dialog is closing
 bne.s .2 ; flag is set,move to update VDP section
 btst.l #DIALOG_FLAG_STYLE_MENU,d7 ; test if this is a menu
 beq.s .1 ; flag is not set, try next test
 bset.l #DIALOG_FLAG_SELECTION_WAIT,d7 ; set waiting for selection flag 
 move.w #DIALOG_MENU_INIT_SELECTION_X,d4 ; x value
 ; move x value based on which item is selected
 move.w #$0038,d6 ; the selections are 38 apart
 mulu.w (MEM_MENU_SELECTION),d6 ; multiply by selection number
 add.w d6,d4 ; add result to x
 move.w #DIALOG_MENU_INIT_SELECTION_Y,d5 ; y value
 addq #$8,d5 ; adjust for options being on 2nd row of dialog
 bra.s .2 ; move to update VDP section
.1
 btst.l #DIALOG_FLAG_STYLE_TEXT_CHOICE,d7 ; test if this is a menu
 beq.s .2 ; flag is not set, use default values
 bset.l #DIALOG_FLAG_SELECTION_WAIT,d7 ; set waiting for selection flag 
 move.w #DIALOG_MENU_INIT_SELECTION_X,d4 ; x value
 move.w #DIALOG_MENU_INIT_SELECTION_Y,d5 ; y value
 ; move y value based on which item is selected
 move.w #$0008,d6 ; the selections are 8 apart
 mulu.w (MEM_MENU_SELECTION),d6 ; multiply by selection number
 add.w d6,d5 ; add result to y
.2
 ;---------------------------------------------------------------------------
 ; update y
 ;---------------------------------------------------------------------------
 move.l #VDP_VRAM_WRITE_SPRITE,d6 ; add to sprite table address
 move.l d6,(VDP_CONTROL) ; set write location in VDP
 move.w d5,(VDP_DATA) ; copy the new y-coordinate
 ;---------------------------------------------------------------------------
 ; update x
 ;---------------------------------------------------------------------------
 add.l #$00060000,d6 ;move to x-coordinate
 move.l d6,(VDP_CONTROL) ; set write location in VDP
 move.w d4,(VDP_DATA) ; copy the new x-coordinate
ExitMoveSelectorSprite:
 rts
HideSelectorSprite:
 ;---------------------------------------------------------------------------
 ; update y
 ;---------------------------------------------------------------------------
 move.l #VDP_VRAM_WRITE_SPRITE,d6 ; add to sprite table address
 move.l d6,(VDP_CONTROL) ; set write location in VDP
 move.w #$0000,(VDP_DATA) ; copy the new y-coordinate
 ;---------------------------------------------------------------------------
 ; update x
 ;---------------------------------------------------------------------------
 add.l #$00060000,d6 ;move to x-coordinate
 move.l d6,(VDP_CONTROL) ; set write location in VDP
 move.w #$0000,(VDP_DATA) ; copy the new x-coordinate
ExitHideSelectorSprite:
 rts

Also needed is code to increment & decrement the selected menu item. This needs to work for both horizontal menus with three options and vertical choice menus with two options:


IncrementMenuSelection:
 btst.l #DIALOG_FLAG_STYLE_MENU,d7 ; test if this is a menu
 beq.s .2 ; flag is not set
 addq #$1,(MEM_MENU_SELECTION) ; increment menu selection
 cmpi.w #$0003,(MEM_MENU_SELECTION) ; at the last menu item?
 bge.s .1 ; at the last menu item, branch
 rts
.1
 move.w #$0000,(MEM_MENU_SELECTION) ; rollover to first menu item
 rts
.2 ; not DIALOG_FLAG_STYLE_MENU - must be DIALOG_FLAG_STYLE_TEXT_CHOICE
 addq #$1,(MEM_MENU_SELECTION) ; increment menu selection
 cmpi.w #$0002,(MEM_MENU_SELECTION) ; at the last menu item?
 bge.s .3 ; at the last menu item, branch
 rts
.3
 move.w #$0000,(MEM_MENU_SELECTION) ; rollover to first menu item
 rts
DecrementMenuSelection:
 btst.l #DIALOG_FLAG_STYLE_MENU,d7 ; test if this is a menu
 beq.s .2 ; flag is not set
 cmpi.w #$0000,(MEM_MENU_SELECTION) ; at the first menu item?
 ble.s .1 ; at the first menu item, branch
 subq #$1,(MEM_MENU_SELECTION) ; decrement menu selection
 rts
.1
 move.w #$0002,(MEM_MENU_SELECTION) ; rollover to first menu item
 rts
.2 ; not DIALOG_FLAG_STYLE_MENU - must be DIALOG_FLAG_STYLE_TEXT_CHOICE
 cmpi.w #$0000,(MEM_MENU_SELECTION) ; at the first menu item?
 ble.s .3 ; at the first menu item, branch
 subq #$1,(MEM_MENU_SELECTION) ; decrement menu selection
 rts
.3
 move.w #$0001,(MEM_MENU_SELECTION) ; rollover to first menu item
 rts

Next up is calling these methods when a menu or choice is displaying. The d-pad should move the indicator, B should cancel out of the menu, while A & C should accept the current selection.


[...]
ProcessDialogTestButtonPress:
 ; 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
 cmpi.w #BUTTON_START_PRESSED,d6 ; test if the start button is held
 beq.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 tomove 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.w ExitProcessDialog ; exit
.4
 btst.l #DIALOG_FLAG_SELECTION_WAIT,d7 ; test if waiting for selection 
 beq.w ProcessDialogSetClosing ; branch if waiting selection flag not set
 cmpi.w #BUTTON_B_PRESSED,d6 ; is the b button pressed?
 beq.w ProcessDialogSetClosing ; close if b button is pressed
 cmpi.w #BUTTON_RIGHT_PRESSED,d6 ; is the right button pressed?
 bne.s .5 ; right is not pressed,move to next test
 bsr.w IncrementMenuSelection ; increment the menu selection
 move.w #$0000,(MEM_CONTROL_PRESSED) ; clear pressed buttons
 bsr.w moveSelectorSprite ;move the selector sprite
 bra.w ExitProcessDialog ; exit
.5 ; down
 cmpi.w #BUTTON_DOWN_PRESSED,d6 ; is the down button pressed?
 bne.s .6 ; down is not pressed,move to next test
 bsr.w IncrementMenuSelection ; increment the menu selection
 move.w #$0000,(MEM_CONTROL_PRESSED) ; clear pressed buttons
 bsr.w moveSelectorSprite ;move the selector sprite
 bra.w ExitProcessDialog ; exit
.6 ; left
 cmpi.w #BUTTON_LEFT_PRESSED,d6 ; is the left button pressed?
 bne.s .7 ; left is not pressed,move to next test
 bsr.w DecrementMenuSelection ; increment the menu selection
 move.w #$0000,(MEM_CONTROL_PRESSED) ; clear pressed buttons
 bsr.w moveSelectorSprite ;move the selector sprite
 bra.w ExitProcessDialog ; exit
.7 ; up
 cmpi.w #BUTTON_UP_PRESSED,d6 ; is the up button pressed?
 bne.s .8 ; up is not pressed,move to next test
 bsr.w DecrementMenuSelection ; increment the menu selection
 move.w #$0000,(MEM_CONTROL_PRESSED) ; clear pressed buttons
 bsr.w moveSelectorSprite ;move the selector sprite
 bra.s ExitProcessDialog ; exit
.8 ; a
 cmpi.w #BUTTON_A_PRESSED,d6 ; is the a button pressed?
 bne.s .9 ; a is not pressed,move to next test
 bsr.w ConfirmMenuSelection ; confirm the menu selection
 move.w #$0000,(MEM_CONTROL_PRESSED) ; clear pressed buttons
 bra.s ExitProcessDialog ; exit
.9 ; c
 cmpi.w #BUTTON_C_PRESSED,d6 ; is the c button pressed?
 bne.s ExitProcessDialog ; c is not pressed, exit
 bsr.w ConfirmMenuSelection ; confirm the menu selection
 move.w #$0000,(MEM_CONTROL_PRESSED) ; clear pressed buttons
 bra.s ExitProcessDialog ; exit
[...]

I think that routine could be shortened a bit. Rather than tackle that now let's proceed to...

Responding to selections

When the A or C button is pressed the first thing that happens is calling a sub-routine that saves the selection and updates the dialog state:



ConfirmMenuSelection:
 btst.l #DIALOG_FLAG_STYLE_MENU,d7 ; test if this is a menu
 beq.s .1 ; flag is not set
 move.w (MEM_MENU_SELECTION),(MEM_ACTION_ID) ; selection->action
 bra.s ExitConfirmMenuSelection ; exit
.1 ; not DIALOG_FLAG_STYLE_MENU - must be DIALOG_FLAG_STYLE_TEXT_CHOICE
 move.w #ACTION_RESPOND,(MEM_ACTION_ID) ; action is respond
 move.w (MEM_MENU_SELECTION),(MEM_MENU_RESPONSE) ; selection->response
ExitConfirmMenuSelection:
 move.l (MEM_GAME_STATE),d6	; copy current game state to d6
 bset.l #STATE_FLAG_ACTION,d6 ; clear the dialog bit
 move.l d6,(MEM_GAME_STATE) ; copy it back
 rts

Back in the main loop, when STATE_FLAG_ACTION is set it redirects to a new routine that processes the queued action. This builds off the last article and adds code to clear the dialog so it can be reused:


ProcessAction:
 bsr.w ResetDialog ; reset the dialog
 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
 move.l (MEM_GAME_STATE),d7 ; copy current game state to d7
 bclr.l #STATE_FLAG_ACTION,d7 ; clear action flag
 move.l d7,(MEM_GAME_STATE) ; save it back
 rts

If the action that was just invoked needs the dialog it would look weird to close and re-open it. I'm having a hard time thinking of when an action wouldn't re-use the dialog. If the player did something and the dialog just closed with no message that would be really confusing. Although I suppose there are cases where the "use" command might fire off an animation or something. Anyway, reseting the dialog is a matter of switching flags so it stays open and starts re-drawing text at the top.


; resets the dialog so new text can be drawn without closing & reopening
ResetDialog:
 move.l (MEM_DIALOG_FLAGS),d7 ; copy current dialog state to d7
 ; 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 text drawing flag
 bclr.l #DIALOG_FLAG_TEXT_OPEN,d7 ; clear open flag
 bclr.l #DIALOG_FLAG_TEXT_NEW_PAGE,d7 ; clear new page flag
 bclr.l #DIALOG_FLAG_STYLE_TEXT_CHOICE,d7 ; clear menu style choice flag
 bclr.l #DIALOG_FLAG_STYLE_MENU,d7 ; clear menu style flag
 bset.l #DIALOG_FLAG_STYLE_SIMPLE_TEXT,d7 ; set simple text style flag
 bclr.l #DIALOG_FLAG_SELECTION_WAIT,d7 ; clear waiting for selection flag
 move.l d7,(MEM_DIALOG_FLAGS) ; copy game state back to d7
 move.w #$0000,(MEM_MENU_SELECTION) ; reset menu selection
 bsr.w HideSelectorSprite ; hide the selection icon
 rts

Somewhere in this article you might be wondering "That's an awful lot of dialog flags, what is this dude thinking?" I think this whole dialog flag thing I'm doing is the direct result of doing Windows UI development for a couple decades. In the Windows world, and presumably similar GUIs, creating dialogs involves setting a bunch of flags then calling a show dialog routine. The dialog appearance & behavior can change quite a bit based on the flag combinations. All that implementation logic is handled by Windows. I'm sort of copying that by creating one large method that handles all dialog behavior and is controlled by a set of dialog flags.

The last steps are updating the action script and adding new text. In this iteration the player can talk to the shopper in the white shirt. At the end of that dialog the player is presented a choice. If the player selects either choice then they (a) receive a message based on the choice and (b) receive a different message next time they talk to the shopper.


Day00Scene00Action01: ; ACTION_USE_TALK
 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 it is
 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 .5 ; branch to setup dialog display
.1
 lea DialogTextDaniScene0Day0Flag1,a6 ; load dialog text
 bra.s .5 ; branch to setup dialog display
.2
 ;---------------------------------------------------------------------------
 ; handle dialog with shopper 
 ;---------------------------------------------------------------------------
 cmpi.w #OBJ_NPC_MALE_SHOPPER0,d6 ; test target
 bne.s .4 ; branch to display default text
 btst.l #$2,d7 ; test if flag 2 is set
 bne .3 ; branch if it is set
 move.l (MEM_DIALOG_FLAGS),d7 ; copy dialog flags to d7
 bset.l #DIALOG_FLAG_STYLE_TEXT_CHOICE,d7 ; set text choice flag
 move.l d7,(MEM_DIALOG_FLAGS) ; save updated dialog flags
 lea DialogTextMaleShopper0Scene0Day0Flag0,a6 ; load dialog text
 bra.s .5 ; branch to setup dialog display
.3 ; flag 2 is set
 lea DialogTextMaleShopper0Scene0Day0Flag2,a6 ; load dialog text
 bra.s .5 ; branch to setup dialog display
.4
 ; default
 lea DialogTextNothingHappens,a6 ; load default text
.5
 move.l a6,MEM_DIALOG_TEXT ; copy address to MEM_DIALOG_TEXT
ExitDay00Scene00Action01:
 rts
[...]
Day00Scene00Action03: ; ACTION_RESPOND
 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_MALE_SHOPPER0,d6 ; test target
 bne.s .2 ; branch to display default text
 bset.l #$2,d7 ; set flag 2
 move.l d7,(MEM_DAY_EVENT_FLAGS) ; save the updated flags
 cmpi.w #$0000,(MEM_MENU_RESPONSE) ; is the menu selection 0?
 bne.s .1 ; branch if not equal to zero (there are only two options)
 lea DialogTextMaleShopper0Scene0Day0Flag0R0,a6 ; load response text
 move.l a6,MEM_DIALOG_TEXT ; copy address to MEM_DIALOG_TEXT
 bra.s ExitDay00Scene00Action03 ; exit
.1
 lea DialogTextMaleShopper0Scene0Day0Flag0R1,a6 ; load response text
 move.l a6,MEM_DIALOG_TEXT ; copy address to MEM_DIALOG_TEXT
 bra.s ExitDay00Scene00Action03 ; exit
.2
 lea DialogTextNothingHappens,a6 ; load default text
 move.l a6,MEM_DIALOG_TEXT ; copy address to MEM_DIALOG_TEXT
ExitDay00Scene00Action03:
 bsr.w ResetDialog ; reset the dialog
 rts
[...]
DialogTextMaleShopper0Scene0Day0Flag0:
 ; "1234567890123456789012"
 dc.b "`Do you know if I have",LF
 dc.b "a mouse on COM2 using^",FF
 dc.b "IRQ3 can I hookup one",LF
 dc.b "of these modems ^",FF
 dc.b "on COM4 using IRQ4?'",LF
 dc.b " ^",FF
 dc.b "{Sure, why not?",LF
 dc.b "{I make $3.35 an hour.",ETX
DialogTextMaleShopper0Scene0Day0Flag0R0:
 ; "1234567890123456789012"
 dc.b "`You chose response",LF
 dc.b "zero, neat.'",ETX
DialogTextMaleShopper0Scene0Day0Flag0R1:
 ; "1234567890123456789012"
 dc.b "`You chose response",LF
 dc.b "one, alright.'",ETX
DialogTextMaleShopper0Scene0Day0Flag2:
 ; "1234567890123456789012"
 dc.b "`You're talking to me",LF
 dc.b "again now.'",ETX

Time to test it all out by talking to the shopper:

First dialog with NPC

Pressing B at the selection dialog will cause this to repeat next time he's talked to. Pressing A or C changes the next dialog with him:

Second dialog with NPC

This was a little painful to get working the first time. You don't see the parts where I'm trying to debug why instead of the dialog a bunch of gibberish prints. However, having gone through this adding new dialog is now reasonably easy. The only tricky part is having to update things in multiple places: dialog text file, dialog table, and the action script which may need updates in multiple action handlers. I don't think there's an easy way to get around this. Perhaps in a couple months I'll think of a better way to handle actions.

What's next?

Let's go back to the "plan" from two articles ago:

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.

Looks like inventory is next.

Download

Download the latest source code on GitHub




Related