Skip to content

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
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

LayerWhat it adds
DialogueEngineThe traversal itself. Nothing Unity-aware.
EngineSessionAssembles a payload, a script runtime and a view context into one thing, and replays the payload’s own diagnostics into its channel
TaleShardPlayerThe policy, PlaythroughEnd, language codes rather than storage keys, a coroutine wait, console diagnostics, and guard rails on every verb
TaleShardPlayerBehaviourLifetime 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

EventRaised when
Action<PlayStep>? StepPresentedA moment the player sees. Draw it.
Action<EngineErrorEvent>? ErrorRaisedThe 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>? EndedThe playthrough is over, however it ended. Exactly once per playthrough.
Action<string>? LanguageChangedThe 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

PropertyNotes
Story · EngineThe session, and its engine
PolicyWhat this host declared. Frozen.
CurrentStepThe step on screen, or null
IsPlayingA story is under way — neither idle nor finished
IsAwaitingChoice
IsSuspendedA command is running and the story is waiting
PendingCommandCodeLets you say cannot save during startCombat rather than cannot save
LastEndHow 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

PropertyWhat it is
LanguageThe project’s code — en, it. What UIStrings, a resource path and a picker all want. "" when the project never named a master language.
LanguageKeyThe engine’s storage spelling. Only for a host talking to Story.View directly.
LanguagesEverything 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

MethodNotes
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.

ReasonPredicateMeaning
end-of-pathReachedTheEndThe story ran out of story
stoppedWasStoppedSomebody called Stop()
errorFailedThe engine could not carry on
rewoundWasRewoundThe 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.

MemberDeclares
SilentNodeTypesNode types this host never shows. Wins over AlwaysPresentNodeTypes.
AlwaysPresentNodeTypesNode types this host always shows, even with no text — a node drawn as a beat
SilentFieldsIndividual fields, as (node type, field code)
BoardEntriesWhat 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

MemberMeaning
EndThePathThe default. A path that ends, ends.
OfferWaysInWhen 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.

MemberNotes
StepPresented · ErrorRaised · EndedForward the player’s own, subscribable before a story exists
PlayerThe 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 handlersDispose detaches the player from the session; it does not clear your subscriptions. A presenter outliving its subscription unsubscribes itself.
MediaRefcounted and never auto-freed — see loading media
Never onErrorIt 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.

Written for
Unity developers · Advanced integrators
Last updated