Skip to main content
This example demonstrates how to create dialogue with player choices (options). Players can select from multiple options, and each option leads to a different dialogue branch.

Overview

In this example, you’ll learn how to:
  • Add options to dialogue entries
  • Set goto IDs for each option
  • Display options in the UI as buttons
  • Handle option selection
  • Join multiple branches back to common dialogue

The Dialogue

extends DialogueEngine

enum {
    DEFAULT_TOPIC = 0, # this is the default branch id used at each add_text_entry call unless a different branch ID is specified
    GO_BACK_TO_SLEEP = 1,
    KEEP_WORKING = 2,
}

func _setup() -> void:
    var entry: DialogueEntry = add_text_entry("The storm rages right outside the window. I should...")
    
    var option_id_1: int = entry.add_option("Go back to sleep.")
    var option_id_1_entry: DialogueEntry = add_text_entry("That's right, sleep is for the strong 💪.", GO_BACK_TO_SLEEP)
    entry.set_option_goto_id(option_id_1, option_id_1_entry.get_id())
    
    var option_id_2: int = entry.add_option("Get back to work.")
    var option_id_2_entry: DialogueEntry = add_text_entry("That's right, let's get back to work 🫡", KEEP_WORKING)
    entry.set_option_goto_id(option_id_2, option_id_2_entry.get_id())
    
    # Join branches into the default topic (i.e. branch id 0)
    var default_topic: DialogueEntry = add_text_entry("Some time passes...")
    option_id_1_entry.set_goto_id(default_topic.get_id())
    option_id_2_entry.set_goto_id(default_topic.get_id())
    
    # None of the following entries will be connected on the graph and won't be shown when advancing the dialogue
    add_text_entry("A sleep entry skipped due to missing goto against this entry.", GO_BACK_TO_SLEEP)
    add_text_entry("A working entry due to missing goto against this entry.", KEEP_WORKING)
    
    add_text_entry("<Press 'Space' or 'Enter' to quit>")

Code Breakdown

1. Branch IDs for Organization

enum {
    DEFAULT_TOPIC = 0,
    GO_BACK_TO_SLEEP = 1,
    KEEP_WORKING = 2,
}
Branch IDs help organize different dialogue paths. The default branch (0) is used for main dialogue flow.

2. Creating the Choice Entry

var entry: DialogueEntry = add_text_entry("The storm rages right outside the window. I should...")
This creates a regular text entry that will display the question.

3. Adding Options

var option_id_1: int = entry.add_option("Go back to sleep.")
var option_id_1_entry: DialogueEntry = add_text_entry("That's right, sleep is for the strong 💪.", GO_BACK_TO_SLEEP)
entry.set_option_goto_id(option_id_1, option_id_1_entry.get_id())
For each option:
  1. Add the option text: add_option() returns an option ID
  2. Create the target entry: Add the dialogue entry for this choice path
  3. Link them: Use set_option_goto_id() to connect the option to its target entry
The option ID is returned by add_option() and is used to reference that specific option. Option IDs start at 0 and increment with each added option.

4. Joining Branches

var default_topic: DialogueEntry = add_text_entry("Some time passes...")
option_id_1_entry.set_goto_id(default_topic.get_id())
option_id_2_entry.set_goto_id(default_topic.get_id())
Both choice branches converge back to a common dialogue entry. This ensures the dialogue continues regardless of which option was chosen.
Use set_goto_id() to explicitly link entries. This creates a “jump” from one entry to another, useful for joining branches or creating loops.

5. Disconnected Entries

add_text_entry("A sleep entry skipped due to missing goto against this entry.", GO_BACK_TO_SLEEP)
add_text_entry("A working entry due to missing goto against this entry.", KEEP_WORKING)
These entries are in the dialogue tree but are not connected to any other entry, so they will never be reached during dialogue execution.
Entries without incoming connections (no goto pointing to them) are unreachable and won’t be displayed. Use the debugger to visualize your dialogue tree and find disconnected entries.

Creating the UI

Here’s the complete UI script that handles displaying options:
extends VBoxContainer

@export var dialogue_gdscript: GDScript = null
var dialogue_engine: DialogueEngine = null

func _ready() -> void:
    dialogue_engine = dialogue_gdscript.new()
    dialogue_engine.dialogue_started.connect(__on_dialogue_started)
    dialogue_engine.dialogue_continued.connect(__on_dialogue_continued)
    dialogue_engine.dialogue_finished.connect(__on_dialogue_finished)
    dialogue_engine.dialogue_canceled.connect(__on_dialogue_canceled)

func _input(p_input_event: InputEvent) -> void:
    if p_input_event.is_action_pressed(&"ui_accept"):
        dialogue_engine.advance()
        accept_event() # to avoid hitting a button due to the input event travelling through the children

func __on_dialogue_started() -> void:
    print("Dialogue Started!")

var enabled_buttons: Array[Button] = []

func __on_dialogue_continued(p_dialogue_entry: DialogueEntry) -> void:
    var label: RichTextLabel = RichTextLabel.new()
    label.set_use_bbcode(true)
    label.set_fit_content(true)
    label.set_text("  > " + p_dialogue_entry.get_text())
    add_child(label)
    
    if p_dialogue_entry.has_options():
        for option_id: int in range(0, p_dialogue_entry.get_option_count()):
            var option_text: String = p_dialogue_entry.get_option_text(option_id)
            var button: Button = Button.new()
            button.set_text(option_text)
            add_child(button)
            if option_id == 0:
                button.grab_focus()
            button.pressed.connect(__advance_dialogue_with_chosen_option.bind(option_id))
            enabled_buttons.push_back(button)
        set_process_input(false)

func __advance_dialogue_with_chosen_option(p_option_id: int) -> void:
    for button: Button in enabled_buttons:
        button.set_disabled(true)
    enabled_buttons.clear()
    
    var current_entry: DialogueEntry = dialogue_engine.get_current_entry()
    current_entry.choose_option(p_option_id)
    dialogue_engine.advance()
    
    set_process_input(true)

func __on_dialogue_finished() -> void:
    print("Dialogue Finished! Exiting...")
    get_tree().quit()

func __on_dialogue_canceled() -> void:
    print("Dialogue Canceled! Exiting...")
    get_tree().quit()

UI Code Breakdown

1. Detecting Options

if p_dialogue_entry.has_options():
    for option_id: int in range(0, p_dialogue_entry.get_option_count()):
        var option_text: String = p_dialogue_entry.get_option_text(option_id)
        # ...
Check if the current entry has options using has_options(), then iterate through all options with get_option_count().

2. Creating Option Buttons

var button: Button = Button.new()
button.set_text(option_text)
add_child(button)
if option_id == 0:
    button.grab_focus()
button.pressed.connect(__advance_dialogue_with_chosen_option.bind(option_id))
enabled_buttons.push_back(button)
For each option:
  • Create a button with the option text
  • Focus the first button by default
  • Connect the button press to the handler, binding the option ID
  • Track the button for later cleanup

3. Disabling Input

set_process_input(false)
Disable the “press Enter to continue” input while options are shown, so the player must click a button.

4. Handling Option Selection

func __advance_dialogue_with_chosen_option(p_option_id: int) -> void:
    for button: Button in enabled_buttons:
        button.set_disabled(true)
    enabled_buttons.clear()
    
    var current_entry: DialogueEntry = dialogue_engine.get_current_entry()
    current_entry.choose_option(p_option_id)
    dialogue_engine.advance()
    
    set_process_input(true)
When an option is selected:
  1. Disable all option buttons
  2. Clear the button array
  3. Get the current entry and call choose_option() with the selected ID
  4. Advance the dialogue
  5. Re-enable keyboard input
You must call choose_option() before calling advance() when handling options. The engine needs to know which option was chosen to determine the next entry.

Dialogue Flow

1. "The storm rages right outside the window. I should..."
   [Options displayed]
   - Go back to sleep.
   - Get back to work.

2a. [Player chooses "Go back to sleep"]
    "That's right, sleep is for the strong 💪."
    "Some time passes..."
    "<Press 'Space' or 'Enter' to quit>"

2b. [Player chooses "Get back to work"]
    "That's right, let's get back to work 🫡"
    "Some time passes..."
    "<Press 'Space' or 'Enter' to quit>"

Advanced: Complex Option Trees

You can nest options to create complex dialogue trees:
func _setup() -> void:
    var entry1: DialogueEntry = add_text_entry("What would you like to know?")
    
    # First level options
    var opt_lore: int = entry1.add_option("Tell me about the lore")
    var opt_quests: int = entry1.add_option("Any quests available?")
    var opt_goodbye: int = entry1.add_option("Goodbye")
    
    # Lore branch with nested options
    var lore_entry: DialogueEntry = add_text_entry("Which topic interests you?", branch.LORE)
    entry1.set_option_goto_id(opt_lore, lore_entry.get_id())
    
    var opt_history: int = lore_entry.add_option("The kingdom's history")
    var opt_magic: int = lore_entry.add_option("Magic system")
    
    var history_entry: DialogueEntry = add_text_entry("Long ago, this kingdom...", branch.HISTORY)
    lore_entry.set_option_goto_id(opt_history, history_entry.get_id())
    
    var magic_entry: DialogueEntry = add_text_entry("Magic flows through...", branch.MAGIC)
    lore_entry.set_option_goto_id(opt_magic, magic_entry.get_id())
    
    # Quests branch
    var quest_entry: DialogueEntry = add_text_entry("I have a task for you...", branch.QUESTS)
    entry1.set_option_goto_id(opt_quests, quest_entry.get_id())
    
    # Goodbye branch
    var goodbye_entry: DialogueEntry = add_text_entry("Farewell!")
    entry1.set_option_goto_id(opt_goodbye, goodbye_entry.get_id())
    
    # Join all branches back to common end
    var end_entry: DialogueEntry = add_text_entry("Come back anytime.")
    history_entry.set_goto_id(end_entry.get_id())
    magic_entry.set_goto_id(end_entry.get_id())
    quest_entry.set_goto_id(end_entry.get_id())
    goodbye_entry.set_goto_id(end_entry.get_id())

Key Takeaways

Use has_options() to detect choices - Check this in your UI code to know when to display option buttons.
Always call choose_option() before advance() - The engine needs to know which option was selected.
Join branches with set_goto_id() - This prevents dialogue from ending prematurely after a choice.
Track and disable buttons - Prevent multiple clicks by disabling buttons after selection.
If an option’s goto ID is invalid or if no option is chosen before advancing, the dialogue_canceled signal will be emitted.

Next Steps

Metadata Handling

Attach custom data to dialogue for character names, emotions, etc.

Timed Options

Create options that expire after a time limit

Build docs developers (and LLMs) love