Dialogue systems
Model conversations as a graph: nodes hold lines, choices branch the flow,
conditions gate options, and variables remember what the player did. The first
real decision is build vs. buy — adopt a proven authoring tool (Ink or
Yarn Spinner) or write a small data-driven runner. This skill owns both Ink
and Yarn; the visual-novel and rpg genres consume it.
When to use
- Use to design branching conversations, choice menus, or narrative state
(flags, relationship values) that affect later dialogue.
- Use to decide between Ink, Yarn Spinner, and a custom JSON/resource format.
- Use to wire a dialogue script into your game loop (advance line, present
choices, run commands, resolve variables).
When not to use: for engine UI (text boxes, portraits, choice buttons), use
godot-ui-control or the engine's UI skill. For persisting narrative variables
across sessions, use save-systems. For data-as-resources in Godot/Unity, see
godot-resources / unity-scriptableobjects.
Core workflow
- Choose the authoring approach.
- Ink — prose-first, writer-friendly, weave/gather flow; great for
dialogue-heavy or CYOA narrative. Integrate via ink runtime / inkle plugins.
- Yarn Spinner — node-based, explicit
<<commands>>, strong for
game-driven dialogue with lots of engine hooks.
- Custom runner — a JSON/resource graph + a small interpreter when you need
full control or minimal dependencies. Don't build a language; build a graph.
- Define the node contract. A node yields one of: a line (speaker + text),
a set of choices, a command/side-effect, or an end/jump. The runner advances
through nodes and hands lines/choices to the UI.
- Separate variables from flow. Keep a variable store (booleans, numbers,
strings) the dialogue reads/writes; gate choices with conditions over it.
- Localize from the start. Author with line IDs, not raw strings, so the
displayed text comes from a string table keyed by locale.
- Drive it from the game loop. The runner is a state machine:
current node
→ emit content → wait for input (continue or choice) → advance.
- Verify by walking branches. Exercise each choice path; confirm conditions,
variable writes, and that every branch reaches an end or a valid jump.
Patterns
1. Engine-neutral dialogue graph (data, not code)
{
"start": "guard_intro",
"nodes": {
"guard_intro": {
"speaker": "Guard", "line": "DLG_GUARD_001",
"choices": [
{ "text": "DLG_OPT_BRIBE", "to": "bribe", "if": "gold >= 50" },
{ "text": "DLG_OPT_LEAVE", "to": "end" }
]
},
"bribe": {
"speaker": "Guard", "line": "DLG_GUARD_BRIBED",
"set": { "gate_open": true, "gold": "gold - 50" },
"next": "end"
},
"end": { "end": true }
}
}
line/text are string-table IDs (localization), not literal text. if
gates a choice; set mutates the variable store. The full interpreter that walks
this graph is in references/runner.md.
2. Runner step (a state machine over the graph)
# The runner holds the current node and a variable store; the UI calls advance().
func present(node):
if node.has("line"):
ui.show_line(node.speaker, localize(node.line))
if node.has("choices"):
var shown = node.choices.filter(func(c): return eval_cond(c.get("if", "")))
ui.show_choices(shown) # only choices whose condition passes
func choose(choice): # called when the player clicks a choice
apply_set(choice.get("set", {})) # write variables
goto(choice.to)
func goto(id):
current = graph.nodes[id]
apply_set(current.get("set", {}))
if current.get("end", false): ui.close(); return
present(current)
if current.has("next") and not current.has("choices"):
goto(current.next) # auto-advance linear nodes
3. Ink — branching with knots, choices, and variables (inkle)
// Ink: '*' = once-only choice, '+' = sticky. [bracketed] text shows only in the
// choice, not the printed result. '->' diverts; '-> END' stops the flow.
VAR gold = 60
=== guard_intro ===
The guard blocks the gate.
* {gold >= 50} [Offer 50 gold] "Here, take it."
~ gold = gold - 50
The guard pockets it and steps aside. -> END
* [Leave] You turn back. -> END
Ink tracks how often each knot was seen, so {visited_knot} is a built-in
condition. Variables are global (VAR) or temporary (~ temp).
4. Yarn Spinner — nodes, options, and commands (Yarn 2.x)
title: GuardIntro
---
<<declare $gold = 60>>
Guard: You can't pass.
-> Offer 50 gold <<if $gold >= 50>>
<<set $gold = $gold - 50>>
Guard: ...fine. Go on through.
<<set $gate_open to true>>
-> Leave
Guard: Good choice.
===
Yarn lines may start with Speaker:; options use ->; <<set>>/<<declare>>
manage $variables; <<if>> gates an option; <<jump NodeName>> moves between
nodes. Interpolate values in text with {$gold}.
Pitfalls
- Hardcoding display strings instead of line IDs makes localization a
rewrite. Author against a string table from day one.
- Inventing a scripting language for a simple branching tree. If you only
need lines + choices + flags, a JSON/resource graph plus a 50-line runner beats
a parser you must maintain. Use Ink/Yarn when writers need real flow control.
- Variables coupled to the UI: store narrative state separately so the same
dialogue works in cutscenes, menus, and tests. Persist it via
save-systems.
- Unreachable or dead-end nodes: a node with no
next, choices, or end
silently stalls. Validate that every node terminates or branches.
- Mutating state in a line node the player can revisit double-applies (
gold
drained twice). Apply set on the transition, or guard with a seen-flag.
- Mixing Ink's
* (once-only) and + (sticky) by accident: looped menus
need sticky + choices or the options vanish after one use.
References
references/ink-and-yarn.md — side-by-side syntax cheat sheet (choices,
diverts/jumps, variables, conditions, includes) and integration notes.
references/runner.md — a complete custom dialogue runner: graph schema,
condition/expression evaluation, variable store, and localization lookup.
Related skills
save-systems — persist narrative variables and seen-flags.
godot-resources, unity-scriptableobjects — store dialogue as engine data.
godot-ui-control — render text boxes, portraits, and choice buttons.
visual-novel, rpg — genres that compose this skill.
1---
2name: dialogue-systems
3description: >
4 Build branching dialogue and narrative — a node/choice graph with conditions,
5 variables, and localization hooks — and choose between authoring tools Ink and
6 Yarn Spinner or a custom data-driven runner. Engine-neutral. Use when the user
7 mentions dialogue system, branching dialogue, conversation tree, choices,
8 Ink (.ink), Yarn Spinner (.yarn), or NPC dialogue.
9---
10
11# Dialogue systems
12
13Model conversations as a **graph**: nodes hold lines, choices branch the flow,
14conditions gate options, and variables remember what the player did. The first
15real decision is *build vs. buy* — adopt a proven authoring tool (**Ink** or
16**Yarn Spinner**) or write a small data-driven runner. This skill owns both Ink
17and Yarn; the `visual-novel` and `rpg` genres consume it.
18
19## When to use
20
21- Use to design branching conversations, choice menus, or narrative state
22 (flags, relationship values) that affect later dialogue.
23- Use to decide between Ink, Yarn Spinner, and a custom JSON/resource format.
24- Use to wire a dialogue script into your game loop (advance line, present
25 choices, run commands, resolve variables).
26
27**When *not* to use:** for engine UI (text boxes, portraits, choice buttons), use
28`godot-ui-control` or the engine's UI skill. For persisting narrative variables
29across sessions, use `save-systems`. For data-as-resources in Godot/Unity, see
30`godot-resources` / `unity-scriptableobjects`.
31
32## Core workflow
33
341. **Choose the authoring approach.**
35 - **Ink** — prose-first, writer-friendly, weave/gather flow; great for
36 dialogue-heavy or CYOA narrative. Integrate via ink runtime / inkle plugins.
37 - **Yarn Spinner** — node-based, explicit `<<commands>>`, strong for
38 game-driven dialogue with lots of engine hooks.
39 - **Custom runner** — a JSON/resource graph + a small interpreter when you need
40 full control or minimal dependencies. Don't build a *language*; build a graph.
412. **Define the node contract.** A node yields one of: a line (speaker + text),
42 a set of choices, a command/side-effect, or an end/jump. The runner advances
43 through nodes and hands lines/choices to the UI.
443. **Separate variables from flow.** Keep a variable store (booleans, numbers,
45 strings) the dialogue reads/writes; gate choices with conditions over it.
464. **Localize from the start.** Author with **line IDs**, not raw strings, so the
47 displayed text comes from a string table keyed by locale.
485. **Drive it from the game loop.** The runner is a state machine: `current` node
49 → emit content → wait for input (continue or choice) → advance.
506. **Verify by walking branches.** Exercise each choice path; confirm conditions,
51 variable writes, and that every branch reaches an end or a valid jump.
52
53## Patterns
54
55### 1. Engine-neutral dialogue graph (data, not code)
56
57```json
58{
59 "start": "guard_intro",
60 "nodes": {
61 "guard_intro": {
62 "speaker": "Guard", "line": "DLG_GUARD_001",
63 "choices": [
64 { "text": "DLG_OPT_BRIBE", "to": "bribe", "if": "gold >= 50" },
65 { "text": "DLG_OPT_LEAVE", "to": "end" }
66 ]
67 },
68 "bribe": {
69 "speaker": "Guard", "line": "DLG_GUARD_BRIBED",
70 "set": { "gate_open": true, "gold": "gold - 50" },
71 "next": "end"
72 },
73 "end": { "end": true }
74 }
75}
76```
77
78`line`/`text` are **string-table IDs** (localization), not literal text. `if`
79gates a choice; `set` mutates the variable store. The full interpreter that walks
80this graph is in `references/runner.md`.
81
82### 2. Runner step (a state machine over the graph)
83
84```gdscript
85# The runner holds the current node and a variable store; the UI calls advance().
86func present(node):
87 if node.has("line"):
88 ui.show_line(node.speaker, localize(node.line))
89 if node.has("choices"):
90 var shown = node.choices.filter(func(c): return eval_cond(c.get("if", "")))
91 ui.show_choices(shown) # only choices whose condition passes
92
93func choose(choice): # called when the player clicks a choice
94 apply_set(choice.get("set", {})) # write variables
95 goto(choice.to)
96
97func goto(id):
98 current = graph.nodes[id]
99 apply_set(current.get("set", {}))
100 if current.get("end", false): ui.close(); return
101 present(current)
102 if current.has("next") and not current.has("choices"):
103 goto(current.next) # auto-advance linear nodes
104```
105
106### 3. Ink — branching with knots, choices, and variables (inkle)
107
108```ink
109// Ink: '*' = once-only choice, '+' = sticky. [bracketed] text shows only in the
110// choice, not the printed result. '->' diverts; '-> END' stops the flow.
111VAR gold = 60
112
113=== guard_intro ===
114The guard blocks the gate.
115* {gold >= 50} [Offer 50 gold] "Here, take it."
116 ~ gold = gold - 50
117 The guard pockets it and steps aside. -> END
118* [Leave] You turn back. -> END
119```
120
121Ink tracks how often each knot was seen, so `{visited_knot}` is a built-in
122condition. Variables are global (`VAR`) or temporary (`~ temp`).
123
124### 4. Yarn Spinner — nodes, options, and commands (Yarn 2.x)
125
126```yarn
127title: GuardIntro
128---
129<<declare $gold = 60>>
130Guard: You can't pass.
131-> Offer 50 gold <<if $gold >= 50>>
132 <<set $gold = $gold - 50>>
133 Guard: ...fine. Go on through.
134 <<set $gate_open to true>>
135-> Leave
136 Guard: Good choice.
137===
138```
139
140Yarn lines may start with `Speaker:`; options use `->`; `<<set>>`/`<<declare>>`
141manage `$variables`; `<<if>>` gates an option; `<<jump NodeName>>` moves between
142nodes. Interpolate values in text with `{$gold}`.
143
144## Pitfalls
145
146- **Hardcoding display strings** instead of line IDs makes localization a
147 rewrite. Author against a string table from day one.
148- **Inventing a scripting language** for a simple branching tree. If you only
149 need lines + choices + flags, a JSON/resource graph plus a 50-line runner beats
150 a parser you must maintain. Use Ink/Yarn when writers need real flow control.
151- **Variables coupled to the UI**: store narrative state separately so the same
152 dialogue works in cutscenes, menus, and tests. Persist it via `save-systems`.
153- **Unreachable or dead-end nodes**: a node with no `next`, choices, or end
154 silently stalls. Validate that every node terminates or branches.
155- **Mutating state in a line node the player can revisit** double-applies (`gold`
156 drained twice). Apply `set` on the transition, or guard with a seen-flag.
157- **Mixing Ink's `*` (once-only) and `+` (sticky)** by accident: looped menus
158 need sticky `+` choices or the options vanish after one use.
159
160## References
161
162- `references/ink-and-yarn.md` — side-by-side syntax cheat sheet (choices,
163 diverts/jumps, variables, conditions, includes) and integration notes.
164- `references/runner.md` — a complete custom dialogue runner: graph schema,
165 condition/expression evaluation, variable store, and localization lookup.
166
167## Related skills
168
169- `save-systems` — persist narrative variables and seen-flags.
170- `godot-resources`, `unity-scriptableobjects` — store dialogue as engine data.
171- `godot-ui-control` — render text boxes, portraits, and choice buttons.
172- `visual-novel`, `rpg` — genres that compose this skill.