Skip to main content
This example demonstrates how to use metadata to trigger animations, hide specific options, and create dynamic visual effects during dialogue. This builds on the metadata handling pattern to create more immersive dialogue experiences.

Overview

In this example, you’ll learn how to:
  • Use metadata to control UI behavior
  • Hide specific options from players
  • Auto-select options based on metadata
  • Implement complex option filtering
  • Create branching paths with hidden logic

The Dialogue

extends DialogueEngine

enum {
    DEFAULT_TOPIC = 0, # this is the branch used by default unless set_branch_id() is used
    WATCH_THE_STORM,
    GO_BACK_TO_SLEEP,
    KEEP_WORKING,
}

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("Wait for storm to finish.")
    var option_id_2: int = entry.add_option("Go back to sleep.")
    var option_id_3: int = entry.add_option("Get back to work.")
    var option_id_4: int = entry.add_option("Hidden option -- this should not be shown on the UI")
    entry.set_metadata("dont_show_options", [option_id_4])
    entry.set_metadata("auto_choose", option_id_4)
    
    var option_id_2_entry: DialogueEntry = add_text_entry("That's right, sleep is for the strong 💪.", GO_BACK_TO_SLEEP)
    entry.set_option_goto_id(option_id_2, option_id_2_entry.get_id())
    
    var option_id_3_entry: DialogueEntry = add_text_entry("That's right, let's get back to work 🫡", KEEP_WORKING)
    entry.set_option_goto_id(option_id_3, option_id_3_entry.get_id())
    
    var option_id_4_entry: DialogueEntry = add_text_entry("I think I'll enjoy watching the storm for a bit...", WATCH_THE_STORM)
    entry.set_option_goto_id(option_id_4, option_id_4_entry.get_id())
    
    # Join branches into the default topic (i.e. branch id 0)
    var default_topic: DialogueEntry = add_text_entry("Some time passes...")
    entry.set_option_goto_id(option_id_1, default_topic.get_id())
    option_id_2_entry.set_goto_id(default_topic.get_id())
    option_id_3_entry.set_goto_id(default_topic.get_id())
    option_id_4_entry.set_goto_id(default_topic.get_id())
    
    add_text_entry("<Press 'Space' or 'Enter' to quit>")

Code Breakdown

1. Creating Options with Hidden Choices

var option_id_1: int = entry.add_option("Wait for storm to finish.")
var option_id_2: int = entry.add_option("Go back to sleep.")
var option_id_3: int = entry.add_option("Get back to work.")
var option_id_4: int = entry.add_option("Hidden option -- this should not be shown on the UI")
All four options are added to the entry, but option 4 will be hidden from the player.

2. Metadata for Option Visibility

entry.set_metadata("dont_show_options", [option_id_4])
entry.set_metadata("auto_choose", option_id_4)
  • dont_show_options: Array of option IDs that should not be displayed to the player
  • auto_choose: The option ID to automatically select if no option is chosen
This pattern is useful for creating “secret” or conditional options, timeout handlers, or default behaviors.

3. Linking All Options

var option_id_2_entry: DialogueEntry = add_text_entry("That's right, sleep is for the strong 💪.", GO_BACK_TO_SLEEP)
entry.set_option_goto_id(option_id_2, option_id_2_entry.get_id())

var option_id_3_entry: DialogueEntry = add_text_entry("That's right, let's get back to work 🫡", KEEP_WORKING)
entry.set_option_goto_id(option_id_3, option_id_3_entry.get_id())

var option_id_4_entry: DialogueEntry = add_text_entry("I think I'll enjoy watching the storm for a bit...", WATCH_THE_STORM)
entry.set_option_goto_id(option_id_4, option_id_4_entry.get_id())
Even though option 4 is hidden, it still needs a valid goto ID since it can be auto-selected.

Implementing the UI

Here’s a simplified version of the UI that handles hidden options:
extends VBoxContainer

@export var dialogue_gdscript: GDScript = null
var dialogue_engine: DialogueEngine = null
var enabled_buttons: Array[Button] = []

func _ready() -> void:
    dialogue_engine = dialogue_gdscript.new()
    dialogue_engine.dialogue_continued.connect(__on_dialogue_continued)

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():
        var dont_show_options: Array = p_dialogue_entry.get_metadata("dont_show_options", [])
        for option_id: int in range(0, p_dialogue_entry.get_option_count()):
            if option_id in dont_show_options:
                continue
            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)

Filtering Hidden Options

var dont_show_options: Array = p_dialogue_entry.get_metadata("dont_show_options", [])
for option_id: int in range(0, p_dialogue_entry.get_option_count()):
    if option_id in dont_show_options:
        continue
    # Create button for this option
The UI checks the dont_show_options metadata and skips creating buttons for those option IDs.

Real Animation Example

Let’s extend this to include actual character animations:
func _setup() -> void:
    var entry: DialogueEntry = add_text_entry("*looks at the window nervously*") \
        .set_metadata("character", "Player") \
        .set_metadata("animation", "look_window") \
        .set_metadata("emotion", "nervous")
    
    var option_id_1: int = entry.add_option("Take a deep breath")
    var option_id_2: int = entry.add_option("Ignore the storm")
    
    var calm_entry: DialogueEntry = add_text_entry("*breathes deeply and relaxes*", CALM_BRANCH) \
        .set_metadata("animation", "breathe") \
        .set_metadata("emotion", "calm")
    entry.set_option_goto_id(option_id_1, calm_entry.get_id())
    
    var ignore_entry: DialogueEntry = add_text_entry("*turns away from the window*", IGNORE_BRANCH) \
        .set_metadata("animation", "turn_away") \
        .set_metadata("emotion", "determined")
    entry.set_option_goto_id(option_id_2, ignore_entry.get_id())

UI Handler for Animations

func __on_dialogue_continued(entry: DialogueEntry) -> void:
    # Display text
    dialogue_label.text = entry.get_text()
    
    # Handle character display
    if entry.has_metadata("character"):
        var character_name = entry.get_metadata("character")
        character_label.text = character_name
    
    # Play animation
    if entry.has_metadata("animation"):
        var anim_name = entry.get_metadata("animation")
        character_animator.play(anim_name)
    
    # Apply emotion effect
    if entry.has_metadata("emotion"):
        var emotion = entry.get_metadata("emotion")
        apply_emotion_effect(emotion)

func apply_emotion_effect(emotion: String) -> void:
    match emotion:
        "nervous":
            character_sprite.modulate = Color(1.0, 0.9, 0.9)
            start_shake_effect()
        "calm":
            character_sprite.modulate = Color(0.9, 1.0, 0.9)
            stop_shake_effect()
        "determined":
            character_sprite.modulate = Color(1.0, 1.0, 1.0)

Advanced: Animation Timing

You can use metadata to control when dialogue text appears relative to animations:
func _setup() -> void:
    var entry: DialogueEntry = add_text_entry("*slams fist on table*") \
        .set_metadata("animation", "slam_table") \
        .set_metadata("animation_wait", true) \
        .set_metadata("animation_duration", 1.5) \
        .set_metadata("sfx", "table_slam")

UI Implementation

func __on_dialogue_continued(entry: DialogueEntry) -> void:
    if entry.has_metadata("animation"):
        var anim_name = entry.get_metadata("animation")
        character_animator.play(anim_name)
        
        if entry.has_metadata("sfx"):
            var sfx = entry.get_metadata("sfx")
            audio_player.stream = load("res://sfx/" + sfx + ".ogg")
            audio_player.play()
        
        # Wait for animation to finish before showing text
        if entry.get_metadata("animation_wait", false):
            var duration = entry.get_metadata("animation_duration", 1.0)
            await get_tree().create_timer(duration).timeout
    
    # Now show the dialogue text
    dialogue_label.text = entry.get_text()

Common Animation Metadata Patterns

Character Entrance/Exit

add_text_entry("A mysterious figure approaches...") \
    .set_metadata("spawn_character", "mysterious_stranger") \
    .set_metadata("entrance_animation", "fade_in")

add_text_entry("Farewell!") \
    .set_metadata("exit_animation", "walk_away") \
    .set_metadata("despawn_character", "mysterious_stranger")

Camera Effects

add_text_entry("BOOM! An explosion!") \
    .set_metadata("camera_shake", {"intensity": 5.0, "duration": 0.8}) \
    .set_metadata("flash_screen", Color.WHITE)

Particle Effects

add_text_entry("*magical energy swirls around them*") \
    .set_metadata("spawn_particles", "magic_swirl") \
    .set_metadata("particle_position", Vector2(100, 200))

Multiple Animations

add_text_entry("The hero strikes with incredible force!") \
    .set_metadata("animations", [
        {"character": "hero", "animation": "attack"},
        {"character": "enemy", "animation": "hit_react"},
        {"character": "enemy", "animation": "fall", "delay": 0.5}
    ])

Key Takeaways

Metadata enables animation control - Store animation names, timings, and parameters in metadata.
Hidden options provide flexibility - Use dont_show_options to hide UI elements while keeping logic intact.
Auto-choose for defaults - Set auto_choose metadata for timeout or default behavior.
Combine multiple metadata - Use multiple metadata keys together for rich, complex behaviors.
Await for timing - Use await in your UI code to synchronize text display with animations.

Next Steps

Timed Options

Create time-limited dialogue choices with animations

Signals Reference

Learn about all dialogue signals for advanced control

Build docs developers (and LLMs) love