Devlog 05: Dialogue Trees

Reading time: 8 min

Hello World!

I know that last time I said I was going to talk about text wrapping and a better way to display text, but right now I need to figure out how to make NPCs talk, so don’t hold it against me.

The Anatomy of a Conversation

Dialogue with an NPCs is really just a very tiny “Choose Your Own Adventure” story. The NPC will have some things to say, then the player is presented a choice. Not all choices will necessarily be available at all times, they may not be shown because the player hasn’t met some criteria or because that choice has already been made. Let’s define some terminology, as usual, I like to work bottom-up.

  • Text ID: An id into our string table, where the actual text string resides.
  • Choice: A Text ID, whether it’s enabled or not, an id to the next Page (see below), and optionally, the offset to the a script that should run if this choice is made.
  • Page: Pages contain a Text ID and either the id to the next Page (for multi page dialogue) or a collection of Choices.
  • Dialogue: A collection of Pages, the Text ID for the NPC’s name, and an image id for the user’s portrait.

Structure

Here’s how the previous definition map to new types in Blitz Basic (I’m using DG_ as a namespace for Dialogue):

NEWTYPE .DG_choice
    text_id.w           ; index into string table
    flag.w              ; flag that determines if criteria is met, $FFFF if none
    page_id.w           ; id to the next page in the conversation
    script_offset.w     ; script to run if selected, $FFFF if none
    enabled.b           ; determine if it should be shown. If this is False,
                        ; it overrides the flag property
    padding.b           ; padding to make this even size
End NEWTYPE

NEWTYPE .DG_page
    text_id.w           ; index into string array, $FFFF if no text
    page_id.w           ; index to the next page to display, $FFFF if no page
    first_choice_id.w   ; id of the first choice
    choice_count.w      ; number of choices in the page
    enabled.b           ; determine if it should be shown.
    padding.b           ; padding to make this even size
End NEWTYPE

NEWTYPE .DG_dialogue
    first_page_id.w     ; id of the first page of dialogue
    page_count.w        ; number of page in the dialogue
    speaker_name.w      ; text id for the speaker's name
    speaker_image.w     ; shape id for the speaker's portrait
End NEWTYPE

Doing anything in Blitz Basic with collections of variable lengths is a huge pain, so we’re making some concessions here. DG_page and DG_dialogue have a first_X_id and X_count properties, which will index into two different dynamically allocated arrays for the choices and pages respectively. This puts the constrain that choices and pages are sequential and makes it almost impossible to reuse pages and choices in other places.

This is fine...

This is fine...

New Chunks

We have to store all of this stuff on disk somehow and, by in large, we’ll store them exactly as they will appear in memory. Even if we decide to compress the file later, uncompressed it will still be in same format we want to have it in RAM.

We’ll store all the DG_dialogues, followed by the DG_pages, and finally all the DG_choices. All the text will be stored in the string table in the file.

Each section is preceded by a small header that looks like this:

NEWTYPE DG_dialogue_header
    name.l              ; $444C4753, 'DLGS' for 'Dialogues'
    count.l             ; number of dialogues to follow
End NEWTYPE

NEWTYPE DG_page_header
    name.l              ; $50414745, 'PAGE' for 'Pages'
    count.l             ; number of pages to follow
End NEWTYPE

NEWTYPE DG_choice_header
    name.l              ; $43484345, 'CHCE' for 'Choices'
    count.l             ; number of choices to follow
End NEWTYPE

While the game’s file format doesn’t call for there to be multiple copies of chunks or arbitrary order for the chunks (they are always in a predefined order) it makes it easy to look at the binary file and debug.

Updating NOIRscript

With these new features, we need to update NOIRscript, the assembly-like scripting language for NEONnoir, to be able to manipulate and display dialogues. These are going to be the new opcodes:

OpcodeValueNameMeaning
dlg id$50dialogueShow dialogue id
choff id$51choice offDisabled choice #id
chon id$52choice onEnables choice #id
pageoff id$53page offDisables page #id.
pageon id$54page onEnables page #id
dlgimg id$55dialogue imageChanges the speaker’s portrait.
dlgname id$56dialogue nameChange the speaker’s name
dlgend$5Fdialogue endCloses the dialogue

Displaying Dialogues

Calling dlg will launch the dialogue UI and set the global flag DG_in_dialogue to True. Running out of Pages or a call to dlgend will set it to false and tear down the dialogue.

While DG_in_dialogue is set, we maintain the a current page id and, if there are choices, rectangular areas that we can test when the user presses the mouse button. If there are no choices, then the whole page is a clickable area.

If we get to a page with no choices and no ’next page’, click on a choice with no destination page, or DG_in_dialogue becomes false, we tear down the dialogue UI.

A matter of style

While we know the “how” we display the dialogue in a technical sense, we don’t know what it looks like. I can think of two options, each with their pros and cons.

Overlay over background: One option is to have a box on the left side of the screen showing the portrait of the speaker and the text on the right, both overlayed over the background.

The Pro for this is that it grounds the conversation in the location and is probably what people would expect to happen.

The Con is that we need to do some advanced palette trickery. We only have 256 colors to use. The first 32 are being used by the font and the UI, leaving 224 colors for each background. If we reduce the color count to 192, we could use those extra 32 as we wish. We could “load” into those color registers any 32 color palette for whatever we want to display, items, special effects, or in this case, the portrait of the speaker.

Unfortunately this comes with extra work but it might be worthwhile to have the flexibility.

Dialogue screen: Another option is to clear the screen black and display a 224 color image of the speaker without any background. This lets us devote more colors to the speaker without having to do any fancy palette trickery. However it takes you away from the scene and it doesn’t solve the issue that the speaker still needs to appear on the background in some form in order to click on them and start the dialogue.

I may have to punt this decision down the road. The overlay over the background I think could look the best at the cost of visual fidelity of the background but some quick testing seems to indicate that at the difference between 224 and 192 is negligible and even going down to 128 colors yields pretty decent results.

This is going to require its own devlog and in the meantime, let’s keep our focus on the functional part of the dialogue system.

Crafting Dialogues

There are many ways to author dialogue trees (dialogue graphs, really, since it can double back on itself). One way would be to use a graph-based editor but, while intuitive, it would require more work to build than I want to devote to my editor. We’re on a tight schedule after all.

Another way, and what I think I’ll settle on, is to use a more flat style like writing a “Choose Your Own Adventure” book. In this form, dialogues would be represented in a table with pages and choices being more indented. Something like this (D stands for Dialogue, P for Page, and C for Choice):

D1: The Gutter - The Alleycat, Speaker: Glitch
    P1: Hey man, you don't look like you're from around these parts.
        Lucky for you, I'mkind of tour guide. (Disable P1, Goto P2)
    P2: So what do you want to know?
        C1: What's the Alleycat? (Goto P4)
        C2: Where's good to eat around here? (Goto P5)
        C3: See you around (Goto P3)
    P3: I'll be here if you need me (End D1)
    P4: Life is the only thing cheaper than the thrills at the Alleycat. If I were
        you, I'd be careful you don't get eaten alive.
        C4: Figuratively or literally? (Goto P6)
    P5: Takeshi's noodles was the best place, and just around the corner too. We
        don't talk about what happened to this him. (Disable C2, Goto P2)
    P6: Both. (Disable C1, Goto P2)

In this particular case, you speak to Glitch. Speaking to him the first time disables P1 so we don’t get his intro next time we talk to him. Choices 1 and 2 lead to pages answering the questions and then disable the choices before returning to the parent page. Eventually, only C3 remains and selecting it closes the dialogue.

The expectation is that talking to Glitch again will go to P1 which is disabled, so we automatically go to P2. C1 and C2 are gone because we asked before, leaving only C3. Not the best designed dialogue because if the player forgets or missed the text they can’t go back and re-read it, but it’s good enough to illustrate how it works.