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 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().

C++
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;

    // 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;
};

The Character field uses the FStoryFlowCharacterData struct:

C++
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

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++:

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 automatically creates an instance of your widget and manages its lifecycle.

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. Add the widget to viewport or make it visible.
  • OnDialogueEnded() — Called when dialogue finishes. Remove the widget from viewport or hide it.

Built-in helper functions:

  • SelectOption(FString OptionId) — Submit a player choice. Pass the Id from an FStoryFlowDialogueOption.
  • AdvanceDialogue() — Advance past a narrative-only dialogue (when bCanAdvance is true).
  • GetCurrentDialogueState() — Returns the current FStoryFlowDialogueState at any time.
  • IsDialogueActive() — Returns whether a dialogue is currently running.
  • GetLocalizedString(FString Key) — Look up a localized string from the project's string tables.
C++
// 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()
{
    AddToViewport();
}

void UMyDialogueWidget::OnDialogueEnded_Implementation()
{
    RemoveFromParent();
}

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.

C++
// 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 Title and Text in text widgets for the dialogue content
  • Display Character.Name and Character.Image for speaker identification
  • Iterate the Options array to create choice buttons, and call SelectOption(Option.Id) when a button is clicked
  • Show a "Continue" button when bCanAdvance is true, and call AdvanceDialogue() on click
  • Display Image in an Image widget 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 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.

C++
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.

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