Skip to main content

Displaying Dialogue

This guide covers how to read dialogue state from the StoryFlow component and display it in your game UI. You will learn about the dialogue state object, how to bind to updates, and two approaches for building your dialogue interface.

Dialogue State

Every time the runtime reaches a dialogue node, the component builds a StoryFlowDialogueState object containing all the information you need to render the dialogue in your UI. This object is passed to the dialogue_updated signal and is available on demand via get_current_dialogue().

GDScript
# StoryFlowDialogueState properties
var is_valid: bool          # Whether this state represents an active dialogue

var node_id: String         # The ID of the current dialogue node

var title: String           # Resolved title text with variable interpolation applied

var text: String            # Resolved dialogue body text with variable interpolation applied

var character: StoryFlowCharacterData  # Character data (name, portrait, variables)

var image: Texture2D        # Current image asset - persists between dialogues
var image_key: String       # The key/path of the current image asset

var audio: AudioStream      # Current audio asset for this dialogue node
var audio_key: String       # The key/path of the current audio asset

var options: Array[StoryFlowDialogueOption]  # Clickable options the player can choose

var text_blocks: Array[StoryFlowTextBlock]   # Non-interactive text blocks

var can_advance: bool       # True when narrative-only with a connected output

var audio_advance_on_end: bool  # Auto-advance after audio finishes playing
var audio_allow_skip: bool      # Player can skip audio and advance early

The character field uses the StoryFlowCharacterData class:

GDScript
# StoryFlowCharacterData properties
var name: String              # The character's display name
var image: Texture2D          # The character's portrait texture (may be null)
var variables: Dictionary     # Per-character variables as { var_name: display_string }

Each entry in options is a StoryFlowDialogueOption with two fields:

  • id (String) - A unique identifier for the option, used when calling select_option()
  • text (String) - The display text with variable interpolation already applied

Each entry in text_blocks is a StoryFlowTextBlock with:

  • id (String) - A unique identifier for the text block
  • text (String) - The display text with variable interpolation already applied

Image Persistence

The image field persists across dialogue nodes. If a previous dialogue set an image and the current dialogue does not specify one, the previous image remains. This lets you set a background image once and have it carry through a sequence of dialogues without repeating it on every node.

Binding to Updates

The StoryFlowComponent emits a dialogue_updated signal every time the dialogue state changes. This fires when a new dialogue node is reached, when a variable update causes the text to re-render, or when options change.

In GDScript:

GDScript
@onready var storyflow: StoryFlowComponent = $StoryFlowComponent

func _ready():
    storyflow.dialogue_started.connect(_on_dialogue_started)
    storyflow.dialogue_updated.connect(_on_dialogue_updated)
    storyflow.dialogue_ended.connect(_on_dialogue_ended)

func _on_dialogue_started():
    # Show your dialogue UI
    dialogue_panel.visible = true

func _on_dialogue_updated(state: StoryFlowDialogueState):
    # Update your UI with the new dialogue state
    print("Dialogue: ", state.text)

func _on_dialogue_ended():
    # Hide your dialogue UI
    dialogue_panel.visible = false

In the Editor:

  1. Select the StoryFlowComponent in the scene tree.
  2. Go to the Node panel and click the Signals tab.
  3. Double-click dialogue_updated to connect it to a method on your script.

The component provides three core signals for the dialogue display lifecycle:

  • dialogue_updated - Emitted every time the dialogue state changes (new node, variable update, option change). Receives the full StoryFlowDialogueState.
  • dialogue_started - Emitted when dialogue execution begins. Use this to show your dialogue UI.
  • dialogue_ended - Emitted when dialogue execution finishes (an End node is reached or there are no more connections). Use this to hide your dialogue UI.

Building Your UI

There are two approaches to building your dialogue UI. Option A provides a ready-made scene with built-in signal handling. Option B gives you full control by manually connecting signals to your own Control nodes.

Option A: Use the Default StoryFlowDialogueUI

The plugin includes a StoryFlowDialogueUI scene (addons/storyflow/ui/storyflow_dialogue_ui.tscn) that extends Control. When you set the dialogue_ui_scene property on the StoryFlow component, it automatically instantiates this scene when dialogue starts and removes it when dialogue ends.

How to set it up:

  1. Select your StoryFlowComponent in the scene tree.
  2. In the Inspector, set Dialogue UI Scene to addons/storyflow/ui/storyflow_dialogue_ui.tscn.
  3. Run the scene. Dialogue will display automatically when start_dialogue() is called.

The default UI includes these nodes (accessed via unique names):

  • %TitleLabel - Displays the dialogue title.
  • %TextLabel - A RichTextLabel with BBCode enabled for the dialogue body text.
  • %CharacterNameLabel - Displays the speaking character's name.
  • %CharacterPortrait - A TextureRect for the character's portrait image.
  • %OptionsContainer - A VBoxContainer that dynamically creates option buttons.
  • %AdvanceButton - A "Continue" button shown for narrative-only dialogue nodes.
  • %BackgroundImage - A TextureRect for full-screen background images (hidden by default).

The default UI has an @export var option_button_scene: PackedScene property that lets you customize the appearance of option buttons. Set it to your own button scene or leave it at the default.

To bind the default UI to your component from code, call initialize_with_component():

GDScript
# If you instantiate the UI manually instead of using dialogue_ui_scene
var ui_scene := preload("res://addons/storyflow/ui/storyflow_dialogue_ui.tscn")
var dialogue_ui := ui_scene.instantiate() as StoryFlowDialogueUI

func _ready():
    add_child(dialogue_ui)
    dialogue_ui.initialize_with_component($StoryFlowComponent)

Customizing the Default UI

The default StoryFlowDialogueUI scene is designed to be a starting point. You can duplicate the scene, restyle the Control nodes, rearrange the layout, or add new elements. As long as the unique name references (%TitleLabel, %TextLabel, etc.) remain intact, the script will continue to work.

Option B: Manual Signal Binding

For full control, skip the dialogue_ui_scene property and connect directly to the component's signals. Build your own Control scene and update it when dialogue changes.

GDScript
extends Control

@onready var storyflow: StoryFlowComponent = %StoryFlowComponent
@onready var title_label: Label = %TitleLabel
@onready var text_label: RichTextLabel = %TextLabel
@onready var speaker_label: Label = %SpeakerLabel
@onready var portrait: TextureRect = %Portrait
@onready var options_container: VBoxContainer = %OptionsContainer
@onready var continue_button: Button = %ContinueButton

func _ready():
    storyflow.dialogue_started.connect(_on_dialogue_started)
    storyflow.dialogue_updated.connect(_on_dialogue_updated)
    storyflow.dialogue_ended.connect(_on_dialogue_ended)
    continue_button.pressed.connect(_on_continue_pressed)
    visible = false

func _on_dialogue_started():
    visible = true

func _on_dialogue_updated(state: StoryFlowDialogueState):
    # Display title and body text
    title_label.text = state.title
    text_label.text = state.text

    # Display character info
    if state.character:
        speaker_label.text = state.character.name
        speaker_label.visible = state.character.name != ""

        if state.character.image:
            portrait.texture = state.character.image
            portrait.visible = true
        else:
            portrait.visible = false
    else:
        speaker_label.visible = false
        portrait.visible = false

    # Display dialogue image
    if state.image:
        # Update a background or scene image if your UI has one
        pass

    # Show continue button or choice options
    continue_button.visible = state.can_advance and state.options.is_empty()

    # Clear and rebuild options
    for child in options_container.get_children():
        child.queue_free()

    for option in state.options:
        var button := Button.new()
        button.text = option.text
        button.pressed.connect(_on_option_selected.bind(option.id))
        options_container.add_child(button)

func _on_dialogue_ended():
    visible = false

func _on_option_selected(option_id: String):
    storyflow.select_option(option_id)

func _on_continue_pressed():
    storyflow.advance_dialogue()

Key points for manual binding:

  • Display title and text in Label or RichTextLabel nodes for the dialogue content
  • Display character.name and character.image for speaker identification
  • Iterate the options array to create choice buttons, and call select_option(option.id) when a button is pressed
  • Show a "Continue" button when can_advance is true and options is empty, and call advance_dialogue() on press
  • Display image in a TextureRect for background or scene images

Variable Interpolation

Dialogue text in StoryFlow supports variable interpolation using the {varname} syntax. When the runtime builds the dialogue state, it automatically replaces these placeholders with the current variable values. All interpolation is resolved before the state reaches your UI - the title, text, and option text fields already contain the final display strings.

Supported interpolation patterns:

  • {varname} - Inserts the value of a project or script variable
  • {Character.Name} - Inserts the current character's name
  • {Character.VarName} - Inserts a per-character variable value

Automatic Re-rendering on Variable Changes

When a Set* node (setBool, setInt, setString, etc.) runs during an active dialogue and has no outgoing edge, the runtime automatically returns to the current dialogue node and re-renders it with the updated variable values. This means the dialogue_updated signal fires again with a new StoryFlowDialogueState containing the freshly interpolated text. Your UI code does not need to handle this specially - simply rebuild the display every time dialogue_updated fires, and variable changes will appear seamlessly.

This re-rendering behavior enables powerful patterns. For example, a dialogue option can trigger a setBool node that changes a flag, and the dialogue text updates immediately to reflect the new state - all without leaving the current dialogue node.

GDScript
# You don't need special handling for variable interpolation.
# The text is already resolved when you receive it:
func _on_dialogue_updated(state: StoryFlowDialogueState):
    # state.text might be "You have 42 gold coins, adventurer."
    # where {gold} and {Character.Name} were interpolated automatically
    text_label.text = state.text

Text Blocks

Text blocks are non-interactive text segments displayed alongside the main dialogue content. Each text block has an id and a text field, and they are available in the text_blocks array of StoryFlowDialogueState.

Unlike options, text blocks are not clickable. They are used for supplementary information, narrator descriptions, stage directions, or any additional text that accompanies the dialogue but does not require player interaction.

GDScript
func display_text_blocks(state: StoryFlowDialogueState):
    # Clear existing text blocks
    for child in text_blocks_container.get_children():
        child.queue_free()

    # Add a label for each text block
    for block in state.text_blocks:
        var label := RichTextLabel.new()
        label.bbcode_enabled = true
        label.text = block.text
        label.fit_content = true
        text_blocks_container.add_child(label)

Display text blocks in your UI below the main dialogue text, or in a separate panel depending on your design. They work well as narrator text, thought bubbles, or contextual information panels.

Need Help?

Join our Discord community to ask questions, share your projects, report bugs, and get support from the team and other users.

Join Discord