Skip to content

The play loop

EngineSession, DialogueEngine, PlayStep and the enums around them — every member you call, and the handful that exist for the engine rather than for you.

All documentation — Reference
On this page

Three types carry a playthrough: a session holds everything, an engine walks the graph, and a step is one moment the player sees. This page is their public surface.

What a step contains — fields, choices, entities — is a step.

EngineSession

TaleShard.Runtime.Engine.EngineSession — sealed.

Everything the engine needs, built from one payload: a graph source, a script runtime and a view context.

Its constructor is private. There are two ways in.

MemberWhat it does
static EngineSession Create(EnginePayload payload, IScriptRuntime runtime, string? language = null, IEngineDiagnostics? diagnostics = null, bool offerBoardEntries = false)Build a session on a payload you already have
static EngineSession FromDirectory(string directory, IScriptRuntime runtime, string? language = null, IEngineDiagnostics? diagnostics = null)Read the payload from a sync output directory and build a session on it

offerBoardEntries decides what happens when a path ends on a board that has other ways in. Off by default, so a path that ends, ends.

PropertyType
PayloadEnginePayload
GraphIGraphSource
ViewINodeViewContext
RuntimeIScriptRuntime
EngineDialogueEngine
DiagnosticsIEngineDiagnostics
StartBoardIdstring? — where a playthrough begins, or null when the project names no start board

StartBoardId being null is a real answer, not a failure: the author has not set a game start, and the engine will not guess. Guessing the first board in the file would be a plausible wrong answer, which is why it refuses to.

void Start() begins the project’s own start board, at the author’s own start node when they named one — reading only the board would open a different scene from the one the editor shows.

The payload’s own diagnostics are replayed into the session’s channel when it is constructed, so a problem found at export time reaches you at startup rather than never.

DialogueEngine

TaleShard.Runtime.Engine.DialogueEngine — sealed.

What it does at each node

  1. Run every script slot, in document order.
  2. Present a step, if the node has content.
  3. Resolve the exits.
  4. A jumpTarget field overrides all of it.

Every behaviour is derived from the node’s declared shape, never from its type name.

Constants

ConstantValueWhy it exists
MaxAutoAdvances1000Breaks a loop in the graph. Also caps call depth and the terminal look-ahead.
MaxRestartsFromCommand100How many times one pump may follow a playthrough a command handler began from inside its own callback. Past this you get command-restart-limit and an error finish.
BoardEntryPort"@entry:"The synthetic port prefix for a board-entry choice. This string reaches a saved playthrough’s recorded decisions — changing it invalidates existing saves.

Telling the engine what your host draws

Three settable properties. Each throws on null, and each re-checks your declarations when set.

PropertyTypeWhat it declares
SilentNodeTypesISet<string>Node types this host never shows
AlwaysPresentNodeTypesISet<string>Node types this host always shows, even with no text
SilentFieldsNodeFieldSetIndividual fields this host never shows

AlwaysPresentNodeTypes is for a node your game draws as a beat rather than as prose: an image, a reward the player dismisses.

Events

EventRaised when
Action<PlayStep>? StepPresentedA moment the player sees
Action<EngineErrorEvent>? ErrorRaisedThe engine could not do what the data asked, and carried on. Not an exception.
Action<string>? FinishedThe story reached the end of a path. Not raised for Stop or for an error state.
Action<EngineState, EngineState>? StateChangedState changed, from the first argument to the second

Properties

PropertyTypeNotes
DiagnosticsIEngineDiagnostics
StateEngineState
HistoryIReadOnlyList<PlayStep>Every step, not the save’s capped window
GraphIGraphSource
CallStackIReadOnlyList<CallFrame>Outermost first. Part of a save, so a playthrough saved inside a sub-board resumes into its caller.
CanForwardbool
LanguagestringThe storage key, which for the master language is the default key rather than the project’s code for it. Map it back through INodeViewContext.LanguageOptions before showing it.

Driving it

MethodWhat it does
Start(string boardId, string? nodeId = null)Begin on a board, at a node or at the board’s entry. Clears history, moments, the roll-forward stack, the call stack, taken ports and said lines — but not the runtime’s values.
Advance()Move past a presented step. Ignored unless presenting.
SelectChoice(int index)Take one of PlayStep.Choices. Ignored for an index that is out of range, disabled, or has no target.
Lapse()A countdown ran out and the player picked nothing. Takes the first flow exit that is not an offered option — never the first option. Records nothing, so a roll-forward will not replay it.
Back()One step back, restoring the full state as it was. Refused while a command is in flight.
Forward()Re-advance after a Back, replaying what the player did rather than re-running it
SetLanguage(string language)Re-draws the whole history and the roll-forward stack immediately
Redraw()Re-draw against current state without re-traversing
NextLine(string boardId, string nodeId)A node’s next line without moving the story — no step, no script, no visit — remembered as said. Null while the engine is drawing — a step, or the history again. See a line without a step.
Stop()End the playthrough. Always works, including mid-command — it is the abort.
SavePlaythrough()Capture everything. Throws if there is nothing to save.
RestorePlaythrough(PlaythroughSave payload)Put one back. Legal at any time; anything pending is abandoned.

Redraw() is for late arrivals. Call it when something a drawn step depends on has resolved since — a media URL, typically, which a step drawn before it arrived would otherwise carry as null forever.

PlayStep

TaleShard.Runtime.Engine.PlayStep — sealed.

One moment of a playthrough — the contract between the engine and the game, and the only thing that crosses it.

What you read

MemberTypeNotes
VerbPlayVerbWhat this moment asks of the player. Switch on this.
ChoiceKindChoiceKindWhat kind of choices these are. Switch on this rather than inspecting a choice’s PortId.
PresentedIReadOnlyList<ResolvedField>The fields a player actually reads, in order. Computed once.
ChoicesIReadOnlyList<ResolvedChoice>Already gate-filtered. Empty unless Verb is Choose.
FieldsIReadOnlyList<ResolvedField>Everything, including what is not text
NodeStepNodeIts id, its board, its type
ArrivedByArrivedByHow the player got here — a different question from Verb
CastIReadOnlyList<ResolvedActor>
AudioIReadOnlyList<ResolvedAudio>Per channel: sfx and voice. Switch on Channel.
FlagsStepFlagsIsLatest, IsTerminal, Awaiting
Idstring

Finding a field

Three verbs, because there are three questions.

MethodAnswers
ResolvedField? Field(string code)A field by its author-facing code. Non-repeater fields only.
ResolvedField? FieldByKey(string key)A field by its exact storage key, including a repeater instance’s
IReadOnlyList<IReadOnlyList<ResolvedField>> Blocks(string repeaterCode, string? withinInstanceId = null)A repeater’s fields, one list per block, in index order. For a repeater declared inside another’s block, withinInstanceId — an outer row’s instance id — keeps that row’s own inner rows; without it, every outer row’s, outer row by outer row

A repeater field’s key embeds an instance id, so a code inside a repeater has one answer per row — which is why Field cannot answer it and Blocks exists.

What is there for the engine, not for you

Public, and you should not normally touch it:

  • The 12-argument constructor. Only the engine builds a PlayStep.
  • Trace and everything under it. Its own summary says for diagnostics.
  • Variables — a debug view. The live source is IScriptRuntime.GetAllVariables().

StepNode

MemberType
Idstring
BoardIdstring
TypeIdstring

The enums

PlayVerb

What this moment asks of the player, derived from the node’s content and exits rather than its type.

Member
ChooseThe player picks
PresentThe player reads and continues

ArrivedBy

How the player got here. Start, Jump and Return are arrivals — the player was placed somewhere. Walk is not: they followed an exit.

Member
Start
Walk
Jump
Return

A transcript that starts a new page at each arrival is written against this. A board-id change cannot stand in for it, because a jump can happen within one board.

This says how control reached a node. Why is Trace.EnteredVia.

ChoiceKind

Member
ExitThe ordinary case: the ways out of the node on screen
BoardEntryThe path ended, and these are the board’s remaining ways in

StepAwaiting

None · Advance · Choice · Timer

EngineState

Its constructor is private; every instance comes from a static.

MemberTypeNotes
KindEngineStateKind
StepPlayStep?
Reasonstring?Why it finished: end-of-path, stopped or error
Errorstring?
PendingCommandCommandCompletion?The command being waited on, or null unless suspended

EngineStateKind is Idle · Presenting · AwaitingChoice · Suspended · Finished.

Suspended means a command is running and the story is waiting. The engine does not decide whether anything stays on screen — it publishes the pending command and the host decides.

What is not deprecated

Nothing here. There is no [Obsolete] member anywhere in this surface, so there is no legacy path to avoid and no newer replacement to prefer.

One thing that is conditional rather than obsolete: AddressablesMediaSource exists only in a project that has the Addressables package.

Before your first attempt

Several types here have an ordering constraint that is easy to get wrong once and never again. They are collected in what to get right first.

Written for
Unity developers · Advanced integrators
Last updated