Saliency and Storylets
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 with StartDialogue(), 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 C#, dialogueRunner.Dialogue.HasSalientContent("Rosa") does the same thing. It throws an ArgumentException if there’s no node with that name.
Choosing a strategy
Set the Dialogue Runner’s Saliency Strategy in the Inspector:
| Saliency Strategy | Picks |
|---|---|
| Random Best Least Recently Viewed | 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. |
| First Best Least Recently Viewed | The same as Random Best Least Recently Viewed, 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. |
| Custom | Your own strategy, set from C#. See Custom Saliency Strategies. |
Only the two Least Recently Viewed 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.
The Dialogue Runner sets up the strategy each time StartDialogue() is called. To change the strategy from C#, set Saliency Strategy to Custom, and give the Dialogue Runner the strategy you want, which can be one of the built-in ones:
dialogueRunner.Dialogue.ContentSaliencyStrategy = new Yarn.Saliency.BestSaliencyStrategy();Looking at a node group from C#
Dialogue.GetSaliencyOptionsForNodeGroup() 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. It doesn’t change any variables.
foreach (var candidate in dialogueRunner.Dialogue.GetSaliencyOptionsForNodeGroup("Rosa"))
{
var passes = candidate.FailingConditionValueCount == 0;
Debug.Log($"{candidate.ContentID} passes: {passes}");
}Each candidate is a ContentSaliencyOption, from the Yarn.Saliency namespace:
| Property | What it is |
|---|---|
ContentID | The candidate’s ID. In a node group, it’s a name that Yarn Spinner makes for the node, such as Rosa.3d371ffd. If the node has a subtitle: header, the name uses that instead, such as Rosa.lamp_out. In a line group, it’s the line’s ID. |
ComplexityScore | The candidate’s complexity. |
PassingConditionValueCount | How many of its conditions are true. |
FailingConditionValueCount | How many of its conditions are false. The candidate can only run if this is 0. |
ContentType | ContentSaliencyContentType.Node for a node, or ContentSaliencyContentType.Line for a line. |
ViewCountKey | The name of the variable the Least Recently Viewed strategies use to count how many times the candidate has been seen. |
If you give GetSaliencyOptionsForNodeGroup() the name of an ordinary node, it returns one candidate for that node, which always passes. If there’s no node with that name, it throws an ArgumentException. Dialogue.IsNodeGroup() tells you whether a name belongs to a node group.
To find out how many times a candidate has been seen, read its view count from the Variable Storage:
dialogueRunner.VariableStorage.TryGetValue<float>(candidate.ViewCountKey, out var seen);An example
This script shows an icon over a character while they have something to say. It checks the node group every frame, so the icon appears and disappears as variables change.
using UnityEngine;
using Yarn.Unity;
public class TalkIndicator : MonoBehaviour
{
[SerializeField] DialogueRunner dialogueRunner;
[SerializeField] GameObject icon;
[SerializeField] string nodeGroup = "Rosa";
void Update()
{
if (dialogueRunner.IsDialogueRunning)
{
icon.SetActive(false);
return;
}
icon.SetActive(dialogueRunner.Dialogue.HasSalientContent(nodeGroup));
}
}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 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.