Over the years I managed to maintain a simple dialog system in Godot, which uses yield (or await for Godot 4). Ever feel like making an epic RPG with deep storylines? Unless you only do environmental storytelling, it's great to have dialogs. Or dialogues. However you spell it.
But first, a word about why maybe your game doesn't need all that.
Do you really need modal dialogs?
By modal dialogs I mean text which stops the gameplay and waits for the player to press a button.
Instead there are very good reasons to do non-modal, positional, "background dialogs". I do it often with jam games, and primarily with Ace of Rope:
- No new control necessary (just let the player character be somewhere)
- Easy to implement: put a Label node in an Area node and show it when entered
- The player can just keep walking if they read fast or want to skip it
- No need for a backlog, you just backtrack
Drawbacks:
- Don't place 2 dialog elements close together or they will both overlap (talk at the same time). Also, not too close to the window border.
- Can't show dialogs bit by bit for timing
- Can't stop the player
- Usually limited to 1 talker
- You might need a backlog anyway if a local dialog changes with story progression.
So yeah, modal dialogs are still useful.
A simple modal dialog
Here is a simple example which doesn't even have text animation or delay support. I have scenes inherited from Areas called Interactables that prompt for buttons to start the dialogue and emit a signal. Then I can connect it to a signal callback in the current level's scene:
func _on_NPC_dialog_started():
await Globals.say("I am the first line...")
await Globals.say("And the second line!")
unlock_door()
The "say" function is in a "Globals" singleton where I put all the other global stuff of the game:
signal _enter
onready var dialog: Label
func is_cutscene() -> bool:
return dialog.visible
func say(text: String) -> GDScriptFunctionState:
dialog.show()
dialog.text = text
var state: GDScriptFunctionState = await _enter
dialog.hide()
$NextDialogSound.play()
return state
func _unhandled_input(event: InputEvent):
if event.is_action_pressed("ui_accept"):
_enter.emit()
get_viewport().set_input_as_handled()
Using is_cutscene, all objects in the game can check if a cutscene (here: a dialog line) is ongoing. For example, the player and enemies stop moving. It doesn't go all the way to pausing the game either: you could keep an animation playing, and even move your actors around for the cutscene.
Can be extended if you want some of the characters to have portraits, speech bubbles to place above them, or thinking bubbles when they don't talk out loud. I do that for Uproar in Bug Parliament.
More complicated dialog systems
I recommend Nathan Hoad's Dialogue Manager.
Bonus: a quick and dirty localization system
Godot has all the necessary tools for localization but what about no-engine games?
Personally, since I self-translate my small games in only 2 languages, I check a boolean ternarily and I hardcode everything in a switch statement. Behold my device:
typedef enum { TITLE, PLAY, STORY1... } line_t;
char *tr(line_t line) {
bool fr = globals.is_french; // or whatever
switch (line) {
case TITLE: return fr ? "Mon jeu" : "My game";
case PLAY: return fr ? "Jouer" : "Play";
case STORY_1: return fr ? "Il était une fois..." : "Once upon a time...";
...
}
}
It works fine. I hope your eyes still work after reading this code.
On that, see ya in the next one!