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 struct, how to bind to updates, and two approaches for building your dialogue widget.
Dialogue State
Every time the runtime reaches a dialogue node, the component builds an FStoryFlowDialogueState struct containing all the information you need to render the dialogue in your UI. This struct is passed to delegates and is available on demand via GetCurrentDialogue().
USTRUCT(BlueprintType)
struct FStoryFlowDialogueState
{
GENERATED_BODY()
// The ID of the current dialogue node
UPROPERTY(BlueprintReadOnly, Category = "StoryFlow")
FString NodeId;
// Resolved title text with variable interpolation applied
UPROPERTY(BlueprintReadOnly, Category = "StoryFlow")
FString Title;
// Resolved dialogue body text with variable interpolation applied
UPROPERTY(BlueprintReadOnly, Category = "StoryFlow")
FString Text;
// Tags authored on this dialogue node, in authored order (empty when untagged)
UPROPERTY(BlueprintReadOnly, Category = "StoryFlow")
TArray<FString> Tags;
// Current image asset - persists between dialogues unless explicitly reset
UPROPERTY(BlueprintReadOnly, Category = "StoryFlow")
UTexture2D* Image = nullptr;
// Current audio asset for this dialogue node
UPROPERTY(BlueprintReadOnly, Category = "StoryFlow")
USoundBase* Audio = nullptr;
// Character data (name, portrait, per-character variables)
UPROPERTY(BlueprintReadOnly, Category = "StoryFlow")
FStoryFlowCharacterData Character;
// Non-interactive text blocks displayed alongside the dialogue
UPROPERTY(BlueprintReadOnly, Category = "StoryFlow")
TArray<FStoryFlowDialogueOption> TextBlocks;
// Clickable options the player can choose from
UPROPERTY(BlueprintReadOnly, Category = "StoryFlow")
TArray<FStoryFlowDialogueOption> Options;
// Whether this state represents an active dialogue
UPROPERTY(BlueprintReadOnly, Category = "StoryFlow")
bool bIsValid = false;
// True when the dialogue is narrative-only with a connected output.
// Use this to show a "Continue" button instead of choice options.
UPROPERTY(BlueprintReadOnly, Category = "StoryFlow")
bool bCanAdvance = false;
// True when dialogue will auto-advance after audio finishes playing
UPROPERTY(BlueprintReadOnly, Category = "StoryFlow")
bool bAudioAdvanceOnEnd = false;
// True when player can click to skip audio and advance early
UPROPERTY(BlueprintReadOnly, Category = "StoryFlow")
bool bAudioAllowSkip = false;
};
The Character field uses the FStoryFlowCharacterData struct:
USTRUCT(BlueprintType)
struct FStoryFlowCharacterData
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "StoryFlow")
FString Name;
UPROPERTY(BlueprintReadOnly, Category = "StoryFlow")
UTexture2D* Image = nullptr;
UPROPERTY(BlueprintReadOnly, Category = "StoryFlow")
TMap<FString, FStoryFlowVariant> Variables;
};
Each entry in Options and TextBlocks is an FStoryFlowDialogueOption with two fields:
- Id (FString) - A unique identifier for the option, used when calling
SelectOption() - Text (FString) - The display text with variable interpolation already applied
The Tags field, added in v1.2.2, carries the tags authored on the current dialogue node in authored order and is empty for an untagged line. The component also raises OnDialogueTagReached once per tag as the line is entered. See Dialogue Tags for the event and its firing rules.
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 UStoryFlowComponent broadcasts an OnDialogueUpdated delegate 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 Blueprint:
Right-click your StoryFlow component in the Details panel and select Bind Event to On Dialogue Updated. The bound function receives an FStoryFlowDialogueState parameter containing the current state.
In C++:
// In your actor's BeginPlay
void AMyDialogueActor::BeginPlay()
{
Super::BeginPlay();
UStoryFlowComponent* StoryFlow = FindComponentByClass<UStoryFlowComponent>();
if (StoryFlow)
{
StoryFlow->OnDialogueUpdated.AddDynamic(
this, &AMyDialogueActor::HandleDialogueUpdated);
StoryFlow->OnDialogueStarted.AddDynamic(
this, &AMyDialogueActor::HandleDialogueStarted);
StoryFlow->OnDialogueEnded.AddDynamic(
this, &AMyDialogueActor::HandleDialogueEnded);
}
}
void AMyDialogueActor::HandleDialogueUpdated(
const FStoryFlowDialogueState& State)
{
// Update your UI with the new dialogue state
UE_LOG(LogTemp, Log, TEXT("Dialogue: %s"), *State.Text);
}
void AMyDialogueActor::HandleDialogueStarted()
{
// Show your dialogue widget
}
void AMyDialogueActor::HandleDialogueEnded()
{
// Hide your dialogue widget
} The component provides three delegates you can bind to:
- OnDialogueUpdated - Fires every time the dialogue state changes (new node, variable update, option change). Receives the full
FStoryFlowDialogueState. - OnDialogueStarted - Fires when dialogue execution begins. Use this to show your dialogue UI.
- OnDialogueEnded - Fires 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 convenient base class with built-in event handling. Option B gives you full control by manually binding to component delegates.
Option A: Extend UStoryFlowDialogueWidget
The plugin provides UStoryFlowDialogueWidget, a base UUserWidget class designed to be extended in Blueprint or C++. When you set the DialogueWidgetClass property on the StoryFlow component, it creates an instance of your widget when dialogue starts and lets it go when dialogue ends. By default it also adds the widget to the viewport and removes it again, which you can take over - see Widget Ownership.
Override these functions to build your UI:
OnDialogueUpdated(FStoryFlowDialogueState)- Called every time the dialogue state changes. Rebuild your text, options, and character display here.OnDialogueStarted()- Called when dialogue execution begins. Under the default placement the widget is already in the viewport by the time this fires, so use it for intro animations and one-time setup.OnDialogueEnded()- Called when dialogue finishes, just before the component releases the widget. Use it for cleanup, or to tear the widget down yourself when your game owns placement.
Built-in helper functions:
SelectOption(FString OptionId)- Submit a player choice. Pass theIdfrom anFStoryFlowDialogueOption.AdvanceDialogue()- Advance past a narrative-only dialogue (whenbCanAdvanceis true).GetCurrentDialogueState()- Returns the currentFStoryFlowDialogueStateat any time.IsDialogueActive()- Returns whether a dialogue is currently running.GetLocalizedString(FString Key)- Look up a localized string from the project's string tables.DetachFromComponent()- Stop following the component: unsubscribes from its events and clears the component reference. Added in v1.2.2 and covered in Widget Ownership.
// MyDialogueWidget.h
UCLASS()
class UMyDialogueWidget : public UStoryFlowDialogueWidget
{
GENERATED_BODY()
protected:
UPROPERTY(meta = (BindWidget))
UTextBlock* TitleText;
UPROPERTY(meta = (BindWidget))
UTextBlock* DialogueText;
UPROPERTY(meta = (BindWidget))
UTextBlock* CharacterNameText;
UPROPERTY(meta = (BindWidget))
UImage* CharacterImage;
UPROPERTY(meta = (BindWidget))
UImage* DialogueImage;
UPROPERTY(meta = (BindWidget))
UVerticalBox* OptionsContainer;
UPROPERTY(meta = (BindWidget))
UButton* ContinueButton;
virtual void OnDialogueUpdated_Implementation(
const FStoryFlowDialogueState& State) override;
virtual void OnDialogueStarted_Implementation() override;
virtual void OnDialogueEnded_Implementation() override;
};
// MyDialogueWidget.cpp
void UMyDialogueWidget::OnDialogueUpdated_Implementation(
const FStoryFlowDialogueState& State)
{
// Display title and body text
TitleText->SetText(FText::FromString(State.Title));
DialogueText->SetText(FText::FromString(State.Text));
// Display character info
CharacterNameText->SetText(FText::FromString(State.Character.Name));
if (State.Character.Image)
{
CharacterImage->SetBrushFromTexture(State.Character.Image);
CharacterImage->SetVisibility(ESlateVisibility::Visible);
}
else
{
CharacterImage->SetVisibility(ESlateVisibility::Collapsed);
}
// Display dialogue image
if (State.Image)
{
DialogueImage->SetBrushFromTexture(State.Image);
DialogueImage->SetVisibility(ESlateVisibility::Visible);
}
else
{
DialogueImage->SetVisibility(ESlateVisibility::Collapsed);
}
// Show continue button or choice options
ContinueButton->SetVisibility(
State.bCanAdvance ? ESlateVisibility::Visible : ESlateVisibility::Collapsed);
// Clear and rebuild options
OptionsContainer->ClearChildren();
for (const FStoryFlowDialogueOption& Option : State.Options)
{
UButton* Button = NewObject<UButton>(OptionsContainer);
UTextBlock* Label = NewObject<UTextBlock>(Button);
Label->SetText(FText::FromString(Option.Text));
Button->AddChild(Label);
// Capture OptionId for the click handler
FString OptionId = Option.Id;
Button->OnClicked.AddDynamic(this, &UMyDialogueWidget::OnOptionClicked);
OptionsContainer->AddChild(Button);
}
}
void UMyDialogueWidget::OnDialogueStarted_Implementation()
{
// Under the default bAutoAddWidgetToViewport = true the component has
// already added this widget to the viewport, so this is where per-dialogue
// setup goes, not AddToViewport()
ContinueButton->SetVisibility(ESlateVisibility::Collapsed);
}
void UMyDialogueWidget::OnDialogueEnded_Implementation()
{
// The component removes the widget as soon as this event returns, so keep
// this to final bookkeeping. An outro animation needs the game to own
// placement instead - see Widget Ownership below.
OptionsContainer->ClearChildren();
} Blueprint Is Easier
While the C++ example above is useful as a reference, the recommended approach is to create a Widget Blueprint that extends UStoryFlowDialogueWidget. You can override OnDialogueUpdated, OnDialogueStarted, and OnDialogueEnded directly in the event graph and build your UI layout in the UMG designer.
Option B: Manual Binding
If you need full control over the widget lifecycle or want to integrate with an existing UI framework, create a standard UUserWidget and bind to the component delegates yourself.
// In your custom widget or game mode
void AMyGameMode::SetupDialogueUI()
{
UStoryFlowComponent* StoryFlow = DialogueActor->FindComponentByClass<UStoryFlowComponent>();
if (!StoryFlow) return;
StoryFlow->OnDialogueUpdated.AddDynamic(this, &AMyGameMode::OnDialogueUpdated);
StoryFlow->OnDialogueStarted.AddDynamic(this, &AMyGameMode::OnDialogueStarted);
StoryFlow->OnDialogueEnded.AddDynamic(this, &AMyGameMode::OnDialogueEnded);
}
void AMyGameMode::OnDialogueUpdated(const FStoryFlowDialogueState& State)
{
if (!DialogueWidget) return;
// Manually update your widget
DialogueWidget->UpdateDialogue(State.Title, State.Text);
DialogueWidget->UpdateCharacter(State.Character.Name, State.Character.Image);
DialogueWidget->UpdateOptions(State.Options);
DialogueWidget->SetContinueVisible(State.bCanAdvance);
}
// When the player selects an option
void AMyGameMode::HandleOptionSelected(const FString& OptionId)
{
UStoryFlowComponent* StoryFlow = DialogueActor->FindComponentByClass<UStoryFlowComponent>();
if (StoryFlow)
{
StoryFlow->SelectOption(OptionId);
}
}
// When the player clicks "Continue"
void AMyGameMode::HandleContinue()
{
UStoryFlowComponent* StoryFlow = DialogueActor->FindComponentByClass<UStoryFlowComponent>();
if (StoryFlow)
{
StoryFlow->AdvanceDialogue();
}
} Key points for manual binding:
- Display
TitleandTextin text widgets for the dialogue content - Display
Character.NameandCharacter.Imagefor speaker identification - Iterate the
Optionsarray to create choice buttons, and callSelectOption(Option.Id)when a button is clicked - Show a "Continue" button when
bCanAdvanceistrue, and callAdvanceDialogue()on click - Display
Imagein an Image widget for background or scene images
Widget Ownership
Option A and Option B are not the only choice. Since v1.2.2 you can keep the component's widget creation and still decide where that widget lives, through the bAutoAddWidgetToViewport setting on the component. It defaults to true, which is exactly the behavior of every earlier version.
true(default) - The component adds the widget to the viewport when dialogue starts and removes it when dialogue ends. Nothing to do on your side.false- The component only creates and initializes the widget, then hands it over. It never touches placement, so the widget can live in your HUD, a widget stack or a 3D widget component. Destroying it is yours too, withOnDialogueEndedas the cue.
What happens when dialogue starts:
- The component creates the widget from
DialogueWidgetClass. - It calls
InitializeWithComponenton it, which subscribes the widget to the component's events. - It adds the widget to the viewport, but only when
bAutoAddWidgetToViewportistrue. OnDialogueWidgetCreatedbroadcasts with the widget.OnDialogueStartedbroadcasts.- The first node runs, which produces the first
OnDialogueUpdated.
The handover event lands after the placement decision and before OnDialogueStarted, so a handler always sees the widget's final placement and can parent it before any content event arrives. GetDialogueWidget() returns the same widget from that point until dialogue end, and returns nullptr otherwise. Every dialogue creates a fresh widget, so ask again rather than caching one across dialogues.
// bAutoAddWidgetToViewport is false: the game decides where the widget lives
void AMyHUDActor::BeginPlay()
{
Super::BeginPlay();
if (UStoryFlowComponent* StoryFlow = FindComponentByClass<UStoryFlowComponent>())
{
StoryFlow->OnDialogueWidgetCreated.AddDynamic(
this, &AMyHUDActor::HandleDialogueWidgetCreated);
StoryFlow->OnDialogueEnded.AddDynamic(
this, &AMyHUDActor::HandleDialogueEnded);
}
}
// Handler signatures - both must be UFUNCTIONs for AddDynamic
void AMyHUDActor::HandleDialogueWidgetCreated(UStoryFlowDialogueWidget* Widget)
{
// Parenting it is what keeps it alive: the component's reference is the
// only other one, and it drops that at dialogue end
DialogueSlot->AddChild(Widget);
CurrentDialogueWidget = Widget;
}
void AMyHUDActor::HandleDialogueEnded()
{
// The component detaches this widget and drops its reference right after
// this event returns, so tearing it down is yours - remove it here, or
// start a fade and remove it when that finishes
if (CurrentDialogueWidget)
{
CurrentDialogueWidget->RemoveFromParent();
CurrentDialogueWidget = nullptr;
}
} How a dialogue ends:
Reaching an End node, calling StopDialogue and the owning actor ending play all run the same teardown. The component broadcasts OnScriptEnded, then OnDialogueEnded, and only then releases the widget: RemoveFromParent() when it placed the widget itself, or DetachFromComponent() when your game owns placement. Either way the widget still receives OnDialogueEnded before it is released.
What detaching does:
DetachFromComponent() unsubscribes the widget from OnDialogueStarted, OnDialogueUpdated, OnDialogueEnded and OnVariableChanged, then clears its component reference. That matters for a widget that is still fading out: without it, that widget would receive the next dialogue's OnDialogueStarted, replay its show animation and repaint itself with the new lines. After detaching, GetStoryFlowComponent() returns nullptr, so SelectOption and AdvanceDialogue do nothing, GetCurrentDialogueState returns an empty state, IsDialogueActive returns false and GetLocalizedString returns the key unchanged. A fade out that still needs to talk to the component has to use your own reference. Call InitializeWithComponent again to reattach.
An Unparented Widget Is Collected
The component's reference is the only one it keeps. Once it lets a widget go, that widget is garbage collected unless your game parented it into a widget tree or holds a reference of its own. With bAutoAddWidgetToViewport off, parent or store the widget inside your OnDialogueWidgetCreated handler - that is what keeps it alive.
A Restart Fires No OnDialogueEnded
Calling StartDialogue while an earlier widget still exists is a restart, not an end. With bAutoAddWidgetToViewport off, the component detaches the old widget and drops its reference without broadcasting OnDialogueEnded, because the dialogue restarted rather than ended. Your cue in that mode is OnDialogueWidgetCreated arriving with a different widget, so compare it against the one you are holding before you replace it.
A Player Controller Is Required
The component creates the widget against the world's first player controller. If there is none when dialogue starts, no widget is created and OnDialogueWidgetCreated never fires. If a dialogue runs correctly but nothing appears on screen, check that first.
Example Widgets
The plugin ships two example Widget Blueprints in StoryFlow Content → Examples. Both extend UStoryFlowDialogueWidget and can be assigned directly to DialogueWidgetClass for a working dialogue UI out of the box, or duplicated and customized for your project.
WBP_Dialogue (standard)
The default dialogue widget. Renders the speaker's portrait inside the dialogue panel alongside the name, body text, and option buttons. Suited for traditional bottom-of-screen dialogue boxes and RPG-style conversations.
Behavior:
- Panel anchored to the bottom of the viewport with the character portrait inside the panel.
- Option buttons are generated dynamically from
State.Options. - Narrative-only nodes (
bCanAdvance == true) auto-advance after a short delay so linear narration flows without requiring a click. Added in v1.0.7 — the legacy version required an explicit "Continue" press. Auto Wrap Textis enabled on the body text block (v1.1.1) so long lines wrap cleanly regardless of viewport width.
WBP_Dialogue_Portrait
Added in v1.1.0. An alternative layout with the character portrait detached from the dialogue panel — rendered as a large image anchored near the bottom-center of the screen, independent of the text panel. The dialogue panel sits below the portrait and contains only the speaker name, body text, and options. Suited for visual-novel-style presentations where the character's full body or half-body portrait should dominate the scene.
Behavior:
- Portrait is a separate
UImagebound toState.Character.Imageand sized to a fixed fraction of the viewport height. - Dialogue panel below the portrait contains the name, body text, and options — same bindings as
WBP_Dialogue. - Portrait is hidden automatically when the current character has no
Imageassigned (e.g. for narrator nodes). - Auto-advance and text wrapping behavior match
WBP_Dialogue.
Choosing a Layout
Use WBP_Dialogue for RPG and adventure-game layouts where the portrait is a small element next to the text. Use WBP_Dialogue_Portrait for visual-novel-style presentations where the character should dominate the screen. Both are equivalent in terms of data binding — you can switch by just changing DialogueWidgetClass on the component; no script or variable changes required.
Asset Locations
Both widgets live in StoryFlow Content → Examples → WBP_Dialogue and WBP_Dialogue_Portrait. Enable Show Plugin Content in the Content Browser view options if you don't see them. To customize, right-click and choose Asset Actions → Duplicate into your own content folder, then assign the duplicate to DialogueWidgetClass.
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 OnDialogueUpdated delegate fires again with a new FStoryFlowDialogueState containing the freshly interpolated text. Your UI code does not need to handle this specially - simply rebuild the display every time OnDialogueUpdated 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.
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 TextBlocks array of FStoryFlowDialogueState.
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.
void UMyDialogueWidget::DisplayTextBlocks(
const TArray<FStoryFlowDialogueOption>& TextBlocks)
{
TextBlocksContainer->ClearChildren();
for (const FStoryFlowDialogueOption& Block : TextBlocks)
{
UTextBlock* TextWidget = NewObject<UTextBlock>(TextBlocksContainer);
TextWidget->SetText(FText::FromString(Block.Text));
TextBlocksContainer->AddChild(TextWidget);
}
}
Display text blocks in your UI below the main dialogue text, or in a separate panel depending on your design. Since they share the same FStoryFlowDialogueOption type as options, you can use a consistent rendering approach for both, differentiating only by interactivity.