Narrative Tooling Project
Project Overview
As part of my much larger continuation of Astral Plague, I'll occasionally work on systems or tooling for that project. This is a small glimpse into what is a relatively large system. My goals for this project was to create…
Note: This page focuses only on the parts of the system I authored. I won’t walk through every component of StateTree or explain how Unreal’s underlying tools function, I’ll assume you already have a baseline familiarity with UE5. If not, that’s completely fine, my breakdown should still be easy to follow. Just note that certain details may be less obvious without prior experience with Unreal’s systems.
Background - Why?
Why a Narrative System at all?
For me, narrative is one of the make-or-break elements of a game. If the story lacks depth or fails to engage, the experience falls flat. Worse still is when the player has no real agency—when their choices don’t influence the outcome or their actions don’t leave a mark. At that point, I can’t help but think: if I can’t meaningfully interact with the story, why not just watch a movie instead?
The challenge is that Unreal Engine provides no native narrative tooling. Without a dedicated system, developers often fall back on inefficient solutions, redundant code blocks, centralized “god objects". These patterns work, but they quickly become brittle and hard to scale. What happens if you redo dialog or change how something branches?
My goal was different: I wanted a system that avoided repeated code, minimized reliance on single narrative controllers, and tied story logic directly to events and moments, where it naturally belongs as well as support branching dialog and story paths.
Example of the kind of narrative systems I see online 24/7
Why use StateTrees for a Narrative Framework?
When it comes to tooling, I’m a strong believer in not reinventing the wheel. As the saying goes, “If it isn’t broken, don’t fix it.” My philosophy is to leverage existing systems wherever possible and only build on top of them when it adds real value. That mindset led me to Unreal’s StateTrees.
Epic describes StateTrees as “a general-purpose hierarchical state machine that combines the Selectors from behavior trees with States and Transitions from state machines.” While powerful, most developers online use it almost exclusively for simple AI transitions. I wanted to push it further.
My goal was to extend StateTrees into a framework that could handle narrative dialogue—both linear and branching—while remaining lightweight and designer-friendly. Instead of forcing designers to code dialog logic, they should be able to focus on writing and setting up activation conditions with minimal technical overhead. Additionally, I wanted to minimize true engine rewrites or custom tooling as much as possible. This system should utilize Unreal's code as much as possible.
This drove a few key objectives:
Lightweight extensibility – minimal programming required per dialogue string.
Branching & linear support – flexible handling of both simple sequences and complex choices.
Conditional dialogue – options that dynamically react to game state (e.g., only appearing if conditions are met).
Clean, usable UI – no messy spaghetti branches; clarity and accessibility were a priority.
StateTree, from prior use, clearly satisfied all of these requirements… and then some, so it was an obvious chose over other bespoke options (all of which would've taken more time and money).
Programming - How
Branching Dialog
Linear Dialog is relatively simple, pick a line and set your dialog UI to that value, so I won't cover it at all. Branching dialog, however, is a bit more complex.
Storing Dialog
The first step was deciding how to store dialogue. Dialogue in games is inherently ambiguous—we don’t always know who’s speaking to whom, in what order, or under what conditions. Because of that, the storage method needed to be simple, easy to parse, and quick to search through. Designers shouldn’t have to spend hours hunting for the right line of text to paste into a scene (more on that later).

Struct View
Another requirement was scalability. We’re not just storing a single line, we’re managing hundreds of lines tied to characters, scenes, or quests. That made the most obvious choice a custom dialogue struct, designed to hold all the necessary information in one place. This struct can then be used in a DataTable as a more centralized location for storage.
The core struct included:
Character Name – the speaker’s identifier.
Event (Gameplay Tag) – the specific trigger or condition associated with the line.
Speaking Index – which “slot” the character’s portrait or sprite should appear in.
Dialogue – the actual line of text.
Originally, I packed more data into this struct, including dialogue conditions and transition requirements (for example, StateTreeStateLinks
—more on that later). But over time, I slimmed the struct down to only the essentials, leaving conditional logic to be handled elsewhere. This kept the dialogue asset lightweight, easier to debug, and more maintainable.
Cool we have dialog… now how do we branch?
Branching was one of the trickiest parts to get right. Conceptually, every branch of dialogue needs a state transition (think of it as the player’s response). But there are two requirements:
Transitions cannot be hard-coded. They should be assignable in the editor (Details panel or equivalent).
The system needs to be designer-friendly and data-driven, without requiring manual setup for every branch.
Thankfully, Unreal’s StateTree includes a feature called StateTreeStateLinks
, which lets designers reference states within the tree. The catch? That’s all they do. To actually transition between states, additional blueprint logic was needed.
Problem 1: How does the UI trigger a transition?
The UI lives on the player, while the StateTree exists elsewhere. That means response buttons in the dialogue UI can’t directly “see” or bind logic to the StateTree.
Solution:
Assign an index to each button at creation.
Bind each button’s
OnClick
delegate to our StateTree Task.The Task then looks up the associated dialogue data (based on the index), retrieves the correct
StateTreeStateLink
, and requests the transition.
This way, buttons remain “dumb” UI, while the StateTree handles the logic cleanly.
Problem 2: Where do we store the StateTreeStateLinks?
At first, I tried keeping them directly inside my core S_DialogData
struct. But StateTreeStateLinks
only function when scoped inside a StateTree. That immediately broke the idea of storing dialogue externally (like in a Google Sheet or DataTable), since those assets wouldn’t have access to valid state references.
Solution:
I split responsibilities:
I made a new main
S_BranchingDialog
struct that became the top-level container.Inside it, I stored a
Data Table Row Handle
that pointed to external dialogue lines.It also stored a
StateTreeStateLink
but with no state linked.Inside the StateTree, I assigned both the dialogue table reference and the specific line index.
Then, because the
StateTreeStateLink
had proper visibility to the StateTree, I could assign the transition as needed.
This approach gave me clean separation: dialogue data stays external and scalable, while transitions remain scoped to the StateTree where they belong.
Putting It All Together…
For each branching dialogue option, I’m essentially setting three values:
The table the dialogue comes from.
The line of dialogue to display.
The state to transition to next.
It sounds simple — and conceptually, it is — but under the hood it required careful structuring to avoid redundancy, keep data externalized, and respect StateTree’s scoping rules.

Note: there is a further expansion of this system in progress that support conditional appearance of dialog. It's WIP though so… yeah I'm not showing that yet.
Want to read about how I made a one click importer for the Dialog data tables?