Unity integration
Description: Export a Narratyr project into Unity and drive dialogues and quests from C#.
Prerelease. The Unity integration comes with Narratyr. The exporter writes the runtime package and generated C# into your project. Details here may change before release.
The Unity exporter adds a C# runtime package, generated types, and your Narratyr data to your game project. Your game uses shared managers to read data and track quests, and flow players to run dialogues.
This guide covers setup and the C# API. See Exporting for the shared export workflow.
Before you start
- A Unity project. The runtime package declares Unity 2021.3 as its minimum version.
It depends on Unity Localization and
com.unity.nuget.newtonsoft-json, which Unity resolves when it loads the package. - A saved Narratyr project. The exporter copies assets from the saved project's asset directory.
- Your own dialogue UI. Narratyr runs the graph and supplies lines and choices. Your game displays them and sends the player's response back to the flow player.
Setting the export path
Open the Export view in Narratyr and select the Unity tab. Set Unity Project
Path to the project root, the folder containing Assets, Packages, and
ProjectSettings.
MyGame/ <-- choose this folder
Assets/
Packages/
ProjectSettings/
Don't select Assets itself. The exporter creates its own Assets/NarratyrData and
Packages/com.narratyr.data folders under the path you choose.
Source Locale is the language code for text authored in Narratyr, such as en or
ja. Use the same code for the source Locale in Unity. It becomes the language column
in the runtime localization CSV and the source language in the translator files.
Install the package and export data
| Install Package | Export Data | |
|---|---|---|
| Writes | Runtime and editor package, plus project-specific C# | JSON data, asset files, and localization sources |
| Location | Packages/com.narratyr.data/ and Assets/NarratyrGameData/ |
Assets/NarratyrData/ and NarratyrLocalization/ |
| Run it when | At setup, after definition changes, or after updating Narratyr | Whenever authored content changes |
| Unity work | Resolve package dependencies and compile C# | Import data and refresh strings, automatically if enabled |
Install Package generates enumerations, Property Set classes, Entity Type classes, quest-id constants, and a function host for your project. Run it again after changing those definitions. Adding or renaming quests also changes the generated quest constants.
Export Data writes variable defaults, enumeration values, entities, graphs, quest definitions, asset paths, and text. After changing a definition, run both operations so the generated code and data agree.
First-time setup
- Choose the Unity project path in Narratyr and select Install Package.
- Select Export Data.
- Open the Unity project and let package resolution, compilation, and asset imports finish.
- In Unity, open Edit → Project Settings → Localization. Create a Localization Settings asset if prompted. Use Locale Generator to add your source language and the other languages your game supports. Unity's Localization settings guide explains these controls.
- Select Narratyr → Import Localization CSV. This creates or updates the
Narratyr Strings String Table Collection under
Assets/NarratyrData/Localization/Tables/. - To import later text exports automatically, open Edit → Project Settings → Narratyr Localization and enable Automatically import Narratyr strings. This option starts off and becomes available after a successful manual import.
- Register your game's function host, connect runtime events, and start a graph or quest.
The localization command needs active Localization Settings and a configured Locale matching Source Locale. Without automatic imports, run the command after each text export.
The default string table is optional. You can leave String Database → Default Table Reference unset in Unity's Localization settings. Narratyr explicitly requests the Narratyr Strings collection. Setting it as the default is also valid, but only affects game code that looks up strings without specifying a table.
The runtime managers are ordinary C# objects, accessed through NarratyrRuntime. You
don't need to add a manager component to a GameObject. Use the shared runtime on Unity's
main thread.
Exported files
MyGame/
Packages/
com.narratyr.data/ installed runtime and editor package
package.json
Runtime/
Editor/
Assets/
NarratyrGameData/ generated by Install Package
Narratyr.Game.asmdef
NarratyrEnums.cs
NarratyrPropertySets.cs
NarratyrEntityTypes.cs
NarratyrFunctionHost.cs
NarratyrQuestIds.cs
NarratyrData/ generated by Export Data
Resources/
NarratyrData/
variables.json
enumerations.json
graph_registry.json
graph_index.json
graphs/
quest_data.json
entity_registry.json
entities/
asset_registry.json
NarratyrAssets/ copied images, audio, and other assets
Localization/
narratyr_strings.csv
Tables/ created by the Unity localization import
NarratyrLocalization/ translator handoff, outside Assets
translator-source.csv
translator-source.xlf
The exporter skips files with no matching data. A project with no variables doesn't
produce variables.json, but every export writes the asset registry, even when empty.
Installation and export overwrite generated files. Keep your scripts outside the package
and Assets/NarratyrGameData/. Unity creates the companion .meta files.
Runtime classes
Import Narratyr for runtime classes and Narratyr.Game for generated project types.
| Class | Purpose |
|---|---|
NarratyrRuntime |
Shared access to variables, quests, graphs, entities, assets, localization, and saves |
NarratyrVariableManager |
Runtime variables, function dispatch, and text interpolation |
NarratyrQuestManager |
Quest lifecycle, objective updates, and quest-wide events |
NarratyrQuestInstance |
One quest's state, objectives, and owned flow players |
NarratyrFlowPlayer |
Graph execution, dialogue stops, and choice selection |
NarratyrGraphRegistry |
Resolve stable graph ids to JSON graph data |
NarratyrEntityRegistry |
Find entities by stable Narratyr id or game id |
NarratyrAssetRegistry |
Load copied assets by stable Narratyr asset id |
NarratyrLocalization |
Read the active locale's Narratyr String Table |
QuestIds |
Generated string constants for quest ids |
FunctionHost |
Generated base class for game-defined functions |
If your game scripts use an assembly definition, add references to
Narratyr.Data.Runtime and, when using generated types, Narratyr.Game. Code that calls
Unity Localization directly also needs a reference to Unity.Localization.
Variables
Read and write declared variables through typed methods:
using Narratyr;
NarratyrRuntime.Variables.SetVariableInt("BoarsKilled", 5);
NarratyrRuntime.Variables.SetVariableBool("MetTheJarl", true);
long killed = NarratyrRuntime.Variables.GetVariableInt("BoarsKilled");
bool metJarl = NarratyrRuntime.Variables.GetVariableBool("MetTheJarl");
Other methods include GetVariableNumber(), GetVariableString(), HasVariable(),
GetAllVariableNames(), and ResetToDefaults(). Runtime integer values use long, and
number values use double.
For a declared list or set, use a typed collection handle:
var visitedPlaces = NarratyrRuntime.Variables.GetVariableSet<string>("VisitedPlaces");
visitedPlaces.Add("Harbor");
var scores = NarratyrRuntime.Variables.GetVariableList<long>("Scores");
scores.Add(10L);
These handles write through the manager and notify listeners. Sets remove duplicates.
Use SetVariableList<T>() or SetVariableSet<T>() to replace a whole collection.
The VariableChanged(name, value) event reports writes. Gates and event listeners react
to manager notifications, so use these methods to change values.
Quests and objectives
For a quest named Down With The Jarl, the exporter generates a readable constant:
using Narratyr;
using Narratyr.Game;
NarratyrRuntime.Quests.DiscoverQuest(QuestIds.DownWithTheJarl);
NarratyrRuntime.Quests.StartQuest(QuestIds.DownWithTheJarl);
var activeQuests = NarratyrRuntime.Quests.GetActiveQuests();
var state = NarratyrRuntime.Quests.GetQuestState(QuestIds.DownWithTheJarl);
QuestIds values are stable graph ids. Renaming a quest preserves its id but changes
the constant name on the next package installation. Update your game code to match.
Use GetQuestInstance(), GetQuestDefinition(), GetQuestsByState(),
GetQuestPrerequisites(), and AreQuestStartRequirementsSatisfied() to build a quest log
or check availability.
Reaching an Objective node activates it. Your game can update that objective through the manager:
NarratyrRuntime.Quests.UpdateObjective(
QuestIds.DownWithTheJarl,
"the-stable-objective-node-id",
status: "completed");
The status accepts "completed", "failed", "not_applicable", or "active" to reopen
a terminal objective. Optional hidden and optional arguments change those flags.
Omit an argument, or pass null, to leave it unchanged.
For fragment objectives, use the executing parent quest's id and the fragment objective's
stable node id. CompleteQuest(), FailQuest(), and ResetQuest() handle explicit
quest lifecycle changes.
Standalone dialogues
Create a flow player, connect its events, then call Play(). This component logs dialogue
text. Replace its presentation handler with your own UI:
using Narratyr;
using Newtonsoft.Json.Linq;
using UnityEngine;
public sealed class Conversation : MonoBehaviour
{
[NarratyrGraphId(NarratyrGraphKind.Dialogue)]
public string GraphId;
private NarratyrFlowPlayer flow;
public void Begin()
{
var graph = NarratyrRuntime.Graphs.ResolveGraph(GraphId);
if (graph == null) return;
flow?.Stop();
flow = NarratyrRuntime.CreateFlowPlayer();
flow.DialogueNodeEntered += OnDialogue;
flow.FlowError += message => Debug.LogError(message);
flow.Play(graph);
}
private void OnDialogue(JObject node)
{
Debug.Log((string)node["text"]);
}
public void Continue() => flow?.ContinueFlow();
private void OnDestroy() => flow?.Stop();
}
Play(graph) starts at the graph's entry node. An optional second argument selects a
stable start-node id. The shared factory connects variables, graph resolution, and quest
status queries. Stop a standalone player when its owner no longer needs it.
Entities, portraits, and graph metadata
The dialogue node's speakerEntity field stores a stable Narratyr entity id:
string speakerId = (string)node["speakerEntity"];
var speaker = NarratyrRuntime.Entities.FindEntityById<NarratyrEntity>(speakerId);
if (speaker != null)
{
string name = speaker.LocalizedDisplayName;
Texture2D portrait = NarratyrRuntime.Assets.Load<Texture2D>(speaker.ProfileImage);
}
NarratyrEntity provides Id, TechnicalName, DisplayName,
DisplayNameLocalizationKey, and ProfileImage. LocalizedDisplayName resolves the
active locale and falls back to the authored display name.
Use a generated class such as CharacterEntity with FindEntityById<T>() to read its
Property Set fields. FindEntity<T>(gameId) looks up the editable external id instead.
NarratyrIdToGameId() and GameIdToNarratyrId() convert between the two identities.
Portraits and asset properties use different references. ProfileImage is a stable
asset id, so load it through NarratyrRuntime.Assets. For ordinary asset properties,
pass their exported Resources paths directly to Resources.Load<T>(path).
Loading a Sprite requires Unity's Sprite import settings. Loading a Texture2D works
with ordinary texture imports. Missing, excluded, or deleted asset ids return null.
Graph metadata remains JSON data:
var metadata = NarratyrRuntime.Graphs.ResolveGraphMetadata(graphId);
var difficulty = NarratyrRuntime.Graphs.GetGraphMetadataField(graphId, "Difficulty");
The field lookup uses the exported PascalCase name and returns null for a missing field.
Node event payloads are JSON objects. Read custom node fields from the fieldValues array.
Flow events you need to handle
Bind on the quest manager for quest content
Connect handlers before starting quests or restoring a save:
| Event | Payload | Purpose |
|---|---|---|
QuestFlowPlayerReady |
(questId, flowPlayer) |
Attach flow events before this player runs |
QuestDialogueStarted |
(questId, flowPlayer, node) |
Present quest dialogue using the supplied player |
QuestCustomNodeStarted |
(questId, flowPlayer, node) |
Handle custom content and resume it if blocking |
QuestStateChanged |
(questId, state) |
Refresh quest state in your UI |
ObjectiveStateChanged |
(questId, objective) |
Refresh objective state, visibility, and optional status |
QuestProgressChanged |
(questId) |
Refresh a quest after quest or objective changes |
Subscribe a flow-error handler before each quest flow starts:
NarratyrRuntime.Quests.QuestFlowPlayerReady += OnQuestFlowReady;
void OnQuestFlowReady(string questId, NarratyrFlowPlayer flow)
{
flow.FlowError += message => Debug.LogError(questId + ": " + message);
}
A quest can own several flows. Retain the player supplied with the dialogue or custom
node event so the response resumes that flow. Restoring a save creates new players and
raises QuestFlowPlayerReady for each before it resumes. Remove your event subscriptions
when you destroy the UI or game system that registered them.
The flow player's own events
NodeEntered(nodeId, nodeType) and NodeExited(nodeId, nodeType) report node traversal.
Typed events supply a JObject, including DialogueNodeEntered, ChoiceEntered,
CustomNodeEntered, ObjectiveEntered, GateEntered, and InstructionEntered.
| Event | Meaning |
|---|---|
GraphPushed(fromGraphId, toGraphId) |
A dialogue or fragment call started |
GraphPopped(toGraphId) |
Execution returned to the parent graph |
EventListenerFired(node) |
A listener is about to interrupt the current presentation |
FlowEnded(reason) |
Execution ended as Natural, Stopped, Error, or DeadEnd |
FlowError(message) |
Evaluation, missing-data, or routing diagnostics |
StartQuestAsyncEntered reports a quest start the runtime has already requested. It
doesn't require your handler to start the quest again.
Suspend the old presentation when an event listener interrupts it. Treat DeadEnd as a
content error during development: outgoing edges exist, but no route is passable.
Build the dialogue interface
At a dialogue or choice stop, call IsAtPlayerChoiceMenu(). If it returns false, show a
Continue button that calls ContinueFlow(). Otherwise, build buttons from
GetAvailablePaths().
Each NarratyrPath includes SlotId, ChoiceLabel, LabelOnDisabled, Passable,
Hidden, Disabled, and BlockedReason. Hide paths marked Hidden. Enable a button only
when its path has Passable == true and Disabled == false.
Choose(index) indexes all passable paths, regardless of their presentation flags. It
doesn't use the visible button index. Conditions can change while the menu is open, so
store the button's stable SlotId and resolve the index when selected:
public static bool TryChooseSlot(NarratyrFlowPlayer flow, string slotId)
{
if (!flow.IsAtPlayerChoiceMenu()) return false;
int index = 0;
foreach (var path in flow.GetAvailablePaths())
{
if (!path.Passable) continue;
if (path.SlotId == slotId)
{
if (path.Hidden || path.Disabled) return false;
flow.Choose(index);
return true;
}
index++;
}
return false;
}
Refresh the menu if the selection is no longer available. Localize choice labels before displaying them, as described below.
Stopping on other node types
Flows stop at dialogue, choice, and blocking custom nodes by default. Routing and instruction nodes advance automatically. Gates wait until their conditions pass.
To also pause at objectives, add their type before playback, or in QuestFlowPlayerReady:
flow.StopOnTypes.Add("objective");
Your game must call ContinueFlow() after handling a stop it adds. A custom node with
blocking turned off emits its event and continues automatically.
TryContinue() advances only when a route is passable. Otherwise it stays at the current
node. NarratyrRuntime.Quests.TryAdvanceQuest(questId) applies this to the primary quest
flow.
Variables and quest-state changes refresh gates and listeners automatically. After a
change to game-owned state, such as inventory, call
NarratyrRuntime.Variables.NotifyExternalStateChanged(). Notify on both falling and
rising changes so event listeners can re-arm.
Implementing functions
Declare functions in Narratyr, then select Install Package. The
generated Narratyr.Game.FunctionHost has a protected virtual method for each
game-defined function. Subclass it in your own script and override those methods.
For a function named hasInventoryItem that returns bool and takes one string parameter:
using System.Collections.Generic;
using Narratyr.Game;
public sealed class GameFunctions : FunctionHost
{
public readonly HashSet<string> Inventory = new HashSet<string>();
protected override bool hasInventoryItem(string itemId)
{
return Inventory.Contains(itemId);
}
}
Register the instance before running any graphs:
var functions = new GameFunctions();
NarratyrRuntime.Variables.FunctionHost = functions;
functions.Inventory.Add("Sword");
NarratyrRuntime.Variables.NotifyExternalStateChanged();
Match the generated signatures. Integer parameters use long, numbers use double,
and true/false values use bool. Unimplemented methods log a warning and return the
default value for their return type.
Narratyr handles its built-in functions, including traversal counters, quest-status
queries, list operations, random values, and debugLog. Those don't need overrides.
Gates and Event Listeners require functions declared pure. Keep those implementations free of side effects and notify the runtime when the state they read changes.
Save and restore
NarratyrRuntime.CaptureSaveState() captures variables, traversal history, quests,
objectives, and every live quest flow. Store the snapshot inside your game's save format:
using Narratyr;
using Newtonsoft.Json;
string json = JsonConvert.SerializeObject(NarratyrRuntime.CaptureSaveState());
// After reading the JSON from your save file:
var snapshot = JsonConvert.DeserializeObject<NarratyrSaveState>(json);
var result = NarratyrRuntime.RestoreSaveState(snapshot);
if (result == NarratyrRestoreResult.RefusedMajorMismatch ||
result == NarratyrRestoreResult.SubsystemsMissing)
{
// Report a failed load in your game's UI.
}
The helper restores variables and traversal history before quests, so resumed conditions see the loaded values. Register the function host and quest event handlers before restoring.
- Keep both version fields.
Versionis the major version andMinorVersionis the minor version. A major mismatch refuses the restore and leaves current state unchanged. A minor mismatch logs a warning and attempts the restore with defaults for missing fields. - Check the result. Successful results are
RestoredandRestoredWithMinorMismatch. The runtime doesn't migrate saves across major versions. - Restore through the helper. Individual variable and quest setters can't restore objective activation order, call stacks, or concurrent flows.
- Objectives match by stable id. Reordering them is safe. Removed objectives are skipped, and new objectives retain their authored defaults.
- Standalone flows need their own snapshot. Store their
CaptureState()result in your save file. Restore shared state first, then create and bind the standalone player before calling itsRestoreState().
To start a new game, stop standalone flows before calling
NarratyrRuntime.Quests.ResetAll() and NarratyrRuntime.Variables.ResetToDefaults().
Refresh your UI after these calls because both resets run without change events.
NarratyrRuntime.Reset() discards the shared managers. Stop existing flows before using
it to reload exported data. The new managers need their own function host and event
subscriptions because references retained by your game still point to the old objects.
Localization, interpolation, and rich text
Narratyr → Import Localization CSV reads
Assets/NarratyrData/Localization/narratyr_strings.csv into the Narratyr Strings
collection. Keep that collection name: the runtime uses it for lookups.
NarratyrRuntime.Localization.Tr(key) reads the active locale. It doesn't expand
Narratyr variables or text directives. Translate first, then interpolate:
string nodeId = (string)node["id"];
string key = nodeId + ".text";
string source = (string)node["text"] ?? "";
string translated = NarratyrRuntime.Localization.Tr(key);
string text = translated == key ? source : translated;
string displayed = NarratyrRuntime.Variables.InterpolateDialogueText(text, nodeId);
InterpolateDialogueText() handles contextual @{visited, ...} directives alongside
variables, select, and plural. Use InterpolateVariables() for other text. See
Text directives for authoring syntax.
Choice labels use slotId + ".label". For a slot with Disabled set and an alternate label,
use slotId + ".labelOnDisabled", with LabelOnDisabled as the source text. Apply
InterpolateVariables() after translation.
Quest definitions returned by GetQuestDefinition() contain localized text. Objectives
provide GetName(), GetDescription(), and GetGroupName(). Translatable graph metadata
uses graphId + ".metadata." + fieldName.
In Narratyr's project settings, select Unity (TextMeshPro) as the rich text format. Display the result in a TextMesh Pro component with rich text enabled. Your game controls fonts, colors, and layout. Exporting with another engine's markup selected produces a warning.
The NarratyrLocalization/translator-source.csv and translator-source.xlf files
include speaker, graph, and role context for translators. They live outside Assets
and aren't runtime resources. Keep translated content in your localization workflow
because later exports replace the generated source files.
Automatic string imports
Automatically import Narratyr strings is a project setting, saved under
ProjectSettings/NarratyrLocalizationSettings.asset. Include that file in version control
to share the choice with your team. Installing or updating the package preserves it.
When enabled, Unity imports changes to the exported narratyr_strings.csv. Narratyr
controls the source-language text: the next changed CSV can replace edits you make directly
to those values in Unity. Edit source text in Narratyr.
Automatic imports preserve other languages, entry metadata, and keys absent from the CSV. They update the source table and shared keys without changing your locale selection, default table, Localization Settings, or asset-loading configuration. The importer saves only affected localization assets and skips unchanged CSV content.
The collection and source-language table must already exist. If you change Source Locale or replace the collection, run Narratyr → Import Localization CSV once to confirm the new target. Missing setup or an invalid CSV skips the automatic import with a Console warning. The importer validates the whole CSV before changing any strings.
Imports wait until Unity finishes compiling and importing assets, and until you leave Play mode. The manual command remains available for retries or to reapply unchanged CSV content. Turn off the project setting if your team uses a custom import workflow.
Editor tooling
Variable debugger
Open Narratyr → Variable Debugger to inspect variables. Outside Play mode it shows read-only exported defaults. During Play mode, once the game has accessed the shared variable manager, it shows live values that you can edit through that manager.
Use the filter to find a variable and Refresh to reload exported defaults. Live edits trigger the same notifications as game code, so they can wake gates and listeners.
Graph and entity pickers
Add attributes to serialized string fields to select content by name while storing its stable id:
using Narratyr;
using UnityEngine;
public sealed class StoryReferences : MonoBehaviour
{
[NarratyrGraphId(NarratyrGraphKind.Dialogue)]
public string Conversation;
[NarratyrGraphId(NarratyrGraphKind.Quest)]
public string StartingQuest;
[NarratyrEntityId("character")]
public string Speaker;
}
The entity filter uses the Entity Type's exact technical name, here character. Omit
the attribute argument to browse all entities. [NarratyrGraphId] accepts any graph kind.
The pickers read exported data. Run Export Data after adding or renaming content. Missing references keep their stored ids so an Inspector refresh doesn't erase them.
Iterating
After content edits, select Export Data and wait for Unity's import to finish. If automatic string imports are off, run Narratyr → Import Localization CSV. Restart Play mode to load the new defaults and graphs.
After schema changes or a Narratyr update, also run Install Package and let Unity compile. Keep game code in your own folders so later installations preserve it.
From the command line
The narratyr CLI runs the same exporter:
narratyr export --engine unity --operation both \
--project /path/to/MyStory.ntproj \
--out /path/to/MyUnityProject \
--source-locale en
--operation accepts install, data, or both. --json emits a machine-readable
result for CI. The CLI reads the license installed by the desktop app, so the same
entitlements apply. Automatic imports also process CLI exports when Unity imports the
changed CSV. If the option is off, run the localization import command in Unity.
Troubleshooting
The Narratyr menu doesn't appear. Check that the export path contains Assets,
Packages, and ProjectSettings. Run Install Package, wait for dependency resolution,
and fix the first C# compilation error in Unity's Console.
A generated type or function is missing. Run Install Package after changing Entity Types, Property Sets, enumerations, or function declarations. Check your game's assembly references if the generated code compiles but your scripts can't see it.
A manager reports missing JSON, or shows old data. Run Export Data and restart Play
mode. Check Assets/NarratyrData/Resources/NarratyrData/. Managers load exported data
when constructed and keep using that data until you replace them.
Text shows keys or old translations. Configure a Locale matching Source Locale, then run Narratyr → Import Localization CSV. Check the Narratyr Strings collection and your active locale. JSON graph data still contains source text, so the UI must translate it.
Automatic imports aren't updating strings. Check Project Settings → Narratyr Localization, leave Play mode, and read the Console warning. A changed source locale or replacement collection needs a manual import before automatic updates resume.
A portrait is missing. Load ProfileImage through NarratyrRuntime.Assets. Confirm
that the asset isn't excluded from export, and check the Unity import settings for the
type you load. Sprite loading needs a Sprite import.
Export reports an asset path collision. Unity Resources paths omit extensions and
ignore case. Rename one conflicting asset, such as hero.png and hero.jpg in the same
folder, or exclude one from export.
A gate or listener doesn't react to inventory changes. Register your function host,
declare the query pure, and call NotifyExternalStateChanged() after changes to the
game-owned state it reads.
A choice button selects the wrong response. Choose() uses the passable-path index.
Resolve the current path by SlotId when the player selects the button.