Play your first story
The smallest program that starts a session and prints a line β no Unity, no MonoBehaviour, about thirty lines.
All documentation β Start here
Start here
Concepts
Authoring for a game
Playing a story
The generated code
The plugin and sync
This is the whole loop. It reads the synced payload, starts the story, prints each stepβs text and advances until the story ends. Nothing here is Unity-specific, so you can run it as a console program before you wire anything into a scene β which is a much faster way to find out whether your project synced correctly than pressing Play.
using System;using System.Collections.Generic;using TaleShard.Runtime.Engine;using TaleShard.Runtime.Script;
namespace TaleShard.Docs.Samples{ public static class FirstStory { // One argument: the folder the sync wrote runtime.json into. public static void Main(string[] args) { string directory = args.Length > 0 ? args[0] : ".";
// The resolved project: boards, nodes, entities and parsed expressions. Nothing here // parses story syntax β the editor did that at sync time. EnginePayload payload = EnginePayloadReader.ReadDirectory(directory);
// Holds the project's variables, entities and enums, and evaluates the expression trees // the payload carries. It also reports what it could not resolve. EnginePayloadBinding binding = EnginePayloadBinding.Create(payload); EngineSession session = EngineSession.Create(payload, binding.Runtime);
session.Start();
// A budget, not a `while (true)`: an authored loop is a story an author is allowed to // write, and a sample that hangs on one teaches nothing. for (int guard = 0; guard < 1000; guard++) { EngineState state = session.Engine.State;
if (state.Kind == EngineStateKind.Presenting) { Print(state.Step); session.Engine.Advance(); continue; }
if (state.Kind == EngineStateKind.AwaitingChoice) { Print(state.Step); int index = FirstTakeable(state.Step.Choices); if (index < 0) break; session.Engine.SelectChoice(index); continue; }
break; }
Console.WriteLine("-- " + (session.Engine.State.Reason ?? session.Engine.State.Kind.ToString())); }
private static void Print(PlayStep step) { // `Presented` is the author's presentable fields, in authored order. The engine has // already decided what belongs on screen, so a host never filters. foreach (ResolvedField field in step.Presented) { // Answers the plain reading, or null when the field holds a number, a reference or // nothing. It never throws and never coerces. string text = field.Text(); if (text != null) Console.WriteLine(field.Label + ": " + text); }
for (int i = 0; i < step.Choices.Count; i++) { ResolvedChoice choice = step.Choices[i]; Console.WriteLine(" [" + i + "] " + ChoiceText(choice) + (choice.IsTakeable ? "" : " (locked)")); } }
private static string ChoiceText(ResolvedChoice choice) { foreach (ResolvedField field in choice.Content) { string text = field.Text(); if (text != null) return text; } return choice.ChoiceId; }
private static int FirstTakeable(IReadOnlyList<ResolvedChoice> choices) { for (int i = 0; i < choices.Count; i++) { if (choices[i].IsTakeable) return i; } return -1; } }}Run it against the folder the sync wrote:
dotnet run -- ./Assets/TaleShard/GeneratedWhat each part is doing
Reading the payload. EnginePayloadReader refuses rather than degrades. A missing file, a
malformed body, or a format version this build does not understand throws β because the alternative
is a project that compiles, plays nothing, and looks like a content problem.
The binding. EnginePayloadBinding.Create builds the expression runtime from the same payload:
the projectβs variables, its entities, its enums, and the expression trees the editor parsed at sync
time. It also reports what it could not resolve, and
starting a session shows why you should surface that rather than
discard it.
The loop. EngineSession wires the payload, the runtime and the engine together. The engine is
a state machine with a small number of states, and the two you handle here are the two that need the
player: Presenting wants an advance, AwaitingChoice wants a
selection. Read State.Kind rather than guessing from the step β see
reading a step.
The budget. The for loop has a bound rather than being while (true). An authored loop is a
story an author is allowed to write, and a sample that hangs on one teaches nothing about either.
What this sample deliberately does not do
It picks the first takeable ResolvedChoice, which no real game does. It prints Label: text, which is a
debugging shape rather than a presentation. And it ignores everything else a step carries β the
cast, the audio channels, the variables snapshot. Reading a step is
where those come in.
It also assumes the payload is a file on disk. In a Unity build the sync mirrors the payload into a
Resources folder as a text asset, so a real game hands the text to EnginePayloadReader.Parse
rather than pointing at a directory.
Next
Starting a session β the same thing with the wiring a real game needs: diagnostics, your own commands, and a presentation policy.