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:
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:
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:
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.
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:
Of course it's worked this way since the beginning of the series:
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:
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:
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:
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:
More often, talking to an NPC opens a dialog where the player can ask about keywords learned throughout the game:
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:
When facing an object the options could be slightly different:
Pressing C could then open the menu:
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:
Here's how that would look with an NPC:
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.
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.
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:
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:
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:
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