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. Details here may change before release.

Narratyr includes the Unreal plugin with the editor instead of distributing it through Fab. The exporter writes the plugin into your project because your data types determine part of its code.

The Unreal exporter adds these files to your project:

  • A plugin that runs graphs and provides runtime features.
  • Generated C++ types based on entity types, option sets, and function definitions.
  • Data under NarratyrData/, including tables, images, and audio.
  • Configuration for the importer. The exporter adds only its managed settings and won't replace your other configuration.

Before you start

  • Unreal Engine 5.4 or later with a C++ project. The exporter writes a project module and registers it in your .uproject. Add any C++ class in Unreal Editor to convert a Blueprint-only project. You don't need to rebuild the engine.
  • A compiler toolchain, such as Visual Studio, Rider, or Xcode. Installing the module changes code and requires a build.

Setting the export path

Open the Export view in Narratyr and pick the Unreal tab.

Point the project path to the folder that contains your .uproject file. Choose the project root instead of Content/, Source/, or its parent. The exporter writes paths 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 can't find a .uproject in the selected folder, it writes the files and warns that module registration failed.

Narratyr remembers a separate path for each project and engine.

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

Install the module and export data

Install Module Export Data
Writes C++ source and the plugin Data files under Content/
Needs a recompile Yes No
Run after The first project export or changes to option sets, property sets, entity types, functions, or quest graphs Changes to dialogue, quests, entity values, or variables
Typical frequency Occasionally Whenever you want to see a change.

Definitions and content. Install the module after changing data definitions. Export data after changing project content.

Module installation

  1. Writes the plugin to Plugins/NarratyrRuntime/.
  2. Writes generated per-project code to Source/NarratyrGameData/.
  3. Adds NarratyrRuntime to Plugins and NarratyrGameData to Modules in .uproject.
  4. Writes a managed block into Config/DefaultEditorPerProjectUserSettings.ini that excludes Content/NarratyrData from Unreal's Auto Reimport watcher. The plugin importer handles those files. Comment markers surround the managed block, and later installations only replace that block.

Close Unreal Editor if it's open, build the project, and reopen it. Blueprint displays new UENUM and USTRUCT types after compilation loads the module.

Data export

Export Data 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. It hashes contents so an unchanged file doesn't dirty its .uasset. With the editor open, you can usually return to it and find the assets already current.

Clean up items removed from the project (the checkbox) deletes files under Content/NarratyrData/ that no longer match items in the project.

Don't add your own data to NarratyrData through Unreal Editor or reorganize the folder manually. The exporter manages this directory and can remove unexpected files.

Exported project structure

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)

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/ contains generated files that each export can overwrite. Put your own code in your own module and subclass what you need. Never edit the generated files.

Runtime classes

Class Kind Purpose
UNarratyrVariableManager Game instance subsystem Read and write Narratyr variables. Register your function host and interpolate @{Var} in text
UNarratyrQuestManager Game instance subsystem Start, complete, and query quests. Provides delegates for game bindings
UNarratyrQuestInstance Object, one per quest Live state for objectives and flow players. Often used for interface bindings
UNarratyrFlowPlayer Object Walk a quest or dialogue graph. Registered callbacks handle decisions and presentation
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 name, portrait, or typed row
UNarratyrFunctionHost Object (generated) Subclass this with your function definitions.
UNarratyrFieldValueLibrary Blueprint library Read a node's or graph's property values by name
UNarratyrSaveLibrary Blueprint library Capture and restore the whole Narratyr state
FNarratyrGraphReference + reference library Struct + Blueprint library References to quest, dialogue, or fragment graphs via a stable id
FNarratyrObjectiveReference + reference library Struct + Blueprint library Stable objective ID. Use this instead of FName when the editor should display a picker
ENarratyrQuestId + UNarratyrQuestIdLibrary Enumeration + library (generated) Legacy quest list pins retained for compatibility

Everything is BlueprintType with Narratyr|… categories, so the same API is available from Blueprint under Narratyr in the node palette.

UNarratyrVariableManager

Manages 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 own UENUMs.
  • OnVariableChanged: broadcasts on every successful write. This also wakes up gates and event listeners.
  • InterpolateVariables: resolves variable interpolation and variable-based text selection in any player-facing text.
  • InterpolateDialogueText: resolves a dialogue node's full text when given its stable node id, including the contextual @{visited, ...} directive. Use this for lines you are about to display. See Text directives.
  • SetFunctions: registers your function host.
  • ResetToDefaults: reloads values from the DataTable for a new game.

UNarratyrQuestManager

A game instance subsystem, so this remains alive between level loads. This reads DT_Quests and 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.

Objective completion. The runtime activates and tracks objectives. Your game must notify the quest manager when gameplay satisfies an objective. Call CompleteObjective directly, with an optional Failed or NotApplicable state. You can also update a variable through UNarratyrVariableManager when a Gate or Event Listener waits for that value. Flow players pause at those nodes and resume automatically after the condition becomes true.

UNarratyrFlowPlayer

UNarratyrFlowPlayer runs a graph. The quest manager creates one for a quest instance and adds more players for concurrent flows. Create a player directly when game code starts a standalone dialogue.

It advances automatically through nodes that aren't stopping points and pauses on Dialogue and Choice nodes. At a stop you call Continue(), Choose(Index), or Complete() to resume.

// Standalone conversation 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 to the graph entry point. This is usually what you want. Narratyr handles save/restore, so there isn't a reason to play from the middle of a graph in normal usage.

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(...);

UNarratyrEntityRegistry

The exporter generates this class from your entity types. It indexes exported entities by their stable Narratyr ID.

UNarratyrEntityRegistry* Entities =
    GetGameInstance()->GetSubsystem<UNarratyrEntityRegistry>();

FNarratyrEntityInfo Info;
if (Entities->FindEntity(Node.SpeakerEntity, Info))
{
    // Info.LocalizedDisplayName is the player-facing FText.
    // Info.DisplayName remains the source/authoring FString.
    // Info.ProfileImage, Info.EntityType, Info.GameId
}

FNarratyrCharacterRow Row;                       // named after YOUR entity type
Entities->FindCharacter(Node.SpeakerEntity, Row);

FindEntity returns the common fields for an entity, including its ID, names, profile image, and type. The shared Narratyr String Table provides LocalizedDisplayName as an FText under the stable key entity.{entityId}.displayName. Use it for player-facing UI. The existing DisplayName remains an FString for source-language authoring labels and editor tooling. The generated Find<TypeName> accessors expose both names on 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.

Entity references

Use Narratyr Entity Reference when an actor or another editable object needs to reference an exported entity. Its Details panel picker searches display names, game IDs, and stable IDs across the imported tables under Content/NarratyrData/Entities. The stored value is always the stable Narratyr ID, so renaming an entity or changing its game ID doesn't change the reference.

In Blueprint:

  1. Add a variable and choose Narratyr Entity Reference as its type.
  2. Compile the Blueprint, then choose the entity in Class Defaults.
  3. Enable Instance Editable to choose different entities for placed actor instances.
  4. Use Get Narratyr Entity ID to connect the reference to Find Entity or other functions that want an id.

Make Narratyr Entity Reference constructs one from an existing stable ID. Is Narratyr Entity Reference Set only tests whether the ID is nonempty, not whether the entity exists. Use the registry's lookup result to check existence at runtime.

The picker offers an All entity types filter, Browse to open the selected entity's table, and Clear to remove the reference. The type filter is a browsing convenience, not a restriction stored on the variable. Duplicate display names or game IDs remain separate choices and display their stable IDs. The picker highlights a missing entity but preserves its ID, including when a table temporarily can't load.

The choices update after table changes or imports. Refresh runs the scan manually. If no choices appear, compile the generated row types and import the exported data. The picker reads tables through reflection.

In C++, include NarratyrEntityReference.h from the NarratyrData module:

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Narratyr")
FNarratyrEntityReference Character;

// Character.Id is the FName accepted by the generated entity registry.

Graph and objective references

Use Narratyr Graph Reference for a stable reference to any imported quest, dialogue, or fragment graph. Its picker reads graph assets under Content/NarratyrData/Graphs and can filter by graph type. Use Narratyr Objective Reference for an objective ID. Its picker reads the same ObjectivesJson data the quest manager loads from DT_Quests.

Both types work as Blueprint variables, editable actor properties, and literal Blueprint node pins. Objective rows and selected values always include their quest prefix. The pickers mark missing graphs or objectives without clearing their stored IDs, so a temporary import failure doesn't rewrite actor or Blueprint defaults.

In C++, include NarratyrGraphReference.h and use the static types from the NarratyrData module:

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Narratyr")
FNarratyrGraphReference Quest;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Narratyr")
FNarratyrObjectiveReference Objective;

For a property that should initially browse only quests, add meta = (NarratyrGraphType = "quest"). This setting acts only as an editor browsing filter. The structure continues to store a general graph ID.

UNarratyrFieldValueLibrary

Dialogue, hub, and custom nodes can store project property values from the node's entity type, and a graph can store metadata the same way. Instead of breaking the structure 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

Narratyr doesn't draw the interface. The runtime supplies content through delegates, and your game presents it. Bind the delegates required by your interface.

Bind quest manager delegates

For quest content, bind once on UNarratyrQuestManager at startup. A quest creates a flow player when it starts and creates more after a Fork or asynchronous listener. Save restoration also creates new players. Quest manager delegates remain registered and provide the relevant flow player as a parameter.

Delegate Signature Why you want it
OnQuestFlowPlayerReady (FName QuestId, UNarratyrFlowPlayer* Flow) Fires before a new flow player starts. Bind its node delegates here to observe nodes that it advances through automatically
OnQuestDialogueStarted (FName QuestId, UNarratyrFlowPlayer* Flow, const FNarratyrDialogueNode& Node) Fires for a dialogue line. Open your dialogue interface and drive it with Flow
OnQuestCustomNodeStarted (FName QuestId, UNarratyrFlowPlayer* Flow, const FNarratyrCustomNode& Node) Fires after entry to a custom node. 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. Use one you created or received from OnQuestFlowPlayerReady.

Lifecycle

Delegate Payload Notes
OnNodeEntered / OnNodeExited (FName NodeId, ENarratyrNodeType Type) General delegates 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

Handle DeadEnd in development builds. It means a node had outgoing edges but none had a true condition. Treat this result as a content error unless the graph expects it.

Per node type

Each node type has a typed On<Type>Entered delegate with its node structure: OnDialogueEntered, OnChoiceEntered, OnObjectiveEntered, OnCompleteQuestEntered, OnCustomNodeEntered, OnInstructionEntered, OnGateEntered, and OnGateEntered. Bind the delegates you need to receive fields for that node type without switching on a node type enumeration.

OnStartQuestAsyncEntered is observational: the flow player has already asked the Quest Manager to start the target quest before this delegate fires. That built-in behavior also applies to standalone dialogue flows.

OnEventListenerFired runs before an event listener interrupts the flow, while the cursor remains on the current node. Bind it to suspend the current presentation. The listener body might display its own lines before a Return node resumes the earlier node.

Basic dialogue interface

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(); });      // one "next" control
}

IsAtPlayerChoiceMenu distinguishes a project choice menu from conditional routing. A dialogue node with conditional outgoing edges and no choice slots performs routing. Show one continue control and let Continue() take the first passable edge.

The result includes blocked choices so you can dim them. You can also display BlockedReason. The choice settings in the project determine bHidden and bDisabled.

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 });

Resume with Continue() or Complete() after setting up the objective. TryContinue() advances only when an outgoing edge can pass. Use it for a rule such as "advance when BoarsKilled >= 5." Call it from gameplay code when the count changes. UNarratyrQuestManager::TryAdvanceQuest forwards the call by quest ID.

Gates resume when their condition becomes true if the condition reads variables or quest status. For inventory, world state, or other state Narratyr can't observe, call ReevaluateGate() or ReevaluateQuestGate() after that state changes.

Custom nodes contain a blocking flag. A blocking custom node pauses for Continue(). A non-blocking node emits its delegate and advances automatically, so an unhandled custom node doesn't stop the flow.

Implementing functions

Functions from Functions call into your game. The runtime uses their names and signatures, while the generated UNarratyrFunctionHost turns each one into a BlueprintNativeEvent method:

// Generated in 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 required methods 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 running a graph because a condition can't evaluate an unregistered function. A game instance subsystem's Initialize, or your game instance's Init, is the usual home.

Don't override Execute_Implementation. The generated dispatcher routes a name and boxed arguments to the typed method.

Functions can't appear in event listener conditions. Listeners check their conditions after variable writes, and the runtime can't detect changes to game state behind a function. Project checks reject these calls. Mirror the state into a variable instead.

Save and restore

Narratyr doesn't create a save slot. It provides a value structure for your save system.

FNarratyrSaveState contains variable state, quest state, and a format version. Quest state includes primary and secondary flow players, so a snapshot records each flow position.

USTRUCT()
struct FMyGameSaveState
{
    GENERATED_BODY()

    UPROPERTY() int32 Version = 0;             // 0 is never a valid version. See below.
    UPROPERTY() FDateTime SavedAtUtc;
    UPROPERTY() FNarratyrSaveState Narratyr;   // the whole Narratyr snapshot
    // ...your own fields
};

Save versions. FNarratyrSaveState uses Major and Minor version fields. If the Major version doesn't match the plugin's runtime, Narratyr will refuse to load it, because a change in Major version signals that a backwards-incompatible change was introduced to the save format. Minor version changes will still load, using defaults for new fields.

We're unlikely to change the major version of save games in a point release update because we don't want to break your saved games. But a major new version of Narratyr may do that. If we do so, we'll document it.

Use UNarratyrSaveLibrary to capture and restore state in the required order:

// Save
MySave->State.Narratyr = UNarratyrSaveLibrary::CaptureSaveState(GetGameInstance());
UGameplayStatics::SaveGameToSlot(MySave, SlotName, UserIndex);

// Load. Check the result because a refused snapshot restores no data.
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.
}

Restore behavior

  • Variable state loads before quest state. Quest restoration evaluates conditions that read variables. UNarratyrSaveLibrary applies this order. Do the same if you call managers directly.
  • RestoreSaveState reports the result. It returns Restored, RestoredWithMinorMismatch, RefusedMajorMismatch, or SubsystemsMissing. A refused restore doesn't change runtime state. Blueprint can't access the related log message, so check the return value. To check compatibility before loading, compare Version with GetCurrentSaveMajorVersion().
  • Major and minor mismatches behave differently. A major mismatch means a persisted field changed structure, so the library reports an error and refuses the complete snapshot. A minor mismatch indicates added fields. The library warns, restores known data, and uses defaults for missing fields. A Narratyr update can change either version, and its release notes identify major changes.
  • You can re-encode a snapshot. Blueprint-callable CaptureState functions return plain BlueprintReadWrite structures. Convert them to your preferred binary or JSON format while preserving Version and MinorVersion. Rebuild the structures before calling RestoreSaveState. Individual setters can't reconstruct quest activation order, objective flags, or secondary flow players.
  • Quest objective changes preserve compatible data. Restore matches objective state by ID. New objectives start from their project state. The library skips missing objectives with a warning.
  • Option set values use entry keys. Reordering or appending entries preserves saved values. If an entry no longer exists, the library warns and leaves the variable unchanged. For a list or set, one missing entry leaves the complete collection unchanged. Renaming or deleting an entry loses its saved value. Generated UENUM numbers also remain stable across reorder unless you turn off automatic values and edit numbers manually.
  • Active quest restoration emits OnQuestFlowPlayerReady. It emits the delegate for each restored flow. Manager-level handlers can then bind new players. Remove stale per-flow interface state before restoration.
  • Save standalone flow players. Call CaptureState() on a player created outside a quest and store its FNarratyrFlowPlayerState. Call RestoreState() after restoring Narratyr variables.
  • Start a new game by resetting both managers. Call UNarratyrVariableManager::ResetToDefaults() and UNarratyrQuestManager::ResetAll(). ResetAll doesn't emit per-quest delegates, so refresh the quest interface afterward.

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 already use that table, so Unreal's normal culture switching localizes them. The source language doesn't require extra setup.

For translators, the exporter also writes .csv and .xlf files to NarratyrLocalization/ at the project root. This folder remains outside Content/, so Unreal doesn't 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. In Narratyr's project settings, choose Unreal Engine as the rich text format. Another engine's markup won't render correctly, so the exporter warns about a different format. The tag styles you define there export to Content/NarratyrData/UI/ and merge into DT_NarratyrRichTextStyles, which you point a URichTextBlock at. During the conservative merge, fields left unset in Narratyr don't replace values that an artist set on the Unreal side. A Narratyr color can coexist with a font asset selected in Unreal.

Editor tooling

The plugin's editor module adds two tabs to the Window menu. The Level Editor groups them under Narratyr Tools, while other windows list them with developer tools.

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

Variable debugger

The tab docks as Narratyr Variables and lists every variable in the project with its type and current value. Outside a play session, it shows project defaults as read-only values. During Play In Editor, it displays live editable values. Change a true or false value, select an option set entry, enter a number, or edit a list.

Edits go through the variable manager, just like graph writes. A Gate or Event Listener responds when you change the required value. This lets you test a branch without playing up to it.

The Narratyr Variables tab during Play In Editor with an expanded string list

Graph viewer

The Narratyr Graph tab provides a read-only view of a quest or dialogue graph with its Narratyr layout. During Play In Editor, it can follow a selected flow player or the latest activity. Turn on Auto focus current node to track the flow cursor on the canvas.

Node History lists nodes entered by the flow, including nodes processed automatically before you opened the viewer. Selecting a node shows its project 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

After changing content, select Export Data, then return to Unreal Editor. The plugin imports changed files into the open project.

Reinstall the module when the schema moves:

  • an option set added, renamed, or changed
  • a property set or entity type changed
  • a function added, renamed, or re-signed
  • a quest graph added or removed when using the generated ENarratyrQuestId enumeration
  • items deleted from the project, so the generated code stops referring to them
  • a Narratyr upgrade that includes plugin changes

Teams can commit Content/NarratyrData/ and generated source with the project. They can also regenerate those files during a build step.

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 enabled by default 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 reports 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 isn't the Unreal project root. See Setting the export path.

Generated types are missing from Blueprint. Build the module after the latest Install Module operation, then reopen the editor.

Assets in Content/NarratyrData aren't updating. Run Install Module at least once because the plugin's editor module contains the importer. Restart Unreal Editor after the first install.

A flow stops without an event. Resume any node type added to the stop set. Development builds log the stopped node. A DeadEnd result means no outgoing edge passed its condition. Bind OnFlowError to receive details for each blocked edge.

A listener never fires. Listeners check conditions after variable writes, and their conditions can't call functions. Copy the required state 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.