Documentation menu

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.

Narratyr's Export view with the Unreal tab selected: an Unreal Project Path field, an Install Module section, and an Export Data section with a cleanup checkbox

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

  1. Writes the plugin to Plugins/NarratyrRuntime/.
  2. Writes generated per-project code to Source/NarratyrGameData/.
  3. Registers both in your .uprojectNarratyrRuntime in Plugins, NarratyrGameData in Modules.
  4. Writes a managed block into Config/DefaultEditorPerProjectUserSettings.ini that excludes Content/NarratyrData from 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:

Unreal's Content Browser at Content/NarratyrData, showing the Assets, Entities, Graphs, Localization, Quests, UI, and Variables folders alongside the GraphRegistry data asset

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 setsAddToVariableList*, RemoveFromVariableList*, VariableListContains*, GetVariableItemCount, plus wildcard nodes (Get Variable As List) whose pin adopts whatever array type you connect, including your own UENUMs.
  • 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
};
We recommend including a version field on your save games. FNarratyrSaveState does this with a Major and Minor version field. An Unreal Engine quirk to be aware of: UE omits any property identical to its default when it writes a save, so a version field whose initializer *is* the current version never gets written at all — and a later build, finding no tag, falls back to its own initializer and reads a stale save as current. Your version check then passes on every old save.

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.
  • RestoreSaveState returns what it did. Restored, RestoredWithMinorMismatch, RefusedMajorMismatch (nothing restored — the runtime still holds whatever it had), or SubsystemsMissing. 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's Version against GetCurrentSaveMajorVersion().
  • Major version mismatches are refused; minor ones are not. FNarratyrSaveState carries 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 plain BlueprintReadWrite data, 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. Version and MinorVersion travel with the data, so a re-encoded snapshot keeps its compatibility signal. Going the other way, rebuild the structs and hand them back to RestoreSaveState: 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 raw uint8 keeps working; that only breaks if you turn auto-compute off and edit the numbers by hand.
  • Restoring an active quest re-fires OnQuestFlowPlayerReady for 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 resulting FNarratyrFlowPlayerState in your own save; RestoreState() after the Narratyr variables are restored.
  • New game means UNarratyrVariableManager::ResetToDefaults() plus UNarratyrQuestManager::ResetAll(). ResetAll is 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.

Unreal's Window menu with the Narratyr Graph Visualizer and Narratyr Variable Debugger entries highlighted

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.

The Narratyr Variables tab during PIE, listing variables by name, type, and value, with a string list expanded to show its elements

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.

The Narratyr Graph tab following a running quest, with node history on the left, the graph canvas in the middle, and the selected gate node's properties on the right

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 ENarratyrQuestId enum 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.