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!
===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?
===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,ororxorin it.onceadds 1, andalwaysadds 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 --> noneIf 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>>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 Strategy | Picks |
|---|---|
| Random Best Least Recent | From 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 Recent | The same as Random Best Least Recent, but takes the first one instead of a random one. |
| Best | The candidate with the highest complexity. If there’s a tie, the first one. |
| First | The first candidate that passes. |
| Random | Any candidate that passes, at random. |
| Custom | Your own strategy, set from GDScript. See Custom Saliency Strategies. |
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.BESTLooking 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)Each candidate is a Dictionary:
| Key | What it is |
|---|---|
content_id | The 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. |
complexity | The candidate’s complexity. |
conditions_passed | How many of its conditions are true. |
conditions_failed | How many of its conditions are false. The candidate can only run if this is 0. |
content_type | YarnSaliencyStrategy.ContentType.NODE for a node, or YarnSaliencyStrategy.ContentType.LINE for a line. |
destination | Where the dialogue goes if this candidate is picked. Only the Dialogue Runner uses it. |
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
weightheader on each node and a#weighttag 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.
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!
===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.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]$lamp_lit in the top right.get_view_count() reads the count from the Variable Storage in the Dictionary.