Documentation menu

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

  1. Choose the Unity project path in Narratyr and select Install Package.
  2. Select Export Data.
  3. Open the Unity project and let package resolution, compilation, and asset imports finish.
  4. 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.
  5. Select Narratyr → Import Localization CSV. This creates or updates the Narratyr Strings String Table Collection under Assets/NarratyrData/Localization/Tables/.
  6. 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.
  7. 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. Version is the major version and MinorVersion is 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 Restored and RestoredWithMinorMismatch. 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 its RestoreState().

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.