Devlog 05: Dialogue Trees
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.
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.
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.
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:
|$50||dialogue||Show dialogue id|
|$51||choice off||Disabled choice #id|
|$52||choice on||Enables choice #id|
|$53||page off||Disables page #id.|
|$54||page on||Enables page #id|
|$55||dialogue image||Changes the speaker’s portrait.|
|$56||dialogue name||Change the speaker’s name|
|$5F||dialogue end||Closes the dialogue|
dlg will launch the dialogue UI and set the global flag
True. Running out of Pages or a call to
dlgend will set it to false and tear down the dialogue.
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.
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.