Today I’ll be going over how I implemented the dialog system for Siege For Yourself, the game a couple of friends and I made for the 2023 GMTK Game Jam. If you’d like to see a post about the whole process, and what we learned, take a look at this post. The dialog system isn’t very complicated, there are no branching paths or anything, but I’m still satisfied with how I organized it. Because of that though, this week’s post is going to be on the shorter side.
Our previous game, Unweighted, had dialog as well. In that game, it was part of the fields we enumerated for each level, along with things like the level’s name. This worked fine for that game, since the text box was completely static. In this game, I wanted to have dialog with a bit more depth to it, so a system like the one we used in Unweighted would not work out.
On the most basic level, a dialog is a list of dialog lines. Each line has to store the text being spoken, at a minimum. To add some flavor, I wanted there to be distinct speakers, so the name of the speaker has to be stored as well. A naive approach might be to store it as a constant, like the following:
const DIALOG = [
{
"speaker": "Abbott",
"text": "Who's on second?"
},
{
"speaker": "Costello",
"text": "No, Who's on first."
},
{
"speaker": "Abbott",
"text": "I'm telling you, I don't know!"
},
{
"speaker": "Costello",
"text": "He's the guy on third base!"
},
...
]
This could work fine for prototyping, but it can be error-prone. Plus all those extra keystrokes copying or typing the “speaker” and “text” fields every time can add up to a lot of wasted time. So to make a better solution, I turned to Godot’s resource system. This allowed me to quickly make a dialog line and dialog class, and use the editor to create instances. The first thing I did was create a new script file that inherits from Resource, instead of inheriting from Node.
I made two of these, one for an individual line, and another for a collection of lines, just to make it easier to type hint things. The full source of both of these scripts are below (yes, they really are that short).
class_name DialogLine
extends Resource
@export var speaker: String ## The speaker.
@export_multiline var text: String ## The text being spoken
class_name Dialog
extends Resource
@export var lines: Array[DialogLine]
Now, in order to create a dialog, all we need to do is make a new Resource, and select Dialog as the type. Editing the dialog is done in the inspector sidebar. It’s not a seamless experience, but it gets the job done, and is a lot nice than editing raw code.
After the data structure was finalized, all that was left was to make a scene to display it. I made a simple text box using some semi-transparent ColorRects. For the text itself, I used a RichTextLabel, which allows the use of BBCode to apply styles like bold and italics. The entire text box scene looks like this:
The text box scene is always present in the game scene, so to get it to play a dialog, you call a function called
play()
, and pass in the Dialog resource. The lines get copied over, and the first is displayed.
func play(new_dialog: Dialog) -> void:
lines = new_dialog.lines.duplicate()
show()
display_line()
Each time the next button is pressed, a new line is displayed by calling next()
. text_finished
is a signal that gets
emitted when the text is done, so that other parts of the game can handle the state change.
func next() -> void:
if lines:
display_line()
else:
text_finished.emit()
hide()
All in all, this was a pretty good solution to storing and displaying dialog. I’m proud that I came up with this under the constraints of a game jam.