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 class, three approaches for building your dialogue UI, and how variable interpolation works.

The 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. This object is passed to event subscribers and is available on demand via GetCurrentDialogue().

C#
[Serializable]
public class StoryFlowDialogueState
{
    // The ID of the current dialogue node
    public string NodeId;

    // Resolved title text with variable interpolation applied
    public string Title;

    // Resolved dialogue body text with variable interpolation applied
    public string Text;

    // Dialogue image (Sprite) - persists between nodes unless explicitly changed
    public Sprite Image;

    // Audio clip assigned to this dialogue node
    public AudioClip Audio;

    // The speaking character (name, portrait, per-character variables)
    public StoryFlowCharacterData Character;

    // Non-interactive text segments displayed alongside the dialogue
    public List<StoryFlowTextBlock> TextBlocks;

    // Clickable options the player can choose from
    public List<StoryFlowOption> Options;

    // Whether this state represents an active, valid dialogue
    public bool IsValid;

    // True when dialogue has a connected output but no options.
    // Use this to show a "Continue" button.
    public bool CanAdvance;

    // Whether the dialogue audio should loop continuously
    public bool AudioLoop;

    // Whether previous audio should be stopped when this node has no audio
    public bool AudioReset;

    // Whether the dialogue should auto-advance when audio finishes playing
    public bool AudioAdvanceOnEnd;

    // Whether the player can skip past audio-gated advances
    public bool AudioAllowSkip;
}

The Character field uses the StoryFlowCharacterData class:

C#
[Serializable]
public class StoryFlowCharacterData
{
    public string Name;
    public string ImageAssetKey;
    public Sprite Image;
    public Dictionary<string, StoryFlowVariant> Variables;
    public List<StoryFlowVariable> VariablesList;
}

Each entry in Options is a StoryFlowOption:

  • Id (string) - Unique identifier for the option, used when calling SelectOption()
  • Text (string) - Display text with variable interpolation already applied
  • IsOnceOnly (bool) - Whether the option disappears after being selected
  • IsSelected (bool) - Reserved field. Currently not populated by the runtime (always false).
  • InputType (string) - The input type if this is a typed input option (e.g. "string", "integer", "boolean"), or empty for standard buttons
  • DefaultValue (string) - The default value for input options

Each entry in TextBlocks is a StoryFlowTextBlock:

  • Id (string) - Unique identifier for the text block
  • Text (string) - 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.

Building Your UI

There are three approaches to building your dialogue UI in Unity, ranging from zero code to full custom control.

Using the Default UI

If no DialogueUI is assigned to the StoryFlowComponent, a built-in fallback UI is automatically created at runtime. This means you can start testing dialogue immediately without any UI setup at all - just add a StoryFlowComponent, assign a script, and call StartDialogue().

The auto-created fallback UI renders dialogue text, character names, option buttons, and a continue button using a basic uGUI layout. It is intended for rapid prototyping and testing.

Explicit Default UI

You can also use StoryFlowDefaultDialogueUI explicitly if you want more control over the layout. This is a uGUI-based MonoBehaviour that extends StoryFlowDialogueUI and lets you wire up your own panel, text fields, and button prefabs in the Inspector. Drag it into the DialogueUI field on your StoryFlowComponent to use it instead of the auto-created fallback.

StoryFlowDefaultDialogueUI Inspector references:

  • dialoguePanel - Root GameObject that is shown/hidden when dialogue starts/ends
  • titleText - TextMeshProUGUI for the dialogue title (optional)
  • bodyText - TextMeshProUGUI for the dialogue body text
  • characterNameText - TextMeshProUGUI for the speaker's name (optional)
  • characterPortrait - Image for the speaker's portrait (optional)
  • dialogueImage - Image for the dialogue-specific image (optional)
  • backgroundImage - Image for the persistent background image (optional)
  • optionsContainer - Transform where option buttons are spawned as children
  • optionButtonPrefab - GameObject prefab with a Button component and a TextMeshProUGUI child
  • continueButton - Button shown when dialogue has no options (calls AdvanceDialogue())

Prototype Only

Both the auto-created fallback and StoryFlowDefaultDialogueUI are intended as quick-start prototypes. For production games, extend StoryFlowDialogueUI with your own visuals, animations, and layout (see below).

Custom UI with Base Class

The plugin provides StoryFlowDialogueUI, an abstract MonoBehaviour base class designed to be extended. When you assign your subclass to the component's DialogueUI field, it automatically subscribes to dialogue events and manages the binding lifecycle.

Override these methods to build your UI:

  • HandleDialogueUpdated(StoryFlowDialogueState state) - Called every time the dialogue state changes. Rebuild your text, options, and character display here.
  • OnDialogueStarted() - Called when dialogue execution begins. Show your panel or play an intro animation.
  • OnDialogueEnded() - Called when dialogue finishes. Hide your panel or play an outro animation.
  • OnVariableChanged(StoryFlowVariable variable, bool isGlobal) - Called when a variable changes. Useful for updating HUD elements.
  • OnBackgroundImageChanged(Sprite backgroundImage) - Called when a SetBackgroundImage node fires.

Built-in convenience methods (call from your subclass):

  • SelectOption(string optionId) - Submit a player choice to the component.
  • AdvanceDialogue() - Advance past a narrative-only dialogue (when CanAdvance is true).
  • InputChanged(string optionId, string value) - Send a typed input value change.
  • GetCurrentDialogue() - Returns the current StoryFlowDialogueState at any time.
  • IsDialogueActive() - Returns whether a dialogue is currently running.
  • GetLocalizedString(string key) - Look up a localized string from the project's string tables.
C#
using StoryFlow.Data;
using StoryFlow.UI;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class MyDialogueUI : StoryFlowDialogueUI
{
    [SerializeField] private GameObject dialoguePanel;
    [SerializeField] private TextMeshProUGUI titleText;
    [SerializeField] private TextMeshProUGUI bodyText;
    [SerializeField] private TextMeshProUGUI characterNameText;
    [SerializeField] private Image characterPortrait;
    [SerializeField] private Transform optionsContainer;
    [SerializeField] private GameObject optionButtonPrefab;
    [SerializeField] private Button continueButton;

    private void Awake()
    {
        dialoguePanel.SetActive(false);
        continueButton.onClick.AddListener(() => AdvanceDialogue());
    }

    public override void OnDialogueStarted()
    {
        dialoguePanel.SetActive(true);
    }

    public override void HandleDialogueUpdated(StoryFlowDialogueState state)
    {
        // Display title and body text
        titleText.gameObject.SetActive(!string.IsNullOrEmpty(state.Title));
        titleText.text = state.Title ?? "";
        bodyText.text = state.Text ?? "";

        // Display character info
        bool hasCharacter = state.Character != null
            && !string.IsNullOrEmpty(state.Character.Name);
        characterNameText.gameObject.SetActive(hasCharacter);
        if (hasCharacter)
            characterNameText.text = state.Character.Name;

        if (characterPortrait != null)
        {
            bool hasPortrait = hasCharacter && state.Character.Image != null;
            characterPortrait.gameObject.SetActive(hasPortrait);
            if (hasPortrait)
                characterPortrait.sprite = state.Character.Image;
        }

        // Show continue button or choice options
        continueButton.gameObject.SetActive(
            state.CanAdvance && state.Options.Count == 0);

        // Clear and rebuild option buttons
        foreach (Transform child in optionsContainer)
            Destroy(child.gameObject);

        foreach (var option in state.Options)
        {
            var buttonObj = Instantiate(optionButtonPrefab, optionsContainer);
            var label = buttonObj.GetComponentInChildren<TextMeshProUGUI>();
            label.text = option.Text;

            string capturedId = option.Id;
            buttonObj.GetComponent<Button>().onClick.AddListener(
                () => SelectOption(capturedId));
        }
    }

    public override void OnDialogueEnded()
    {
        dialoguePanel.SetActive(false);
    }
}

Custom UI with Events

If you need full control or want to integrate StoryFlow with an existing UI framework, skip the base class entirely and subscribe directly to the component's C# events.

C#
using StoryFlow;
using StoryFlow.Data;
using UnityEngine;

public class DialogueUIController : MonoBehaviour
{
    [SerializeField] private StoryFlowComponent storyFlow;
    [SerializeField] private MyCustomDialoguePanel dialoguePanel;

    private void OnEnable()
    {
        storyFlow.OnDialogueStarted += OnDialogueStarted;
        storyFlow.OnDialogueUpdated += OnDialogueUpdated;
        storyFlow.OnDialogueEnded += OnDialogueEnded;
    }

    private void OnDisable()
    {
        storyFlow.OnDialogueStarted -= OnDialogueStarted;
        storyFlow.OnDialogueUpdated -= OnDialogueUpdated;
        storyFlow.OnDialogueEnded -= OnDialogueEnded;
    }

    private void OnDialogueStarted()
    {
        dialoguePanel.Show();
    }

    private void OnDialogueUpdated(StoryFlowDialogueState state)
    {
        // Manually update your panel
        dialoguePanel.SetTitle(state.Title);
        dialoguePanel.SetBody(state.Text);
        dialoguePanel.SetCharacter(state.Character?.Name, state.Character?.Image);
        dialoguePanel.SetOptions(state.Options);
        dialoguePanel.SetContinueVisible(
            state.CanAdvance && state.Options.Count == 0);
    }

    private void OnDialogueEnded()
    {
        dialoguePanel.Hide();
    }

    // Called by your UI when the player clicks an option button
    public void HandleOptionSelected(string optionId)
    {
        storyFlow.SelectOption(optionId);
    }

    // Called by your UI when the player clicks "Continue"
    public void HandleContinue()
    {
        storyFlow.AdvanceDialogue();
    }
}

Key points for any approach:

  • Display Title and Text for the dialogue content
  • Display Character.Name and Character.Image for speaker identification
  • Iterate the Options list to create choice buttons, and call SelectOption(option.Id) when a button is clicked
  • Show a "Continue" button when CanAdvance is true and Options is empty, and call AdvanceDialogue() on click
  • Display Image for scene or background 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 event 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 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 list 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.

C#
// Display text blocks in your dialogue UI
private void DisplayTextBlocks(List<StoryFlowTextBlock> textBlocks)
{
    // Clear previous text blocks
    foreach (Transform child in textBlocksContainer)
        Destroy(child.gameObject);

    foreach (var block in textBlocks)
    {
        var textObj = Instantiate(textBlockPrefab, textBlocksContainer);
        var label = textObj.GetComponentInChildren<TextMeshProUGUI>();
        label.text = block.Text;
    }
}

Display text blocks in your UI below the main dialogue text, or in a separate panel depending on your design. Since text blocks share a similar structure to options (both have an Id and Text), 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