Quest System

A node-graph scripting language for narrative flow, built on top of Unreal's Flow Graph. Designers author quests without touching code — and can launch the game at any beat, with everything that came before replayed as world state rather than played out.

Engine
Unreal Engine 5
Role
Design & implementation
Language
C++ (runtime + editor modules)
Users
Narrative & level designers
Authoring
Graph assets, no code

The problem

Quest logic in an open-world game is long, stateful, and touches everything: streaming, spawning, cinematics, the player's position, UI. The usual result is that it ends up half in Level Blueprints and half in an engineer's head, and only that engineer can change it safely.

The harder problem is iteration cost. A beat twenty minutes into a questline can only be reached by playing the twenty minutes before it. When testing a change is that expensive, designers stop testing — they make the change, eyeball the graph, and move on. Bugs get found weeks later by someone else.

So the system had two jobs: make quests authorable by designers, and make any point in the game reachable in seconds.

assets/media/quest-play-from-here.mp4
<video autoplay loop muted playsinline>
Right-click any beat in a quest graph, choose Play From Here, and PIE starts at that beat — data layers loaded, player placed, and every earlier action's end state already applied.

What I built

Three nested authoring layers, all of them graph assets a designer opens and edits directly:

  • Questline — the sequence and branching of quests across the game.
  • Quest — one quest's logic. Owns the data layer it needs loaded and where the player starts.
  • Quest Beat — a chunk of a quest, authored as its own sub-graph so beats stay readable and independently launchable.

Inside a graph, designers work with a small deliberate vocabulary of node types — Action (does something to the world), Condition, Trigger, Route, and Prepared — rather than a flat pile of generic nodes. The constraint is the point: a node's category tells you what it is allowed to do, which is what makes the debug launch below possible at all.

The designer workflow

  1. Create a Quest asset from the content browser and author its graph.
  2. Point the level's World Settings at the questline; drop a Start Quest node in it.
  3. Right-click any Start Quest node or Quest Beat and choose Play From Here.
  4. Iterate. Use Quest.Debug 1 to watch questline progress and live node state on screen.

How it works

Built on Flow Graph, not beside it

Quests and questlines are subclasses of Flow's own UFlowAsset, and the custom nodes subclass Flow's nodes. That decision bought the whole graph editor, node validation, live debugging and asset tooling for free, and it means designers learn one graph paradigm rather than a bespoke one. Where the engine's behaviour was wrong for quests I overrode it rather than forking: a Start Quest node looks and previews like a Sub Graph node, but executes its quest as a root flow instance, which is what makes quest lifetime, console commands and streaming waits behave sensibly.

Prepare and Start: transitions without a hitch

The Start Quest node has two input pins. Prepare loads everything the next quest needs — data layers, assets — without running it, so the previous quest decides when loading begins and can cover it with whatever it is already doing. Once loading finishes, a Prepared node fires inside the incoming quest's own graph, which is where per-quest setup belongs. Start then runs it; if Start arrives while loading is still in flight it waits, so a quest can never begin before its world exists.

The prepare state is persisted, so a save taken mid-transition restores into the same state instead of silently losing an in-flight preparation.

The debug launch, and replaying what was skipped

This is the part that does the real work. Launching at a beat means every action before it never ran — so the NPC that should be waiting was never spawned, the door that should be open was never opened, the flag that gates the next branch was never set. A launch into that world is worthless.

Every Action node therefore answers one question: On Debug Skipped — put the world into the state you would have left behind, and do nothing else. No cinematics, no waiting, no gameplay effects. Implementing it is optional per node and added as the need arises, so the cost is incremental rather than a wall of work up front.

Three rules make the replay trustworthy:

  • Only the path actually taken is replayed. Each skipped node is handed the output pin it fired on the way to the launch point, so a Spawn node reached through its Despawned output leaves the actor gone rather than spawning it.
  • Last write wins. Nodes declare what they act on, and two skipped nodes acting on the same target collapse to the most recent one — spawn an actor early and despawn it later, and the launch leaves it gone.
  • A replayed node cannot fire its outputs. See below.
The constraint that shaped the design. In Flow, triggering an output does not merely mark a pin — it hands execution to whatever is connected. A node "replaying" its end state that also fired an output would run the graph forward for real from a point the launch was explicitly meant to skip, quietly executing and finishing later nodes so that their own replay then fails. So outputs are sealed off for the duration of a replay and refused with a warning. The replay is held to exactly what it claims to be: apply end state, touch nothing else.

Streaming discipline

Each quest owns the data layer that holds its spawners and triggers. A quest does not start when its layers report activated — it starts when streaming reports genuinely complete, because activation and "the actors exist" are not the same moment, and the gap is where players fall through unloaded geometry. The player teleport therefore also lives inside the quest and runs after that wait, not in world settings before it.

Debug surfaces

Quest.Debug 1 puts an overlay on screen: questline progress, which quests are running, and per-node state inside them. Alongside it, Quest.Start, Quest.Finish and Quest.Teleport cover the cases where a designer wants to jump without leaving the running session. All of it is stripped from shipping.

assets/media/quest-debug-hud.png
The Quest.Debug overlay: questline position, running quests, and live node state.

What it changed

  • Testing a late-game beat went from minutes of replay to seconds. Fill in the real before/after number.
  • Quest content stopped needing an engineer. New quests are graph assets; new node types are the only thing that reaches C++.
  • Quest-to-quest transitions became testable in isolation — a debug launch can enter the real questline part-way rather than running a quest standalone, so the seams get tested, not just the middles.

What I would do differently

Honest limitation or two — the thing a lead will ask about anyway, answered before they ask.

← Lumos systems Area Loading →