Pardon our dust while we move our docs here. The Godot (GDScript) docs are on this site, and the Yarn language, Unity, Godot (C#) and Unreal docs are still at docs.yarnspinner.dev.

Sometimes a Yarn Script offers several pieces of content that could run at the same point, and only one of them should. There are two ways to write this:

  • A node group is several nodes with the same title, each with when: headers that say when it can run. Nodes like these are often called storylets.
  • A line group is a set of lines that start with =>, each with an optional condition.

When the dialogue reaches a node group or a line group, the Dialogue Runner works out which pieces of content can run, and asks its saliency strategy to pick one. To write your own strategy, see Custom Saliency Strategies. For how to write node groups and line groups, see the Storylets and Saliency Primer.

title: Rosa
when: always
---
Rosa: Hello again.
===

title: Rosa
when: always
---
Rosa: Oh, it's you.
===

title: Rosa
when: not $lamp_lit
---
Rosa: Who's there? It's too dark.
===

title: Rosa
when: $lamp_lit
---
Rosa: Ah, I can see you now.
===

title: Rosa
when: once
when: $lamp_lit
---
Rosa: Thanks for lighting the lamp!
===
Lines 1–11 Two nodes that can always run.
Lines 13–17 Can only run while the lamp is out.
Lines 19–23 Can only run while the lamp is lit.
Lines 25–30 Can only run while the lamp is lit, and only once.
A node group called Rosa. Talking to Rosa runs one of these five nodes.

To run a node group, <<jump>> or <<detour>> to its name, or start the dialogue there, the same as any other node.

A line group does the same job inside a node, for a single line. When the dialogue reaches it, one of its lines is shown, the others are skipped, and the dialogue carries on. It’s useful for small variations, like a greeting, where a whole node for each one would be too much:

title: RosaGreeting
---
=> Rosa: Evening.
=> Rosa: Mind the step, it's dark. <<if not $lamp_lit>>
=> Rosa: Nice and bright in here. <<if $lamp_lit>>
Rosa: What can I do for you?
===
Line 3 A line with no condition. It can always be picked.
Lines 4–5 Lines with a condition. Each can only be picked while its condition is true.
Line 6 An ordinary line. It’s shown after whichever line was picked.
A line group at the start of a node. Rosa says one of the three lines, then carries on.

Node groups and line groups are picked the same way.

How one is picked

Each piece of content that could run is a candidate. The Dialogue Runner tracks three things for each one:

  • Whether it passes: a candidate passes if all its conditions are true. Only candidates that pass can be picked.
  • Its complexity: each condition adds 1, and so does each and, or or xor in it. once adds 1, and always adds nothing. A candidate with a higher complexity has more specific conditions.
  • How many times it’s been seen: the default strategy counts how many times it has picked each candidate.

The saliency strategy uses these to pick one candidate that passes.

flowchart TD
  reach["The dialogue reaches a<br>node group or line group"]
  check["Check each candidate's conditions"]
  pick{"Do any pass?"}
  strategy["The saliency strategy<br>picks one"]
  run["That candidate runs"]
  none["Nothing from the group runs"]
  reach --> check --> pick
  pick -- yes --> strategy --> run
  pick -- no --> none
What happens when the dialogue reaches a node group or a line group.

If nothing passes, nothing from the group runs. After a <<detour>> to a node group, the dialogue carries on after the <<detour>>. After a <<jump>> to one, the dialogue ends.

To check first, use the has_any_content() Function in Yarn. It returns true if something in the node group can run:

-> Talk to Rosa <<if has_any_content("Rosa")>>
    <<detour Rosa>>
Only offer the option if Rosa has something to say.

From GDScript, dialogue_runner.has_salient_content("Rosa") does the same thing.

Choosing a strategy

Set the Dialogue Runner’s Saliency Strategy in the Inspector, under Advanced:

Saliency StrategyPicks
Random Best Least RecentFrom the candidates that pass, the ones seen the fewest times. Of those, the ones with the highest complexity. Then one of those at random. This is the default.
Best Least RecentThe same as Random Best Least Recent, but takes the first one instead of a random one.
BestThe candidate with the highest complexity. If there’s a tie, the first one.
FirstThe first candidate that passes.
RandomAny candidate that passes, at random.
CustomYour own strategy, set from GDScript. See Custom Saliency Strategies.
The Dialogue Runner’s Saliency Strategy options.

Only the two Least Recent strategies count how many times each candidate has been seen. They keep the counts in the Variable Storage, so the counts are saved and loaded along with your other variables.

You can also set it from GDScript. The new strategy is used from the next time the dialogue starts:

dialogue_runner.saliency_strategy = YarnDialogueRunner.SaliencyStrategyType.BEST
Changing to the Best strategy from GDScript.

Looking at a node group from GDScript

get_saliency_options_for_node_group() returns the candidates in a node group, as they are right now. Use it to show which storylets are available, or to debug why one isn’t running:

for candidate in dialogue_runner.get_saliency_options_for_node_group("Rosa"):
	var passes: bool = candidate.conditions_failed == 0
	print(candidate.content_id, " passes: ", passes)
Printing each candidate in the Rosa node group.

Each candidate is a Dictionary:

KeyWhat it is
content_idThe candidate’s ID. In a node group, it’s a name that Yarn Spinner makes for the node, such as Rosa.3d371ffd. In a line group, it’s the line’s ID.
complexityThe candidate’s complexity.
conditions_passedHow many of its conditions are true.
conditions_failedHow many of its conditions are false. The candidate can only run if this is 0.
content_typeYarnSaliencyStrategy.ContentType.NODE for a node, or YarnSaliencyStrategy.ContentType.LINE for a line.
destinationWhere the dialogue goes if this candidate is picked. Only the Dialogue Runner uses it.
The keys in each candidate Dictionary.

is_node_group() tells you whether a name belongs to a node group.

Writing your own strategy

If none of the built-in strategies pick content the way you want, you can write your own. See Custom Saliency Strategies.

Samples

Three samples show saliency in bigger scenes:

  • Basic Saliency has four characters who say different things depending on the day and the time of day, using both node groups and line groups.
  • Custom Saliency has a custom strategy that picks at random, with a weight header on each node and a #weight tag on each line to make some more likely than others.
  • Advanced Saliency builds different scenes in a room from choices you make, using storylets.

The demo scene

This scene has one node group, Rosa. In the video, the player talks to Rosa, lights the lamp, and puts it out again. The panel in the top left lists every node in the Rosa node group, whether it passes, its complexity, how many times it’s been seen, and its first line. The one that was picked last is yellow.

The demo scene running. Each time you talk to Rosa, the panel shows which candidates pass and which one was picked.
GameNode2D
BackgroundColorRect
FloorColorRect
DoorColorRect
LampColorRect
GlowColorRect
RosaNode2D
BodyColorRect
NameLabel
YarnDialogueRunner
VariableStorageYarnInMemoryVariableStorage
UICanvasLayer
CandidatesPanelPanelContainer
CandidatesRichTextLabel
LampPanelPanelContainer
LampStateLabel
YarnLinePresenter
YarnOptionsPresenter
The demo scene. Game’s script shows the Rosa node group in Candidates, and the lamp in LampState.
title: SaliencyDemo
---
<<declare $lamp_lit = false>>
-> Talk to Rosa
    <<detour Rosa>>
-> Light the lamp <<if not $lamp_lit>>
    <<set $lamp_lit to true>>
-> Put the lamp out <<if $lamp_lit>>
    <<set $lamp_lit to false>>
<<jump SaliencyDemo>>
===

title: Rosa
when: always
---
Rosa: Hello again.
===

title: Rosa
when: always
---
Rosa: Oh, it's you.
===

title: Rosa
when: not $lamp_lit
---
Rosa: Who's there? It's too dark.
===

title: Rosa
when: $lamp_lit
---
Rosa: Ah, I can see you now.
===

title: Rosa
when: once
when: $lamp_lit
---
Rosa: Thanks for lighting the lamp!
===
Lines 4–5 Detour to the Rosa node group, so one of its nodes runs and then the dialogue comes back here.
Lines 6–9 Light the lamp, or put it out. This changes which of Rosa’s nodes pass.
Line 10 Go back to the start, and offer the options again.
Lines 13–23 Two nodes that can always run. Their complexity is 0.
Lines 25–35 One node for when the lamp is out, and one for when it’s lit. Each has one condition, so their complexity is 1.
Lines 37–42 Only while the lamp is lit, and only once. once and the condition each add 1, so its complexity is 2. The first time the player talks to Rosa after lighting the lamp, this node is picked.
The Yarn Script.
extends Node2D

const LAMP_OFF := Color(0.35, 0.35, 0.35)
const LAMP_ON := Color(1.0, 0.85, 0.35)
const HEADINGS := ["Passes", "Complexity", "Seen", "First line"]

@export var dialogue_runner: YarnDialogueRunner
@export var candidates_label: RichTextLabel
@export var lamp_label: Label
@export var lamp: ColorRect
@export var glow: ColorRect

var _seen := {}
var _picked := ""


func _ready():
	dialogue_runner.start_dialogue("SaliencyDemo")


func _process(delta: float) -> void:
	var storage := dialogue_runner.variable_storage
	var lamp_lit := storage.get_bool("$lamp_lit")
	lamp_label.text = "$lamp_lit = %s" % lamp_lit

	var target := 1.0 if lamp_lit else 0.0
	var brightness := glow.modulate.a
	brightness = move_toward(brightness, target, delta * 3.0)
	lamp.color = LAMP_OFF.lerp(LAMP_ON, brightness)
	glow.modulate.a = brightness

	_show_candidates()


func _show_candidates() -> void:
	var storage := dialogue_runner.variable_storage
	var program := dialogue_runner.yarn_project.get_program()
	var candidates := dialogue_runner \
			.get_saliency_options_for_node_group("Rosa")

	var text := "[b]Node group: Rosa[/b]   "
	text += "[color=#ffd35a]yellow: picked last[/color]\n"
	text += "[table=4]"
	for heading in HEADINGS:
		text += _cell(heading, "#99aaaa")

	for candidate in candidates:
		var id: String = candidate.content_id

		# The built-in strategies count how many times each
		# candidate has been picked. When a count goes up,
		# that candidate was just picked.
		var seen := YarnSaliencyStrategy.get_view_count(
				{"variable_storage": storage}, id)
		if seen > _seen.get(id, 0):
			_picked = id
		_seen[id] = seen

		var passes: bool = candidate.conditions_failed == 0
		var line_id := program.get_line_ids_for_node(id)[0]
		var line := program.get_string(line_id)

		var colour := "#777777"
		if id == _picked:
			colour = "#ffd35a"
		elif passes:
			colour = "#ffffff"

		text += _cell("yes" if passes else "no", colour)
		text += _cell(str(candidate.complexity), colour)
		text += _cell(str(seen), colour)
		text += _cell(line.trim_prefix("Rosa: "), colour)

	text += "[/table]"
	candidates_label.text = text


func _cell(value: String, colour: String) -> String:
	var cell := "[cell][color=%s]%s   [/color][/cell]"
	return cell % [colour, value]
Lines 3–4 The lamp’s colour when it’s out and when it’s lit.
Line 5 The panel’s column headings.
Lines 7–11 Nodes in the scene, set in the Inspector.
Lines 13–14 Each candidate’s view count from last frame, and the one that was picked last.
Line 18 Start the dialogue. The Dialogue Runner’s Auto Start is off.
Lines 22–24 Show $lamp_lit in the top right.
Lines 26–30 Fade the lamp on or off to match.
Line 32 Update the panel every frame, so it changes as soon as the lamp does.
Line 37 The compiled Yarn Project, used to look up each node’s first line.
Lines 38–39 Every candidate in the Rosa node group, whether it passes or not.
Lines 41–45 The panel’s title, and a row of headings.
Lines 53–57 Read how many times the candidate has been seen. If that’s gone up since last frame, it’s the one that was picked. get_view_count() reads the count from the Variable Storage in the Dictionary.
Line 59 A candidate passes if none of its conditions failed.
Lines 60–61 The node’s first line.
Lines 63–67 Yellow for the one picked last, white if it passes, and grey if it doesn’t.
Lines 69–72 Add one row to the table.
Lines 78–80 One cell of the table, in a colour.
The script on the Game node.
Next step Custom Saliency Strategies Writing your own way to pick content from node groups and line groups.