Unreal Engine integration
Description: Export a Narratyr project into an Unreal Engine project and drive dialogues and quests from C++ or Blueprint.
Prerelease. The Unreal integration ships with Narratyr rather than from the Marketplace — the exporter writes the plugin into your project for you. Details here may change before release.
The Unreal exporter writes two things into your game project: a plugin that never changes between projects, and generated C++ and data that mirrors your Narratyr document. Your game code talks to a handful of subsystems from the plugin and to the generated classes named after your own entity types, functions, and quests.
Exporting covers the idea in general terms. This page is the Unreal specifics: where to point the exporter, what each button does, what lands on disk, and what you have to implement before a dialogue will actually run.
Before you start
- Unreal Engine 5, with a C++ project. The exporter writes a project module and
registers it in your
.uproject, so a Blueprint-only project needs to be converted to C++ first (adding any C++ class in the editor does this). You do not need to rebuild the engine — the plugin is a normal project plugin. - A compiler toolchain — the same one you would use to build any C++ Unreal project. Installing the module is a code change and needs a build.
- Your Narratyr project, saved. The exporter exports the document as it currently is in the editor.
Setting the export path
Open the Export view in Narratyr and pick the Unreal tab. The one setting that matters is the project path.
Point it at the folder that contains your .uproject file — the Unreal project root,
not Content/, not Source/, and not the folder above it. Everything the exporter writes
is placed relative to that folder:
MyGame/
MyGame.uproject <-- the exporter looks for this
Config/
Content/
Source/
So for a project at D:/Games/MyGame/MyGame.uproject, the export path is
D:/Games/MyGame.
If the exporter cannot find a .uproject in the folder you chose, it still writes the
files but warns that it could not register the modules — which is the usual sign the path
is one level off.
The path is remembered per Narratyr project and per engine, so you set it once.

Install Module vs Export Data
The two buttons do different jobs and run on different schedules. This is the single most useful thing to understand about the exporter.
| Install Module | Export Data | |
|---|---|---|
| Writes | C++ source and the plugin | Data files under Content/ |
| Needs a recompile | Yes | No |
| Run it when | Enumerations, property sets, entity types, functions, or quest graphs are added, renamed, or removed — and once at the start | Any time content changes: dialogue text, quest structure, entity values, variables |
| Typical frequency | Occasionally | Constantly |
Install Module writes code. It is the schema half of the export: your enumerations
become UENUMs, your property sets become USTRUCTs, your entity types become DataTable
row types, your function definitions become virtual methods. Changing any of those shapes
means new C++, which means Unreal has to build it.
Export Data writes content. It is the values half: the actual entities, the actual variable defaults, the actual graphs. It never changes a type, so it never needs a build — the plugin's importer picks the files up and updates the assets in place.
Rule of thumb. If you changed what shape the data has, install the module. If you changed what the data says, export data.
What Install Module does
- Writes the plugin to
Plugins/NarratyrRuntime/. - Writes generated per-project code to
Source/NarratyrGameData/. - Registers both in your
.uproject—NarratyrRuntimeinPlugins,NarratyrGameDatainModules. - Writes a managed block into
Config/DefaultEditorPerProjectUserSettings.inithat excludesContent/NarratyrDatafrom Unreal's own auto-reimport watcher, leaving the plugin's importer as the only thing importing those files. The block is fenced with comment markers and rewritten in place, so your own settings in that file are left alone.
Then close the editor if it is open, build, and reopen. New UENUMs and USTRUCTs will
not appear in Blueprint until the module is compiled and loaded.
What Export Data does
Writes source files into Content/NarratyrData/ plus a translator handoff folder outside
Content/. The plugin watches Content/NarratyrData/ and imports anything that changed
into the matching Unreal asset, hashing contents so an unchanged file does not dirty its
.uasset. With the editor open you can usually just alt-tab back and the assets are
already current.
Clean up items removed from the project (the checkbox) deletes files under
Content/NarratyrData/ that this export did not write — orphans left behind by entities,
graphs, or assets you have since deleted in Narratyr. Leave it on unless you are
deliberately hand-managing that folder. Deleting the source file is what lets the plugin
retire the matching asset; reinstall the module once afterwards so it picks the deletion up.
What lands in your project
MyGame/
MyGame.uproject Modules + Plugins entries added
Config/
DefaultEditorPerProjectUserSettings.ini auto-reimport exclusion (managed block)
Plugins/
NarratyrRuntime/ the plugin — same in every project
Source/NarratyrData/ runtime module
Source/NarratyrDataEditor/ editor module (importers, debug tooling)
Source/
NarratyrGameData/ generated from YOUR document
Public/NarratyrEnums.h your enumerations
Public/NarratyrPropertySets.h your property sets
Public/NarratyrEntityTypes.h your entity types, as DataTable row structs
Public/NarratyrFunctionHost.h one method per function you declared
Public/NarratyrEntityRegistry.h typed lookups for your entity types
Public/NarratyrQuestIds.h an enum of your quests, for Blueprint pins
Content/
NarratyrData/ written by Export Data, imported by the plugin
Entities/ DT_<EntityType> one DataTable per entity type
Variables/ DT_Variables
Quests/ DT_Quests
Graphs/ one asset per dialogue / quest graph
Localization/ST_NarratyrStrings
UI/ rich text styles
Assets/ images and audio, mirroring your Narratyr asset folders
GraphRegistry id -> graph asset lookup
NarratyrLocalization/ CSV + XLIFF for translators (outside Content, never imported)
Once the plugin has imported them, the same folders show up in the Content Browser under Content/NarratyrData, holding real Unreal assets — DataTables, graph data assets, a string table, and the graph registry:

Everything under Plugins/NarratyrRuntime/, Source/NarratyrGameData/, and
Content/NarratyrData/ is generated and overwritten on every export. Put your own code
in your own module and subclass what you need — never edit the generated files in place.
Why the split
The plugin is fixed code: it knows how to walk a graph, evaluate an expression, hold
variable state, and import a .rpggraph file. It knows nothing about your game's
particular enumerations or entity types.
The generated module is the part that does. It turns your authored schema into real C++
types, so an entity's Faction property is a genuine UENUM pin in Blueprint rather than
a string you compare by hand, and a function you declared as
hasInventoryItem(itemId: string) -> boolean is a method with that signature waiting to
be overridden.
The generated module depends on the plugin. Your game module depends on both — add
NarratyrData and NarratyrGameData to your module's PublicDependencyModuleNames:
PublicDependencyModuleNames.AddRange(new string[] {
"Core", "CoreUObject", "Engine", "InputCore",
"NarratyrData", "NarratyrGameData"
});
The classes you will actually use
| Class | Kind | What it is for |
|---|---|---|
UNarratyrVariableManager |
Game instance subsystem | Read and write Narratyr variables; register your function host; interpolate @{Var} in text |
UNarratyrQuestManager |
Game instance subsystem | Start, complete, and query quests; the delegates most games bind to |
UNarratyrQuestInstance |
Object, one per quest | Live state of one quest: objectives, flow players — mostly for UI binding |
UNarratyrFlowPlayer |
Object | Walks a graph. Presents dialogue, exposes choices, fires the node delegates |
UNarratyrGraphAsset |
Data asset | One imported dialogue or quest graph |
UNarratyrGraphRegistry |
Data asset | Graph id to graph asset, for resolving graphs by id |
UNarratyrEntityRegistry |
Game instance subsystem (generated) | Look up entities by id — display names, portraits, typed rows |
UNarratyrFunctionHost |
Object (generated) | Subclass this to implement your functions |
UNarratyrFieldValueLibrary |
Blueprint library | Read a node's or graph's property values by name |
UNarratyrSaveLibrary |
Blueprint library | Capture and restore the whole Narratyr state |
ENarratyrQuestId + UNarratyrQuestIdLibrary |
Enum + library (generated) | Quest dropdown pins in Blueprint instead of typed ids |
Everything is BlueprintType with Narratyr|… categories, so the same API is available
from Blueprint under Narratyr in the node palette.
UNarratyrVariableManager
Owns the runtime value of every variable you declared in
Variables. Loads DT_Variables on startup.
UNarratyrVariableManager* Variables =
GetGameInstance()->GetSubsystem<UNarratyrVariableManager>();
Variables->SetVariableInt(TEXT("BoarsKilled"), 5);
const bool bMet = Variables->GetVariableBool(TEXT("MetTheJarl"));
Beyond typed get/set it gives you:
- Lists and sets —
AddToVariableList*,RemoveFromVariableList*,VariableListContains*,GetVariableItemCount, plus wildcard nodes (Get Variable As List) whose pin adopts whatever array type you connect, including your ownUENUMs. OnVariableChanged— broadcast on every successful write. This is also what wakes up gates and event listeners.InterpolateVariables— substitutes@{VarName}placeholders in dialogue text. Call it on any line you are about to display; see Text directives.SetFunctions— where you register your function host (below).ResetToDefaults— reload from the DataTable, for starting a new game.
UNarratyrQuestManager
A game instance subsystem that loads DT_Quests, builds a UNarratyrQuestInstance per
quest, and owns quest lifecycle.
UNarratyrQuestManager* Quests = GetGameInstance()->GetSubsystem<UNarratyrQuestManager>();
Quests->DiscoverQuest(TEXT("down_with_the_jarl")); // known but not running
Quests->StartQuest(TEXT("down_with_the_jarl")); // runs the quest graph
Quests->CompleteObjective(TEXT("down_with_the_jarl"), TEXT("kill_the_jarl"));
Queries worth knowing: GetActiveQuests, GetQuestsByState, GetQuestDefinition (title,
description, and objectives as localized text), GetQuestPrerequisites, and
AreQuestStartRequirementsSatisfied.
Objectives do not complete themselves. The runtime activates and tracks them, but
nothing in a graph knows that the player killed five boars — your game calls
CompleteObjective (or passes Failed / NotApplicable) when the gameplay condition is
met. The graph reacts to that.
In Blueprint, prefer the generated UNarratyrQuestIdLibrary wrappers: they take an
ENarratyrQuestId dropdown showing quest display names rather than an id you have to type.
The enumerator identifiers are the stable ids, so renaming a quest in Narratyr does not
break existing Blueprint references.
UNarratyrFlowPlayer
The thing that actually walks a graph. A quest owns one (plus any concurrent flows it spawns); for a standalone conversation you create your own.
It auto-advances through everything that is not a stopping point and pauses on
Dialogue and Choice nodes. At a stop you call Continue(), Choose(Index), or
Complete() to resume.
// Standalone conversation — e.g. an NPC with a UNarratyrGraphAsset* property.
Flow = NewObject<UNarratyrFlowPlayer>(this); // keep this in a UPROPERTY!
Flow->Initialize(GetGameInstance());
Flow->OnDialogueEntered.AddDynamic(this, &AMyNPC::HandleLine);
Flow->OnFlowEnded.AddDynamic(this, &AMyNPC::HandleConversationEnded);
Flow->Play(ConversationGraph);
Hold a reference. A flow player you create is a plain UObject. Store it in a
UPROPERTY() or it will be garbage collected mid-conversation.
Play with no start node resolves the entry point itself — the graph's single
Dialogue Entry node for a dialogue, its single Quest node for a quest.
To resolve a graph by id instead of referencing the asset, load the registry:
UNarratyrGraphRegistry* Registry = LoadObject<UNarratyrGraphRegistry>(
nullptr, UNarratyrGraphRegistry::DefaultAssetPath);
UNarratyrGraphAsset* Graph = Registry->ResolveGraph(TEXT("tavern_intro"));
UNarratyrEntityRegistry
Generated, because it knows your entity types. Indexes every exported entity by its stable
Narratyr id — which is what a dialogue node's SpeakerEntity and every entity reference
field hold.
UNarratyrEntityRegistry* Entities =
GetGameInstance()->GetSubsystem<UNarratyrEntityRegistry>();
FNarratyrEntityInfo Info;
if (Entities->FindEntity(Node.SpeakerEntity, Info))
{
// Info.DisplayName, Info.ProfileImage, Info.EntityType, Info.GameId
}
FNarratyrCharacterRow Row; // named after YOUR entity type
Entities->FindCharacter(Node.SpeakerEntity, Row);
FindEntity gives the common subset every entity has — id, display name, profile image,
type. The generated Find<TypeName> accessors give the full typed row, and
FindPropertySet_<Name> pulls out one property set. NarratyrIdToGameId /
GameIdToNarratyrId translate between Narratyr ids and the external ids your game uses.
UNarratyrFieldValueLibrary
Dialogue, hub, and custom nodes can carry authored property values (from the node's entity type), and a graph can carry metadata the same way. Rather than breaking the struct and looping the array, read them by name:
const double Delay = UNarratyrFieldValueLibrary::GetFieldAsNumber(Node.FieldValues, TEXT("Delay"), 0.0);
const uint8 Mood = UNarratyrFieldValueLibrary::GetFieldAsEnumByte(Node.FieldValues, TEXT("Mood"));
There is a GetFieldAs* for each scalar type, list variants, and a wildcard
Get Field As List whose pin adopts your UENUM array type with no cast.
Flow player events you need to handle
Nothing in Narratyr draws UI. The runtime tells you what to show; presenting it is your job. That contract is delivered through delegates, and a working integration means binding the right ones.
Bind on the quest manager, not on each flow player
For quest-driven content, bind once on UNarratyrQuestManager at startup. Flow players
come and go — a quest creates one when it starts, more if it forks or an async listener
fires, and fresh ones on save restore. The quest manager's delegates survive all of that
and hand you the relevant flow player as a parameter.
| Delegate | Signature | Why you want it |
|---|---|---|
OnQuestFlowPlayerReady |
(FName QuestId, UNarratyrFlowPlayer* Flow) |
Fires after a flow player exists but before it starts running. The only place to bind that flow's own node delegates in time to see nodes it auto-advances through |
OnQuestDialogueStarted |
(FName QuestId, UNarratyrFlowPlayer* Flow, const FNarratyrDialogueNode& Node) |
A quest reached a dialogue line. Open your dialogue UI and drive it with Flow |
OnQuestCustomNodeStarted |
(FName QuestId, UNarratyrFlowPlayer* Flow, const FNarratyrCustomNode& Node) |
A custom node was entered. Read its fields and act |
OnQuestStateChanged |
(FName QuestId, ENarratyrQuestState NewState) |
Quest log, notifications |
OnObjectiveStateChanged |
(FName QuestId, const FNarratyrQuestObjective& Objective) |
Objective tracker. Name and description arrive already localized |
OnQuestProgressChanged |
(FName QuestId) |
Payload-free "something moved" signal. Useful for a blanket UI refresh |
void AMyPlayerController::BeginPlay()
{
Super::BeginPlay();
UNarratyrQuestManager* Quests = GetGameInstance()->GetSubsystem<UNarratyrQuestManager>();
Quests->OnQuestFlowPlayerReady.AddDynamic(this, &AMyPlayerController::HandleFlowReady);
Quests->OnQuestDialogueStarted.AddDynamic(this, &AMyPlayerController::HandleQuestDialogue);
Quests->OnObjectiveStateChanged.AddDynamic(this, &AMyPlayerController::HandleObjective);
}
void AMyPlayerController::HandleQuestDialogue(FName QuestId, UNarratyrFlowPlayer* Flow,
const FNarratyrDialogueNode& Node)
{
ActiveFlow = Flow; // UPROPERTY on the controller
DialogueWidget->Present(Node, Flow->GetCurrentChoices());
}
The flow player's own delegates
Bind these on a specific flow player — either one you created, or one handed to you by
OnQuestFlowPlayerReady.
Lifecycle
| Delegate | Payload | Notes |
|---|---|---|
OnNodeEntered / OnNodeExited |
(FName NodeId, ENarratyrNodeType Type) |
Untyped; good for analytics and logging |
OnGraphPushed / OnGraphPopped |
(FName Parent, FName Child) |
Fired when the flow dives into another graph and returns |
OnFlowEnded |
(ENarratyrFlowEndReason Reason) |
Natural, Stopped, Error, or DeadEnd. Close your dialogue UI here |
OnFlowError |
(FString Message) |
Also fires on DeadEnd, with a per-edge breakdown of why nothing was passable |
DeadEnd is worth handling separately in development builds: it means the flow reached a
node that had outgoing edges but every condition on them was false, which is nearly
always a content bug rather than an intended ending.
Per node type
There is one typed On<Type>Entered delegate per node type, each carrying that node's
struct — OnDialogueEntered, OnChoiceEntered, OnObjectiveEntered,
OnCompleteQuestEntered, OnCustomNodeEntered, OnInstructionEntered, OnGateEntered,
and so on. Bind only the ones you care about and you get exactly the fields that type has,
with no switching on a node-type enum.
OnEventListenerFired deserves a mention. It fires the moment an
event listener interrupts the flow, before the interruption happens and
while the cursor is still on the interrupted node. Bind it to suspend whatever was on
screen — typically hiding the dialogue widget, because the listener body may present its
own lines before a Return node resumes the interrupted node.
The minimum viable dialogue UI
At a stop, ask the flow player what to draw:
if (Flow->IsAtPlayerChoiceMenu())
{
for (const FNarratyrAvailablePath& Path : Flow->GetCurrentChoices())
{
if (Path.bHidden) continue; // author hid it
AddButton(Path.bDisabled && !Path.LabelOnDisabled.IsEmpty()
? Path.LabelOnDisabled : Path.ChoiceLabel,
/*bEnabled=*/ Path.bPassable && !Path.bDisabled,
/*OnClick=*/ [Flow, Index = Path.OrderIndex] { Flow->Choose(Index); });
}
}
else
{
ShowContinuePrompt([Flow] { Flow->Continue(); }); // single "next" affordance
}
IsAtPlayerChoiceMenu is the distinction between the author wrote a menu and the
runtime is routing on conditions. A dialogue node with several conditional outgoing edges
and no choice slots is branching, not asking — show a single continue prompt and let
Continue() take the first passable edge.
Blocked choices are still returned so you can grey them out and, if you want, show
BlockedReason. bHidden and bDisabled come from the hide / disable conditions the
author set on the choice.
Stopping on other node types
By default the flow only pauses on Dialogue and Choice. Add more when the game needs to do something at a node:
Flow->SetStopOnTypes({ ENarratyrNodeType::Objective });
Then resume with Continue() / Complete() once you have set up whatever that objective
needs. TryContinue() is the conditional variant: it advances only if an outgoing edge is
currently passable, and otherwise stays parked — the natural fit for
"advance when BoarsKilled >= 5", poked from your gameplay code whenever the count
changes (UNarratyrQuestManager::TryAdvanceQuest forwards to it by quest id).
Gates resume by themselves when their condition becomes true, as long as the condition
reads variables or quest status. If it depends on state Narratyr cannot see — inventory,
world state, a function's side effects — call ReevaluateGate() (or
ReevaluateQuestGate) after that state changes.
Custom nodes carry an authored blocking flag. A blocking custom node pauses for your
Continue(); a non-blocking one fires its delegate and auto-advances, so an unhandled
custom node never hangs a flow.
Implementing functions
Functions you declare in Functions are calls into your game — the
runtime knows their names and signatures and nothing else. The generated
UNarratyrFunctionHost turns each one into a BlueprintNativeEvent method:
// Generated — Source/NarratyrGameData/Public/NarratyrFunctionHost.h
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Narratyr|Functions")
bool HasInventoryItem(const FString& ItemId);
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Narratyr|Functions")
void AddInventoryItem(const FString& ItemId, int32 Count);
Subclass it and override the ones you need — in C++:
UCLASS()
class MYGAME_API UMyFunctionHost : public UNarratyrFunctionHost
{
GENERATED_BODY()
public:
virtual bool HasInventoryItem_Implementation(const FString& ItemId) override;
virtual void AddInventoryItem_Implementation(const FString& ItemId, int32 Count) override;
};
…or by creating a Blueprint child of NarratyrFunctionHost and implementing the events
there. Either way, register one instance at startup:
UNarratyrVariableManager* Variables =
GetGameInstance()->GetSubsystem<UNarratyrVariableManager>();
FunctionHost = NewObject<UMyFunctionHost>(this); // UPROPERTY
Variables->SetFunctions(FunctionHost);
Do this before anything runs a graph — a condition that calls an unregistered function
cannot evaluate. A game instance subsystem's Initialize, or your game instance's Init,
is the usual home.
Do not override Execute_Implementation; that is the generated dispatcher that routes a
name and boxed arguments to the typed method.
Functions cannot appear in event listener conditions. Listeners are re-evaluated on
variable writes, and the runtime cannot know when opaque game state changed — so a
listener that called hasInventoryItem() would silently never fire. Narratyr's validator
rejects it. Mirror the state into a variable instead.
Save and restore
Narratyr does not own a save slot. It hands you a value struct; your save system decides what to do with it.
FNarratyrSaveState carries variable state and quest state together, with a format
version. Quest state includes each quest's flow players — primary and any secondaries — so
capturing quests captures where every quest flow is parked.
USTRUCT()
struct FMyGameSaveState
{
GENERATED_BODY()
UPROPERTY() int32 Version = 0; // 0, never a valid version — see below
UPROPERTY() FDateTime SavedAtUtc;
UPROPERTY() FNarratyrSaveState Narratyr; // the whole Narratyr snapshot
// ...your own fields
};
Initializing to 0 and assigning the real number at capture time
(MySave->State.Version = MyGameSaveVersion;) makes the value differ from the
default, so it is always written. FNarratyrSaveState does this.
Capture and restore through UNarratyrSaveLibrary rather than the individual managers —
it enforces the ordering:
// Save
MySave->State.Narratyr = UNarratyrSaveLibrary::CaptureSaveState(GetGameInstance());
UGameplayStatics::SaveGameToSlot(MySave, SlotName, UserIndex);
// Load -- check the result; a refused snapshot restored NOTHING.
const ENarratyrRestoreResult Result =
UNarratyrSaveLibrary::RestoreSaveState(GetGameInstance(), MySave->State.Narratyr);
if (Result == ENarratyrRestoreResult::RefusedMajorMismatch)
{
// The save is unreadable by this build. Tell the player; do not carry on
// as though it loaded.
}
Things to know:
- Variables are restored before quests, because quest restore evaluates conditions that read variables. The library does this for you; if you call the managers directly, do it in that order yourself.
RestoreSaveStatereturns what it did.Restored,RestoredWithMinorMismatch,RefusedMajorMismatch(nothing restored — the runtime still holds whatever it had), orSubsystemsMissing. The refusal is also logged, but the log is not reachable from Blueprint, so the return value is the only way to react in your UI. To decide before committing to a load — worth doing if you tear down session state first — compare the snapshot'sVersionagainstGetCurrentSaveMajorVersion().- Major version mismatches are refused; minor ones are not.
FNarratyrSaveStatecarries a major (Version) and a minor (MinorVersion). A major mismatch means a persisted field changed shape — an error is logged, nothing is restored, and the save should be treated as unloadable. A minor mismatch only ever means fields were added, so the restore proceeds after a warning and anything the snapshot lacks keeps its default. Reinstalling the module after a Narratyr upgrade can move either number. However, if we do change the major version number we'll be certain to call it out. - The snapshot is an interchange format, but restore is not. Every layer exposes a
Blueprint-callable
CaptureState, and the state structs are plainBlueprintReadWritedata, so you can walk them and re-encode into any container or format you like — your own binary stream, your own JSON, a backend blob.VersionandMinorVersiontravel with the data, so a re-encoded snapshot keeps its compatibility signal. Going the other way, rebuild the structs and hand them back toRestoreSaveState: reconstructing state by calling individual setters is not supported. There is no "set quest state", the objective flags and activation order round-trip only through the snapshot, and secondary flow players can only be rebuilt by the quest instance that owns them. - Editing a quest's objectives does not invalidate its saves. Objective state is restored by objective id, so adding, removing, or reordering objectives in Narratyr is safe. An objective in the save that the quest no longer has is skipped with a warning; one added since the save starts at its authored state.
- Reordering an enumeration does not invalidate its saves either. An enum-typed
variable persists its entry key, not the enumeration's integer, so a saved value
follows its key. An entry the enumeration no longer has is skipped with a warning and the
variable keeps its current value; for a list or set, one unresolvable entry leaves the
whole collection alone rather than silently dropping members. Renaming or deleting an
entry is the case that costs you a saved value — reordering and appending are free.
The generated
UENUM's numbers are stable across a reorder too, so C++ or Blueprint holding a rawuint8keeps working; that only breaks if you turn auto-compute off and edit the numbers by hand. - Restoring an active quest re-fires
OnQuestFlowPlayerReadyfor every flow it brings back. If your handlers are bound on the quest manager, they rebuild automatically. Tear down stale per-flow UI before restoring so you do not end up with two dialogue widgets. - Standalone flow players are yours to save. A flow player you created outside a quest
is not in the snapshot. Call
CaptureState()on it and store the resultingFNarratyrFlowPlayerStatein your own save;RestoreState()after the Narratyr variables are restored. - New game means
UNarratyrVariableManager::ResetToDefaults()plusUNarratyrQuestManager::ResetAll().ResetAllis deliberately silent — no per-quest delegates fire — so refresh your quest UI yourself afterwards.
Localization and rich text
Dialogue text, choice labels, quest titles, objective names, and translatable entity
fields all export as keys into a shared string table at
Content/NarratyrData/Localization/ST_NarratyrStrings. The FText fields you read off a
node are already backed by it, so they localize with Unreal's normal culture switching —
no extra work for the source language.
For translators, the exporter also writes CSV and XLIFF to a NarratyrLocalization/ folder
at the project root. It sits outside Content/ deliberately, so Unreal never tries to
import it.
UNarratyrQuestManager::Localize(Key, Fallback) resolves any string-table key directly,
and GetObjectiveName / GetObjectiveDescription / GetObjectiveGroupName resolve the
localized text for an objective — prefer them over reading the raw Name and
Description fields, which hold untranslated source strings.
Rich text. Set the project's rich text format to Unreal Engine in Narratyr's
project settings — exporting to Unreal with another engine's format selected produces a
warning, because the markup in your dialogue will not render. The tag styles you define
there export to Content/NarratyrData/UI/ and merge into
DT_NarratyrRichTextStyles, which you point a URichTextBlock at. The merge is
conservative: fields you left unset in Narratyr do not overwrite what an artist set on the
Unreal side, so a Narratyr-chosen color can coexist with a UE-chosen font asset.
Editor tooling
The plugin's editor module adds two tabs to the Window menu — grouped under a Narratyr Tools section in the Level Editor, and listed with the other developer tool tabs elsewhere.

Narratyr Variable Debugger
The tab docks as Narratyr Variables and lists every variable in the project with its type and current value. Out of play it shows the authored defaults, read-only. During PIE it goes live and becomes editable — flip a boolean, pick an enum entry, type a number, expand a list to add or remove elements.
Edits go through the variable manager, exactly as a graph's own writes do, so a gate waiting on a variable opens and an event listener rises the moment you change the value that satisfies it. That makes it the fastest way to test a branch without playing up to it.

Narratyr Graph Visualizer
Docked as Narratyr Graph: a read-only view of a quest or dialogue graph, laid out the way you authored it in Narratyr. During PIE it follows a running flow player — pick a specific one or leave it on Follow latest activity, and with Auto-focus current node on, the canvas tracks the cursor as the flow moves.
Node History on the left is the ordered list of nodes this flow has entered — the path that got the player here, including the nodes a flow auto-advanced through before you were watching. Selecting a node shows its authored properties on the right, so you can read the condition a gate is actually waiting on.

Double-clicking an imported graph asset opens the same visualizer in an asset editor.
Iterating
Day to day the loop is: change content in Narratyr, press Export Data, alt-tab to Unreal. The plugin imports what changed and the running editor picks it up.
Reinstall the module when the schema moves:
- an enumeration added, renamed, or given new entries
- a property set or entity type changed
- a function added, renamed, or re-signed
- a quest graph added or removed (the
ENarratyrQuestIdenum is generated) - items deleted from the project, so the generated code stops referring to them
- a Narratyr upgrade that ships plugin changes
Exports are derived artifacts. Teams generally either commit
Content/NarratyrData/ and the generated source alongside the rest of the project, or
regenerate them in a build step — both work.
From the command line
For a build step, the narratyr CLI runs the same exporters headlessly and produces
byte-identical output:
narratyr export --engine unreal --operation data \
--project /path/to/MyStory.ntproj \
--out /path/to/MyGame
--operation takes install, data, or both. --no-cleanup disables the orphan
cleanup that is otherwise on for Unreal data exports, and --json emits a
machine-readable result for a CI step to parse. The CLI reads the license installed by the
desktop app, so the same entitlements apply.
Troubleshooting
"Exporting is disabled." The free tier caps how many graph nodes a project can export. The message names the cap and your project's node count — activate a Pro licence or reduce the node count.
"No .uproject file found — modules were not registered." The export path is not the Unreal project root. See Setting the export path.
Generated types are missing from Blueprint. The module has not been compiled, or was compiled before the last Install Module. Close the editor, build, reopen.
Assets in Content/NarratyrData are not updating. Check that Install Module has been
run at least once — the importer lives in the plugin's editor module. If the editor was
open during a first install, restart it.
A flow stops and nothing happens. Either it parked on a node type you added to the stop
set and never resumed (development builds log a warning naming the node), or it ended as
DeadEnd because no outgoing edge was passable. Bind OnFlowError — the log names each
blocked edge and why.
A listener never fires. Listeners are only re-evaluated on variable writes, and the condition must not call a function. Write the state you are listening for into a Narratyr variable.
A gate never opens. Its condition depends on something outside the variable manager
and the quest manager. Call ReevaluateGate() after that state changes.