The Unity player
TaleShardPlayer and the component that owns one — the four layers, every verb, the policy that freezes at construction, and how a playthrough ends.
All documentation — Reference
Start here
Concepts
Authoring for a game
Playing a story
The generated code
The plugin and sync
On this page
TaleShardPlayer is the one object a Unity game holds to play a story. It is not a UI and ships
none: subscribe to its steps and draw what it hands you.
Four layers, and where to start
| Layer | What it adds |
|---|---|
DialogueEngine | The traversal itself. Nothing Unity-aware. |
EngineSession | Assembles a payload, a script runtime and a view context into one thing, and replays the payload’s own diagnostics into its channel |
TaleShardPlayer | The policy, PlaythroughEnd, language codes rather than storage keys, a coroutine wait, console diagnostics, and guard rails on every verb |
TaleShardPlayerBehaviour | Lifetime and subscription ordering, and nothing else |
Reach for the player’s verbs before the engine’s. They add the guards — refusing a port id where a choice id is wanted, refusing a stale index — and the engine’s own equivalents do not.
Getting one
The ordinary case is attaching to a session somebody else built, which is what the generated bootstrap sets up for you. A second session over the same project would hold its own history and its own idea of where the player is.
new TaleShardPlayer(GameData.Story, policy, onError);
TaleShardPlayer.Create(payload, runtime, policy, language, diagnostics, onError);Create builds the session and the player together, and it is the only way
PlayerPolicy.BoardEntries can be honoured — the engine takes that as a constructor argument and
never re-reads it. The plain constructor throws rather than accepting a value it would silently drop.
TaleShardPlayer.UnityConsole is a ready-made diagnostics sink routing the engine’s channel to
Debug.LogError / LogWarning / Log. Stateless and shareable; nothing is buffered. Pass it by
name, because the other routes into a session default to collecting instead.
Events
| Event | Raised when |
|---|---|
Action<PlayStep>? StepPresented | A moment the player sees. Draw it. |
Action<EngineErrorEvent>? ErrorRaised | The engine could not do what the data asked and carried on. Not an exception, not the end. Unhandled, these reach the Unity console. |
Action<PlaythroughEnd>? Ended | The playthrough is over, however it ended. Exactly once per playthrough. |
Action<string>? LanguageChanged | The language changed and every drawn step has already been re-drawn in it |
LanguageChanged carries the project’s code, never the engine’s storage key — so your own chrome
follows in one line: player.LanguageChanged += uiStrings.SetLanguage;
State
| Property | Notes |
|---|---|
Story · Engine | The session, and its engine |
Policy | What this host declared. Frozen. |
CurrentStep | The step on screen, or null |
IsPlaying | A story is under way — neither idle nor finished |
IsAwaitingChoice | |
IsSuspended | A command is running and the story is waiting |
PendingCommandCode | Lets you say cannot save during startCombat rather than cannot save |
LastEnd | How the last playthrough ended, or null |
CanBack · CanForward |
Gate a Back button on CanBack. Back() from the opening moment rewinds past the beginning:
it restores the variables the story opened with, leaves the engine idle, and blanks the screen.
Language
| Property | What it is |
|---|---|
Language | The project’s code — en, it. What UIStrings, a resource path and a picker all want. "" when the project never named a master language. |
LanguageKey | The engine’s storage spelling. Only for a host talking to Story.View directly. |
Languages | Everything a picker needs, in offer order: master first, then each declared language, blanks dropped and duplicates collapsed. Computed once and cached. |
Draw LanguageOption.Label; hand LanguageOption.Code to SetLanguage.
Driving it
| Method | Notes |
|---|---|
Play() | Begin at the board and node the author chose |
Play(boardId, nodeId?) | Begin on a board |
Restart() | Begin again from the beginning, variables and all |
Restart(boardId, nodeId?) | The same reset, on a board of your choosing |
Advance() | Move on from a step the player has read |
Choose(ResolvedChoice choice) | The overload to prefer |
Choose(string choiceId) | |
ChooseAt(int index) | Positional, and it goes stale |
Lapse() | The countdown ran out and the player picked nothing |
Back() · Forward() | |
Stop() | End it. Always works, including mid-command. |
Redraw() | Re-present because something it depended on resolved late |
SetLanguage(string language) | |
Save() · Restore(save) | |
WaitForEnd() | A coroutine wait |
Dispose() |
Prefer Choose(ResolvedChoice). It resolves by ChoiceId rather than by object identity, so a
choice held across a re-draw still works. ChooseAt is positional and a re-draw rebuilds the list —
a gate whose value changed can drop an option out of it.
Choose(string) refuses a PortId by name, because a port is a handle id and is not unique
across a step’s choices. A choice id that is not on the step throws; one that is on the step but
cannot be taken is reported and does nothing.
Advance() is ignored when the story is not presenting — a player mashing Continue during a fade
is normal, not an error.
Stop() always settles: every in-flight command token goes stale, so an abandoned fade cannot
resume the next playthrough.
Redraw() re-traverses nothing and re-runs no script, so it cannot fire a command twice or re-count
a visit.
WaitForEnd
yield return player.WaitForEnd(); resumes when the playthrough that was running when you asked
has finished — by reaching the end, by Stop(), by an engine error, by a Back() out of the opening
moment, or because a new playthrough started under it.
LastEnd is current by the time it releases, except in that last case. Test IsPlaying to tell them
apart.
How a playthrough ended
PlaythroughEnd has a Reason and four predicates. Prefer the predicates to a string comparison.
| Reason | Predicate | Meaning |
|---|---|---|
end-of-path | ReachedTheEnd | The story ran out of story |
stopped | WasStopped | Somebody called Stop() |
error | Failed | The engine could not carry on |
rewound | WasRewound | The player rewound past the beginning |
Failed is not the same as an ErrorRaised, after which the story continues.
WasRewound is not a failure at all: the variables the story opened with are restored, the engine is
idle, and Forward() takes it back in.
Declaring what your host draws
A PlayerPolicy tells the engine which node types and fields your host never shows — and it matters
more than it looks.
| Member | Declares |
|---|---|
SilentNodeTypes | Node types this host never shows. Wins over AlwaysPresentNodeTypes. |
AlwaysPresentNodeTypes | Node types this host always shows, even with no text — a node drawn as a beat |
SilentFields | Individual fields, as (node type, field code) |
BoardEntries | What happens when a path ends and the board has other ways in |
IsApplied · DeclaresAnything |
Applied once, then frozen. Every write afterwards throws and names the property. And a player refuses to attach a declaring policy to a story that is already playing.
SilentFields silences presentation, never content: the field still reaches you in
PlayStep.Fields and is only absent from Presented. It does not touch a choice’s label.
BoardEntryPolicy
| Member | Meaning |
|---|---|
EndThePath | The default. A path that ends, ends. |
OfferWaysIn | When a path ends with no call frame waiting, offer the board’s remaining ways in as choices |
EndThePath is what a game whose entry nodes are world triggers needs — entered by walking into a
zone or clicking an NPC — since offering the other entries would end every scene in a menu the player
cannot leave.
With OfferWaysIn, those choices arrive as ChoiceKind.BoardEntry, and a board that has ways in
never produces a step flagged IsTerminal.
Only Create can deliver it. The plain constructor throws.
A PlayerPolicyAsset is a ScriptableObject wrapper, so a policy can be authored in the inspector
and Build() into one.
The component
TaleShardPlayerBehaviour — [DisallowMultipleComponent], and it appears in the Add Component menu
under TaleShard.
| Member | Notes |
|---|---|
StepPresented · ErrorRaised · Ended | Forward the player’s own, subscribable before a story exists |
Player | The player it owns, or null |
Require() | |
Attach(EngineSession story) | The ordinary case |
Create(…) | When you need OfferWaysIn |
Adopt(TaleShardPlayer player, bool owned = true) | For a player shared between scenes |
Release() | Called on destroy. Safe to call twice. |
With Adopt(player, owned: false) the caller keeps the disposal duty.
What to dispose
TaleShardPlayer.Dispose() | Detaches from the session. It does not stop the story — call Stop() first if ending it is what you mean. Every WaitForEnd handed out is released. Idempotent; every verb throws afterwards. |
| Your own handlers | Dispose detaches the player from the session; it does not clear your subscriptions. A presenter outliving its subscription unsubscribes itself. |
| Media | Refcounted and never auto-freed — see loading media |
Never onError | It is constructor-injected on purpose, and there is nothing to unsubscribe |
TaleShardPlayerBehaviour.Release() is the model to copy: it removes its own forwards, then disposes
only if it owns the player.