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!
===
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 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?
===
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 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 StrategyPicks
Random Best Least Recently ViewedFrom 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 ViewedThe same as Random Best Least Recently Viewed, 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.
CustomYour own strategy, set from C#. See Custom Saliency Strategies.
The Dialogue Runner’s Saliency Strategy options.
PLACEHOLDER — Screenshot needed
The Inspector for a Dialogue Runner, with the Saliency Strategy dropdown open, showing Random Best Least Recently Viewed, First Best Least Recently Viewed, Best, First and Custom.

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();
Changing to the Best strategy from C#, with Saliency Strategy set to Custom.

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}");
}
Printing each candidate in the Rosa node group.

Each candidate is a ContentSaliencyOption, from the Yarn.Saliency namespace:

PropertyWhat it is
ContentIDThe 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.
ComplexityScoreThe candidate’s complexity.
PassingConditionValueCountHow many of its conditions are true.
FailingConditionValueCountHow many of its conditions are false. The candidate can only run if this is 0.
ContentTypeContentSaliencyContentType.Node for a node, or ContentSaliencyContentType.Line for a line.
ViewCountKeyThe name of the variable the Least Recently Viewed strategies use to count how many times the candidate has been seen.
The properties of each ContentSaliencyOption.

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);
Reading how many times a candidate has been 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));
    }
}
Lines 6–8 The Dialogue Runner, the icon to show, and the name of the node group. Set them in the Inspector.
Lines 12–16 Hide the icon while a conversation is running.
Line 18 Show the icon if the saliency strategy would pick something from the node group.
A script that shows an icon while a node group has content that can run.

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 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.
Next step Custom Saliency Strategies Writing your own way to pick content from node groups and line groups.