toamig.com / Plugins / StoryGraph Get on Fab

StoryGraph

A node-based story graph system for Unreal Engine 5. Author branching dialogue and narrative flows in a visual graph editor, then play them back at runtime via a simple component API, no C++ required for designers.

v0.1

Overview

StoryGraph is a node-based dialogue and narrative system for Unreal Engine 5. Stories are authored as visual graphs in a dedicated asset editor, designers connect Dialogue, Choice, Condition, Event, and Jump nodes to build branching conversations without writing code. At runtime, a single UStoryGraphComponent drives playback through a delegate-based API: your UI binds to events, advances through lines, and presents choices, StoryGraph handles traversal state.

The runtime graph is compiled from the editor graph at save time and stored in a UStoryGraphAsset. Each PlayStory() call clones the compiled data so multiple components can run the same asset simultaneously without sharing state. Speaker metadata, portrait, voice, and name color, lives in reusable UStoryGraphSpeakerDataAsset data assets loaded on demand.

Visual Graph Editor

Author branching dialogue entirely in the graph editor. Connect node types to build conversations with choices, conditions, events, and loops, no code required for designers.

Delegate-Based Playback

Bind OnDialogueLine, OnChoicesPresented, OnEventFired, and OnStoryComplete to drive your UI. StoryGraph handles all traversal, your code only reacts to events.

Data Asset Workflow

Stories are UStoryGraphAsset data assets. Speaker portraits, voice, and name colors live in reusable UStoryGraphSpeakerDataAsset assets loaded as soft references.

Isolated Playback

Each PlayStory() call clones the compiled runtime, so multiple NPCs can run the same story asset simultaneously without interfering with each other's traversal state.

Game-Driven Conditions

Condition nodes fire a named event to your game code. You evaluate the condition and call SelectChoice(0) for true or SelectChoice(1) for false, the graph branches accordingly.

Jump Loops

Jump nodes resolve to any labeled node at compile time, use them to loop back to earlier dialogue, implement repeatable NPC lines, or redirect branches across distant graph regions.

Getting Started

A story running on an NPC is up in five steps. No C++ is required beyond adding the module dependency if you're using Blueprints.

1

Enable the plugin

Open Edit → Plugins, search for StoryGraph, enable it, and restart the editor.

2

Create a Story Graph asset

Right-click in the Content Browser and select StoryGraph → Story Graph. Double-click the asset to open the graph editor. Add a Dialogue node as your starting line and connect it to a Choice node or more dialogue.

3

Create a Speaker asset

Right-click and select StoryGraph → Speaker. Set DisplayName and optionally a Portrait texture. Assign this speaker asset to Dialogue nodes in your graph.

4

Add UStoryGraphComponent to your NPC

In your NPC Blueprint or C++, add a StoryGraphComponent. In BeginPlay, bind the delegates you need: at minimum OnDialogueLine and OnStoryComplete. For branching stories, also bind OnChoicesPresented.

5

Start the story

Call PlayStory(StoryAsset) to begin. When OnDialogueLine fires, display the text. On player input, call AdvanceToNextNode(). When OnChoicesPresented fires, show the choices and call SelectChoice(Index) with the player's pick.

Minimal C++ wiring

C++
// In your NPC .h
UPROPERTY(VisibleAnywhere)
TObjectPtr<UStoryGraphComponent> StoryComp;

UFUNCTION()
void OnDialogueLine(const FText& DialogueText, const FString& SpeakerName);

UFUNCTION()
void OnStoryComplete();
C++
// In BeginPlay
StoryComp->OnDialogueLine.AddDynamic(this, &AMyNPC::OnDialogueLine);
StoryComp->OnStoryComplete.AddDynamic(this, &AMyNPC::OnStoryComplete);

// When the player interacts with the NPC
StoryComp->PlayStory(StoryAsset);

Story Graph Asset

UStoryGraphAsset is the top-level data asset for a story. It stores the visual editor graph (EditorGraph) and the compiled runtime data (RuntimeGraph). The runtime graph is compiled from the editor graph each time you save the asset, you never author RuntimeGraph directly. Assign a UStoryGraphAsset reference to any actor that needs to play it, then pass it to UStoryGraphComponent::PlayStory().

PropertyTypeDescription
RuntimeGraph UStoryGraphRuntime* Compiled runtime data. Written by the editor on save. Read by UStoryGraphComponent on PlayStory().
EditorGraph UEdGraph* (editor only) Visual graph shown in the graph editor. Stripped from non-editor builds.
Save the asset after editing the graph to trigger a recompile. RuntimeGraph will be null until the asset has been saved at least once.

Node Types

StoryGraph provides five node types that cover all branching dialogue patterns. Every node inherits from UStoryGraphNode, which exposes a NodeLabel property, an optional name used by Jump nodes to locate their target at compile time.

Dialogue

A single spoken line. At runtime, UStoryGraphRuntime fires OnDialogueLine with the line text and speaker name, then waits for AdvanceToNextNode(). Assign a Speaker asset to identify who is speaking; the speaker's display name, portrait, and voice are loaded on demand from the soft reference.

PropertyTypeDescription
DialogueText FText The line of dialogue to display or speak.
Speaker TSoftObjectPtr<UStoryGraphSpeakerDataAsset> The character delivering this line. Resolved at runtime via GetCurrentSpeakerAsset().

Choice

Presents the player with multiple branches. Each entry in the Choices array produces one output pin on the node and one entry in the OnChoicesPresented delegate payload. Call SelectChoice(index) with the player's pick to continue traversal. Index maps directly to the output pin order.

PropertyTypeDescription
Choices TArray<FStoryChoiceOption> Each entry has a ChoiceText field shown in your choice UI. Adding or removing entries updates the node's output pins automatically.

Condition

A two-branch gate evaluated by your game code. At runtime, OnConditionRequested fires with the ConditionName. Your handler evaluates the named condition against game state, then calls SelectChoice(0) for true or SelectChoice(1) for false. The graph advances on the matching branch.

PropertyTypeDescription
ConditionName FName Identifier sent to OnConditionRequested. Use descriptive names like "HasKey" or "QuestComplete".

Event

Fires a named event and advances automatically, no player input needed. Use it to trigger side effects mid-conversation: give an item, start an animation, set a quest flag, or play a sound. At runtime, OnEventFired fires with the EventName, then the graph continues to the next node without waiting.

PropertyTypeDescription
EventName FName Identifier sent to OnEventFired. Use descriptive names like "GiveQuestItem" or "PlayCutscene".

Jump

Jumps unconditionally to another node identified by its NodeLabel. Has no output pin, the target is resolved at compile time. Use Jump nodes to create loops (e.g. repeat a greeting), merge converging branches, or redirect flow across distant graph regions. Set NodeLabel on the destination node and TargetLabel on the Jump node to the same value, then save to compile.

PropertyTypeDescription
TargetLabel FName Must match the NodeLabel of the destination node. Resolved at compile time; unresolved jumps are flagged as errors in the editor.

Speaker Data Asset

UStoryGraphSpeakerDataAsset defines a character that speaks in a story graph. Create one asset per NPC or player character and assign it to Dialogue nodes in the graph editor. All media properties use soft references, portrait textures and voice audio are only loaded into memory when that speaker is actively displayed.

PropertyTypeDescription
DisplayName FText The name shown in the dialogue UI name plate.
Portrait TSoftObjectPtr<UTexture2D> Portrait texture displayed during this speaker's lines. Loaded on demand and released when the speaker is no longer active.
VoiceSound TSoftObjectPtr<USoundBase> Optional audio played when this speaker's line is shown. Null means no audio plays. Loaded on demand.
NameColor FLinearColor Tint applied to the speaker's name in the dialogue UI. Defaults to white.

Retrieve the current speaker asset during playback via UStoryGraphComponent::GetCurrentSpeakerAsset(). The returned soft reference can be async-loaded to access portrait and voice without blocking the game thread.

Story Graph Component

UStoryGraphComponent is the primary runtime interface. Add it to any actor, an NPC, player controller, or trigger volume. It wraps UStoryGraphRuntime, clones the compiled graph on each PlayStory() call, and routes runtime events to its own delegates. Bind your UI and game logic to these delegates in BeginPlay; then control playback with the four core functions.

Playback API

FunctionDescription
PlayStory(UStoryGraphAsset*) Begin playing the given story asset. Clones the compiled runtime for isolated playback. If a story is already active it is ended first.
AdvanceToNextNode() Move linearly to the next node (first output branch). Use this to step through Dialogue nodes on player input.
SelectChoice(int32 ChoiceIndex) Branch to a specific output. For Choice nodes, pass the player's selection index. For Condition nodes, pass 0 for true and 1 for false.
EndStory() Immediately terminate the active story and fire OnStoryComplete. Use for skip or interrupt flows.

State queries

FunctionReturnsDescription
IsPlaying() bool True if a story is currently active.
HasChoices() bool True if the current node is a Choice node.
IsWaitingForCondition() bool True if the current node is a Condition node waiting for game code to call SelectChoice.
GetChoiceCount() int32 Number of output branches on the current node.
GetChoiceTexts() TArray<FText> Display labels for the current Choice node's branches.
GetCurrentDialogueText() FText Dialogue text of the current node.
GetCurrentSpeakerName() FString Speaker display name of the current node, compiled from the speaker asset.
GetCurrentSpeakerAsset() TSoftObjectPtr<UStoryGraphSpeakerDataAsset> Soft reference to the current speaker asset. Use this to async-load the portrait and voice for your UI.

Delegates

All runtime events are surfaced as BlueprintAssignable dynamic multicast delegates on UStoryGraphComponent. Bind them in BeginPlay, Blueprint or C++, and react to the events your UI and game logic need. The graph waits at Dialogue and Choice nodes until your code calls AdvanceToNextNode() or SelectChoice(); Event and Jump nodes advance automatically.

DelegateParametersWhen it fires
OnDialogueLine FText DialogueText, FString SpeakerName A Dialogue node is reached. Display the line in your UI, then call AdvanceToNextNode() on player input.
OnChoicesPresented TArray<FText> Choices A Choice node is reached. Show the choices, then call SelectChoice(index) with the player's pick.
OnConditionRequested FName ConditionName A Condition node is reached. Evaluate the condition and call SelectChoice(0) for true, SelectChoice(1) for false.
OnEventFired FName EventName An Event node is reached. React to the named event, give items, start animations, set flags. The graph advances automatically.
OnStoryComplete (none) The graph has no more nodes to process, or EndStory() was called. Close your dialogue UI here.

Condition Handling

Condition nodes bridge the story graph and your game state. The graph fires OnConditionRequested with a named condition, halts, and waits. Your handler reads game state for that condition name and calls SelectChoice to branch. This keeps condition logic in your game code, the graph itself has no knowledge of quests, inventory, or game variables.

C++
// In your NPC header
UFUNCTION()
void HandleCondition(FName ConditionName);

// In BeginPlay
StoryComp->OnConditionRequested.AddDynamic(this, &AMyNPC::HandleCondition);

// Handler, evaluate and branch
void AMyNPC::HandleCondition(FName ConditionName)
{
    bool bResult = false;

    if (ConditionName == "HasKey")
    {
        bResult = PlayerInventory->HasItem(KeyItemDefinition);
    }
    else if (ConditionName == "QuestComplete")
    {
        bResult = QuestManager->IsComplete(FName("MainQuest"));
    }

    // 0 = True branch, 1 = False branch
    StoryComp->SelectChoice(bResult ? 0 : 1);
}
You must call SelectChoice() in your OnConditionRequested handler, the graph will not advance until it receives a branch selection. Failing to call it will leave the story stalled at the Condition node.