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().
# 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:
# 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 callingselect_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:
@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:
- Select the
StoryFlowComponentin the scene tree. - Go to the Node panel and click the Signals tab.
- Double-click
dialogue_updatedto 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:
- Select your
StoryFlowComponentin the scene tree. - In the Inspector, set Dialogue UI Scene to
addons/storyflow/ui/storyflow_dialogue_ui.tscn. - 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- ARichTextLabelwith BBCode enabled for the dialogue body text.%CharacterNameLabel- Displays the speaking character's name.%CharacterPortrait- ATextureRectfor the character's portrait image.%OptionsContainer- AVBoxContainerthat dynamically creates option buttons.%AdvanceButton- A "Continue" button shown for narrative-only dialogue nodes.%BackgroundImage- ATextureRectfor 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():
# 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.
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
titleandtextin Label or RichTextLabel nodes for the dialogue content - Display
character.nameandcharacter.imagefor speaker identification - Iterate the
optionsarray to create choice buttons, and callselect_option(option.id)when a button is pressed - Show a "Continue" button when
can_advanceistrueandoptionsis empty, and calladvance_dialogue()on press - Display
imagein aTextureRectfor 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.
# 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.
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.