Skip to content

The play loop

Every Patterplay runtime (JavaScript, Unity, Unreal, Godot) works the same way. Learn the shape here once; each engine’s quickstart is then mostly install notes and local naming.

  • An Engine is built from a loaded bundle. It holds the shared story state (your @patter and @scene properties, visit counts) and it’s what you save and load.
  • A Flow is one position cursor walking through the story. You open a flow at a starting address (a scene, optionally a block); most games run one flow, but you can run several (a main thread plus a side conversation) off the same engine.
engine = Engine(bundle) // shared state
flow = engine.openFlow("main", scene) // a cursor at a starting point

The method names differ slightly per engine (openFlow / OpenFlow / open_flow) but the idea is identical.

Your game owns the loop: it calls advance() (or choose(id) on a choice), the flow returns one step (line, text, gameEvent, choice, or end), and the game renders it. The game also reads and writes story state with getProperty and setProperty and supplies its own values as @world. Your game the host, owns the loop Flow one run of the story advance() / choose(id) returns one step, one of: line text gameEvent choice end The host also reads and writes story state with getProperty / setProperty, and supplies its own live values to the story as @world.

You drive the flow by asking for the next step and rendering it. A step is one of five kinds:

StepMeaningWhat it carries
lineA character speaksthe speaker, the (localised) display name, the text, direction, Game Data, tags
textNarration the player readsthe text, Game Data, tags
gameEventA host-facing cue, no spoken textan id + Game Data (play a sound, move the camera)
choiceThe player must picka list of options (each with prompt text + an eligible flag)
endThe flow finished:

A minimal loop: advance until a choice or the end, rendering each beat as it comes.

loop:
step = flow.advance()
switch step.type:
"line": show(step.characterName ?? step.character, step.text)
"text": show(step.text)
"gameEvent": doHostCue(step.id, step.gameData)
"choice": presentOptions(step.options); break // wait for the player
"end": finish(); break

When the player picks, call choose with the option’s id, then resume advancing:

flow.choose(optionId)
// ...back to the loop

Options that fail their condition come back ineligible rather than missing (so you can grey them out), unless the author marked them to hide entirely. That eligible flag is yours to render however suits your UI.

The engine exposes the story’s properties by reference:

engine.getProperty("@gold") // read @patter / @scene state
engine.setProperty("@gold", 10) // write it (e.g. from a shop your game runs)

Your game also supplies the @world values the story reads (threat level, location). If you don’t bind one, the runtime falls back to the value the project declared as its default. The details (including reading typed Game Data and tags off each step) are in Save/load & Game Data.

The whole run: every flow’s position, the shared state, visit counts, even the seeded random generator’s place in its sequence: serialises in one call and restores in one call. Every runtime handles save/load the same way, so a save made by one engine round-trips exactly. See your engine’s quickstart for the local call and Save/load & Game Data for the shape.

In Embedded mode the text on each step is already resolved to the current language, and you can switch language live. In IDs-only mode the step carries ids instead of text, and you resolve them through your own localisation system. The runtime API is the same either way; only where the words come from changes. → Localisation

MIT-licensed open source · Made by · patterkit.com