Appearance
Tarinoi for Unreal Engine
Plugin Tarinoi 0.2.0 – requires Unreal Engine 5.8 and a C++ project
Read Game Engine Plugins first if you have not – it covers what this plugin does and, just as importantly, what it leaves to you.
Pre-1.0
Version 0.2.0. The API will change before 1.0. Unreal Engine 5.8 only – there are no backports to earlier 5.x releases, and there will not be any before 1.0.
You will need: a Tarinoi project with some dialogue in it, permission to create an API token for it, and a C++ project with a working compiler (Xcode on macOS, Visual Studio on Windows). The plugin ships as source, and the bindings it generates for your content are C++ classes. A Blueprint-only project can be converted by adding any C++ class (Tools → New C++ Class).
1. Install the plugin
The plugin is the Tarinoi folder in the plugin repository. Put it in your project's Plugins folder, either by copying it:
YourGame/
Plugins/
Tarinoi/
Tarinoi.uplugin
Source/...or by cloning the repository there, which lets you pull updates:
bash
cd YourGame/Plugins
git clone https://github.com/tarinoi/tarinoi-unreal-plugin.gitUnreal finds a plugin anywhere under Plugins, so the clone's extra folder level does no harm.
Then add "Tarinoi" to your game module's dependencies, in Source/YourGame/YourGame.Build.cs:
csharp
PublicDependencyModuleNames.AddRange(new[] { "Core", "CoreUObject", "Engine", "Tarinoi" });Open the project and let it rebuild. SQLite comes with the engine – the plugin enables Unreal's own SQLite plugin as a dependency – so there is nothing else to install.
Pinning to a specific version
A clone tracks the repository's default branch. Check out a tag (git checkout v0.2.0) or commit to pin it; worth doing while the plugin is pre-1.0 and the API is still moving.
2. Point Unreal at your Tarinoi project
Open Edit → Project Settings → Plugins → Tarinoi.
- API Path – paste the documents endpoint from your Tarinoi project's Integrate wizard. It ends in
/documents. The Project row underneath fills in automatically; if it does not, the path is wrong. - API Token – click Set… and paste a token from the same place. A Read token is enough.
The settings are saved to Config/DefaultTarinoi.ini, a file of their own, so you can decide whether your team shares one project pointer (commit it) or each developer sets their own (ignore it). The token is not in there: it is stored in your user folder, outside the project, so it is never committed and never ends up in a build.
3. Sync
Tools → Tarinoi → Sync.
A notification reports what arrived – for example sync complete: 281 upserted, 737 removed, 22 collections – and the details go to the Tarinoi message log (Window → Message Log). Syncing again is incremental; only what changed comes down.
4. Generate your bindings
Tools → Tarinoi → Regenerate Bindings.
This writes C++ into your game module, under Source/YourGame/Tarinoi/Generated, describing what your authors declared:
| Authored | Generated |
|---|---|
A function collection, Fn.global.* | UTarinoiGlobalFunctions: an abstract, Blueprintable class with one BlueprintNativeEvent per function |
A variable collection, Var.global.* | UTarinoiGlobalVariables: a typed, Blueprint-editable UPROPERTY per variable, initialised to the author's default |
Option lists, Ls.global.thresholds.* | Key constants in TarinoiLists::Global::Thresholds |
| Dialogue-capable entities | Identifier constants in TarinoiEntities::… |
Then rebuild. New classes cannot be hot-reloaded: close the editor and build from your IDE, or reopen the project and let it compile. Live Coding cannot add them.
The generated functions are stubs that log "not implemented". Deriving from the class and implementing the events is how your game gives those authored names meaning – in C++:
cpp
UCLASS()
class UMyGlobalFunctions : public UTarinoiGlobalFunctions
{
GENERATED_BODY()
virtual bool HasKeycard_Implementation(const FTarinoiValue& Door) override
{
return Inventory->Has(Door.ToString());
}
};or in Blueprint: create a Blueprint class based on TarinoiGlobalFunctions and implement its events, which appear under Functions → Override.
Every argument arrives as an FTarinoiValue, because an author may pass a literal, a list option, an entity, or a variable. ToBool, ToNumber and ToString read any of them. A Var.* argument arrives as a reference rather than a value, which is what lets a function write back with Write – Blueprints have To Bool / To Number / To String (Tarinoi Value) and Write Variable for the same.
Authored names that clash
An authored name that would collide with something every Unreal object already has – Rename, GetName, IsValid and the like – gets a prefix: FnRename, VarIsValid. The authored name is still what the dispatcher matches; only the C++ member is renamed.
Core functions
The tarinoi collection is Tarinoi's own core function set – flags, counters, text and comparisons – with fixed semantics that in-app playback already implements. You do not write these: when the synced project has the collection, Regenerate Bindings also writes a reference implementation, UTarinoiCoreFunctions (TarinoiCoreFunctions.h and .cpp), into the implementations folder (Codegen Impl Path, by default Source/YourGame/Tarinoi).
That file is yours. It is written once and never overwritten, so edit it freely – route the reads and writes through your save system, add logging, whatever the game needs. Delete both files to get a fresh copy on the next regenerate. The class carries the version of the set it was rendered from as metadata; Check Bindings warns when the plugin has moved on, or when a function in the synced collection is not implemented in the file.
Bind it under the collection's identifier:
cpp
Runtime->GetBindings()->BindFunctions(TEXT("tarinoi"), NewObject<UTarinoiCoreFunctions>(this));The quickstart game mode does this on its own – along with binding any generated variables class you have not bound yourself (UTarinoiBindings::BindGeneratedDefaults) – so a project that only uses core functions plays with no bindings code at all.
Two things to keep if you rewrite it. A Var.* argument arrives as a reference, which is how the mutators write back; the comparators read anything. And unset variables read as false, 0 and "": that is what an author verified in playback, and the game must agree.
Variable classes can be overridden the same way. The generated class keeps the values in its properties; to keep a variable somewhere else, derive and override GetVariable / SetVariable (both BlueprintNativeEvents), calling Super for the rest:
cpp
FTarinoiValue UMyVariables::GetVariable_Implementation(FName Name)
{
if (Name == TEXT("pc_health"))
{
return FTarinoiValue::MakeNumber(SaveGame->PlayerHealth);
}
return Super::GetVariable_Implementation(Name);
}The name passed to the accessors is the author's declared variable name, as it appears in Var.global.*.
Re-run Regenerate Bindings whenever authors add or rename something. Tools → Tarinoi → Check Bindings reports what has drifted without writing anything – which additions are harmless, and which changes will break code that overrides the generated classes.
Commit the generated folder, or say so in your README
Code that derives from generated bindings does not compile in a fresh clone if the generated folder is ignored. Either commit it, or document that Sync, Regenerate Bindings and a rebuild come first.
5. Play it
Open any level, set World Settings → GameMode Override to TarinoiQuickstartGameMode, and press Play.
You get a list of every entry point in your content. Pick one and the dialogue plays: Space continues, number keys choose, Esc ends the dialogue. That confirms the whole chain works: sync, bindings, and playback.
To register your own bindings, derive from ATarinoiQuickstartGameMode – in C++ or Blueprint – and implement Setup Bindings; it runs once the runtime is configured and before any dialogue plays. The quickstart project has an example.
Wiring it into your own game
The quickstart game mode is a development tool, not a starting point for your UI. A real game does three things.
cpp
// 1. Get the runtime. UTarinoiSubsystem configures it from the project settings when the game starts.
UTarinoiRuntime* Tarinoi = UTarinoiSubsystem::GetTarinoiRuntime(this);
// 2. Register your bindings, before any dialogue runs.
Tarinoi->GetBindings()->BindVariables(TEXT("global"), MyVariables);
Tarinoi->GetBindings()->BindFunctions(TEXT("global"), MyFunctions);
Tarinoi->GetBindings()->BindFunctions(TEXT("tarinoi"), NewObject<UTarinoiCoreFunctions>(this));
// 3. Handle the events, and drive it from your own UI.
Tarinoi->OnLineReady.AddDynamic(this, &UMyDialogueWidget::ShowLine);
Tarinoi->OnChoicesReady.AddDynamic(this, &UMyDialogueWidget::ShowChoices);
Tarinoi->OnDialogueEnded.AddDynamic(this, &UMyDialogueWidget::Close);
Tarinoi->StartDialogue(CollectionId, CardId);Then Advance() past a line, and SelectChoice(Index) to take an option. All of it is available in Blueprint too: Get Tarinoi Runtime, Bind Functions, the events under Assign, and the calls under Tarinoi|Dialogue.
Bind the machine identifier, not the label
A collection shown as "Global State" in Tarinoi may be global in expressions, and global is what you register. Getting this wrong fails when the dialogue runs, not when you bind. Identifiers are case-sensitive.
Event reference
UTarinoiRuntime lives for the game instance, owned by UTarinoiSubsystem. Everything runs on the game thread, and events fire synchronously from the call that caused them – StartDialogue raises the first line before it returns.
| Event | Payload | Raised when |
|---|---|---|
OnLineReady | FTarinoiDialogueLine | A line is ready to display. Call Advance() when the player moves on. |
OnChoicesReady | TArray<FTarinoiDialogueChoice> | The player has options, already ordered for display. Call SelectChoice(Index). |
OnChoiceMade | FTarinoiDialogueLine | A choice was taken – useful for echoing the player's line into a log. |
OnDialogueEnded | – | Flow reached an end. Close your UI here. |
OnDialogueError | FString | Traversal stopped on an authoring or binding problem. |
OnPinChoiceNeeded | TArray<FString> | Development aid: a card needs a pin picked by hand with SelectPin. Not a player-facing case. |
OnSyncStarted / OnSyncProgress / OnSyncCompleted / OnSyncFailed | – / message and fraction / FTarinoiSyncStats / FString | Content sync lifecycle, for editor tooling and loading screens. |
FTarinoiDialogueLine carries Line, EntityRef, EntityLabel, LineMode, BaseRef, TemplateRef, CardId, CollectionId, bIsSystem and Data. FTarinoiDialogueChoice adds Index and bVisited and drops the label fields you do not need for a button.
Data holds the card's template-defined properties – whatever your authors declared. This is where you read a mood, a camera cue, an audio reference. In Blueprint it is a map of strings (nested values as JSON); from C++, DataJson has the parsed object.
Starting dialogue from the world
| Class | Behaviour |
|---|---|
UTarinoiDialogueTrigger | A component holding a CollectionId and CardId. Raises OnTriggered when your code calls Trigger(). In the editor, its Start Card dropdown lists the synced entry points. |
ATarinoiDialogueVolume | A trigger box carrying one. In OnEnter mode it fires the moment a player-controlled pawn walks in; in WhileInside mode it fires nothing by itself and instead raises OnOccupantEntered / OnOccupantExited and answers IsOccupied(). |
WhileInside is the shape most games with world dialogue want: the player walks into range, your game shows a prompt, and they decide to talk.
cpp
Volume->OnOccupantEntered.AddDynamic(this, &AMyHud::ShowTalkPrompt);
Volume->OnOccupantExited.AddDynamic(this, &AMyHud::HideTalkPrompt);
Volume->DialogueTrigger->OnTriggered.AddDynamic(this, &AMyGameMode::StartTalking);
// from your player controller, on the interact key:
if (Volume->IsOccupied()) { Volume->DialogueTrigger->Trigger(); }The separation is deliberate – the trigger notices, your game decides. Prompt UI, range checks, and whether the player may talk right now stay yours.
Replacing the dialogue UI
The bundled widgets – UTarinoiDialogueStripWidget, UTarinoiStartPickerWidget and UTarinoiQuickstartWidget – are debug views built in C++ at runtime: plain, unstyled, and not intended to ship. To use your own, do not add them: build your UI however you like (a Widget Blueprint is the usual answer) and bind it to the events above.
Console commands
While a game is running, Tarinoi.List, Tarinoi.Start <CollectionId> <CardId>, Tarinoi.StartFirst [N], Tarinoi.Advance, Tarinoi.Choose <N>, Tarinoi.Abort and Tarinoi.Sync drive the dialogue from the console – handy for testing content without clicking through it, or from -ExecCmds.
Remembering what the player has seen
Two features need to know which cards a player has already been shown: the bVisited flag on a choice, which lets you dim options already taken, and the shown once flag an author can tick on a card.
The plugin stays stateless about it. It asks for a dialogue's seen cards when the dialogue starts and hands the updated set back when it ends – persisting them is your job, keyed by the start card's ID. Implement ITarinoiHistoryStore, in C++ or on a Blueprint (Class Settings → Interfaces):
cpp
UCLASS()
class UMySaveHistory : public UObject, public ITarinoiHistoryStore
{
GENERATED_BODY()
virtual TArray<FString> GetVisited_Implementation(const FString& StartCardId) override
{
return MySave->LoadSeenCards(StartCardId);
}
virtual void SaveVisited_Implementation(const FString& StartCardId, const TArray<FString>& VisitedIds) override
{
MySave->StoreSeenCards(StartCardId, VisitedIds);
}
};
Tarinoi->SetHistoryStore(NewObject<UMySaveHistory>(this));UTarinoiInMemoryHistoryStore is supplied for when this only needs to hold for the current play session. Set none and nothing survives past the current dialogue.
A card joins the seen set the moment the player actually sees it: an NPC line when it is displayed, a player line when it is chosen. An option offered but not taken does not count.
The shown once flag
When an author ticks shown once, a card the player has already seen stops being a valid continuation – the "ask this only once" pattern, without a flag per card in your game code.
A spent card is skipped wherever the runtime would otherwise go to it, and its functions do not run, since nobody saw it. What that means depends on what else is available:
- Offered alongside other options – dropped from the choice set, before its entry condition is even evaluated.
- The last option standing – followed directly, with no choice UI.
- The only way forward – the dialogue ends, as a dead end like any other.
Without a history store the flag still works inside a single dialogue – a hub the player loops back to will not re-offer a spent option – but it resets when the dialogue ends.
Dead ends
A card with nowhere valid to go ends the dialogue and logs an error, handing the player back to your game rather than leaving them stuck. Every cause is treated the same way: no connections, no connection naming a target, every entry condition false, every remaining candidate a spent shown-once card, or a spent shown-once card that was the only continuation.
The message names the card and says which case it was. Tarinoi's health check flags graphs where a dead end can arise, at authoring time. If a shown-once card must stay reachable after it is spent, give its source a fallback option without the flag.
Shipping a build
This step is required, not optional. A packaged game cannot see the content you synced in the editor: it keeps its own Saved folder, separate from the project's. Ship without a snapshot and your game starts with no dialogue at all.
- Sync everything you want to ship.
- Tools → Tarinoi → Export Snapshot – copies the content to
Content/Tarinoi, stripping the API path and sync cursor. The first time, it also adds two lines toConfig/DefaultGame.ini: one to stageContent/Tarinoi, which holds no assets and would otherwise be skipped, and one to tell packaging thatDefaultTarinoi.iniis meant to ship. - Tick Offline Mode in Project Settings → Plugins → Tarinoi.
The game then copies that snapshot into place on start and never contacts the network. Re-export whenever you want shipped content to change.
The same separation applies to your API token, deliberately: it lives outside the project and does not travel into a build.
From a build script, the Tarinoi commandlet runs the same steps without the editor:
bash
UnrealEditor YourGame.uproject -run=Tarinoi -Sync -Generate -unattended -nullrhi
UnrealEditor YourGame.uproject -run=Tarinoi -Export -unattended -nullrhiSettings reference
Project Settings → Plugins → Tarinoi, saved to Config/DefaultTarinoi.ini.
| Setting | Default | Meaning |
|---|---|---|
| API Path | – | The project's documents endpoint, ending in /documents. |
| API Token | – | Set through the masked dialog; stored in your user folder, outside the project. |
| Poll Enabled / Poll Interval | off / 10 | Re-sync periodically while playing, so authored changes appear without a restart. Leave it off for builds. |
| Codegen Module | the project's first runtime module | The game module generated bindings are written into. |
| Codegen Output Path | Tarinoi/Generated | Where generated bindings are written, inside the module's source folder. Regenerated every time. |
| Codegen Impl Path | Tarinoi | Where your own binding implementations live, inside the module's source folder. The core functions reference implementation is scaffolded here, once, for you to edit. |
| Codegen On Sync | off | Regenerate bindings after every successful sync from the editor. |
| Committed Only | off | Show only committed content, hiding uncommitted edits – what a player would see. |
| Log Level | Log | How much the LogTarinoi category writes to the Output Log. |
| Offline Mode | off | Play from the bundled snapshot and never contact the API. Required for builds. |
| Document Store Class | – | A UTarinoiDocumentStore subclass to read content from instead of the built-in SQLite store. |
Tools menu
Tools → Tarinoi: Sync · Export Snapshot · Clear Local Content · Regenerate Bindings · Check Bindings · Set API Token… · Settings…
Command line
UnrealEditor YourGame.uproject -run=Tarinoi [-ApiPath=<url>] [-TokenFile=<path>] [-Sync] [-Generate] [-Check] [-Export]| Option | What it does |
|---|---|
-ApiPath=<url> | Points a fresh checkout at a project without opening the editor. |
-TokenFile=<path> | Stores the token read from a file. A token on the command line would show up in process lists and logs, so there is no option for that. |
-Sync | Syncs. |
-Generate | Regenerates bindings (and scaffolds the core functions once). |
-Check | Compares the compiled bindings with the content; exits non-zero on a breaking difference. |
-Export | Writes the shipping snapshot. |
Going further
Custom document stores
UTarinoiDocumentStore is the seam between the runtime and storage. UTarinoiSqliteDocumentStore is the default. Subclass it to serve content from somewhere else – a pre-baked cache, your own format, a test double – and select your class under Document Store Class, or call SetDocumentStore on the runtime before it is configured.
The store is synchronous, unlike the Unity plugin's. Unreal's SQLite build allows a single connection per database file, used from the game thread, so there is no background reader to hand work to; a store that needs slow I/O should do it ahead of time and answer from memory.
Troubleshooting
| What you see | What it means |
|---|---|
| "Tarinoi is not set up for this project" | Project Settings → Plugins → Tarinoi, paste the documents URL. |
| "credentials rejected" | The token is wrong or expired. Set it again. |
| "project not found" | The API path points at a project that is not there. Check it. |
| "the server could not be reached" against a local development server | The server's certificate is not trusted. On macOS, trust its certificate authority in Keychain Access. On Windows and Linux, set n.VerifyPeer=False under [/Script/Engine.NetworkSettings] in Config/DefaultEngine.ini for development only – never ship it. |
| The entry-point list is empty | Nothing synced yet, or your content has no start cards. Sync and check the Tarinoi message log. |
| "no bindings registered for function collection 'x'" | Register a binding under x – and check you used the machine identifier, not the label. |
| "…is not implemented. Override it in your bindings class." | The generated default is still in place. Derive from the class and implement the event. |
| "…is not bound. Regenerate your bindings." | An author added a function since you last generated. Run Regenerate Bindings and rebuild. |
| Dialogue stops with "has no pin 'x'" | A card's output selector returned a pin the card does not have. Fix the selector or add the pin. |
| "this project has no C++ module to generate bindings into" | The project is Blueprint-only. Add a C++ class (Tools → New C++ Class) and rebuild. |
Generated code does not compile: Bindings/TarinoiBindings.h not found | Your module does not depend on the plugin. Add "Tarinoi" to PublicDependencyModuleNames. |
| Regenerated bindings do not appear in the editor | New classes need a full rebuild; Live Coding cannot add them. Close the editor and build. |
| "Failed to open database" | Another process has the project's content database open – a second editor, or a standalone game launched from this one. Close it; Unreal's SQLite build allows one connection per file. |
| A build has no dialogue, though the editor does | No snapshot was exported, or Offline Mode is off. See Shipping a build. |
| A build logs "No API token saved" | It is trying to sync. Turn on Offline Mode; players should play a snapshot, not call the API. |
| A fresh clone of your repository does not compile | Your code derives from generated bindings and the generated folder is ignored. Commit it, or document that Sync, Regenerate Bindings and a rebuild come first. |
Under the hood
The plugin source is on GitHub, MIT licensed. If you plan to modify it, start with Writing your own integration for the data contract and Adapting a plugin for how the implementation is laid out. Its automation tests run from Tools → Session Frontend → Automation (filter Tarinoi), or headless with -ExecCmds="Automation RunTests Tarinoi".