Basic Saliency

Four characters stand in a room, and what each of them says depends on the day and the time of day. Several pieces of content can often run at once, and Yarn Spinner picks one. Alice, Barry and Liz each have a node group, and George has a line group.

When you run it, the sign at the back of the room says “It is Monday morning.” A switch on the floor in front of the sign moves time on. Walk into it and the sign changes to Monday evening, then Tuesday morning, and so on. Talk to the characters before and after, and they say different things.

PLACEHOLDER — Screenshot needed
The Basic Saliency sample in Play mode: a walled arena with a checked floor, with four characters along the back wall either side of an orange floor switch below a sign reading It is Monday morning, the green player character in the middle, and Capsley holding a coffee cup.

Running it

Open the BasicSaliencyDemo scene in the Basic Saliency sample’s folder and press Play.

KeyWhat it does
W, A, S, D or the arrow keysMove
SpaceTalk to the character you’re next to
Space, or the button in the dialogue boxGo to the next line
The controls.

Walk into the orange switch at the back of the room to move time on. Each time, the time goes from morning to evening, or from evening to the next morning. After Wednesday evening, it goes back to Monday morning.

Capsley, the blue character with the coffee cup, explains the sample. Capsley’s node has when: once, so after you’ve talked to Capsley once, the speaking indicator doesn’t appear over Capsley again.

How it works

The day and the time are two Yarn variables, $day and $time. Every node in the characters’ node groups has when: headers that check them. Each time you talk to a character, the Dialogue Runner works out which of that character’s nodes can run, and its saliency strategy picks one. The sample leaves the Dialogue Runner’s Saliency Strategy at the default, Random Best Least Recently Viewed. See Saliency and Storylets for how the strategies pick.

BasicSaliencyDemo
Basic Arena
Camera Rig Simple Camera Follow
Player Character ControllerSimple CharacterCharacter AppearanceMouth View
Alice Character ControllerSimple CharacterCharacter AppearanceMouth ViewDialogue Interactable
Barry Character ControllerSimple CharacterCharacter AppearanceMouth ViewDialogue Interactable
Liz Character ControllerSimple CharacterCharacter AppearanceMouth ViewDialogue Interactable
George Character ControllerSimple CharacterCharacter AppearanceMouth ViewDialogue Interactable
Dialogue System Dialogue RunnerIn Memory Variable StorageBasic Saliency Variable StorageLink Markup Handler
Time Advancer Box ColliderTime Advancer
button-floor-square Mesh FilterMesh Renderer
column Mesh FilterMesh RendererMesh Collider
Canvas CanvasCanvas Scaler
Image Canvas RendererImage
Text (TMP) Canvas RendererTextMeshPro - Text (UI)
Explanation Character Character ControllerSimple CharacterCharacter AppearanceMouth ViewDialogue Interactable
The main parts of the sample scene. Each character’s Dialogue Interactable starts the node group named after the character.

The day and the time

The Yarn Script declares two enums, and a variable for each:

<<enum Day>>
    <<case Monday>>
    <<case Tuesday>>
    <<case Wednesday>>
<<endenum>>

<<enum TimeOfDay>>
    <<case Morning>>
    <<case Evening>>
<<endenum>>

<<declare $day = Day.Monday>>
<<declare $time = TimeOfDay.Morning>>
Lines 1–5 The days the sample uses.
Lines 7–10 The two times of day.
Line 12 The current day. It starts on Monday.
Line 13 The current time. It starts in the morning.
Part of the Setup node in BasicSaliency.yarn.

Nothing runs the Setup node. Declarations apply to the whole Yarn Project, so it never has to run.

In a condition, .Monday is short for Day.Monday. Yarn Spinner works out the enum from the variable it’s compared with.

Node groups

Alice has a node group with eight nodes. Some depend on the day, and some on the day and the time. On Wednesday evening, two of them can run:

title: Alice
when: $day == .Wednesday
when: $time == .Evening
when: once
---
Player: Hello
Alice: the week is half over, what a relief.
===
title: Alice
when: $day == .Wednesday
when: $time == .Evening
---
Player: Hello
Alice: Probably should go to bed, it is Wednesday evening after all.
===
Lines 2–4 Two conditions and once, so this node’s complexity is 3. It can only run once.
Lines 10–11 The same two conditions without once, so this node’s complexity is 2.
Alice’s two Wednesday evening nodes, from BasicSaliency.yarn.

The default strategy first looks for the candidates seen the fewest times, then picks the one with the highest complexity. The first time you talk to Alice on a Wednesday evening, neither node has been seen, so the once node wins. After that it stops passing, and Alice says the other line.

The other characters show other ways of writing node groups:

  • Barry has three nodes for the morning and three for the evening, each with one condition. The default strategy picks the one seen the fewest times, so Barry works through all three before he repeats one.
  • Liz’s nodes don’t depend on the day or the time. Three of them have when: always, and one has when: once and when: always. The once node has the highest complexity, so it’s the first thing Liz says.
  • Alice has two nodes for Tuesday evening and two for Wednesday morning, with the same conditions. When nodes tie like this, she takes turns between them.

Each character’s Dialogue Interactable starts the node group with the character’s name, such as Alice. Before showing the speaking indicator, it asks the Dialogue Runner’s saliency strategy whether anything in the group can run. See Saliency and Storylets.

A line group

George’s node is a single node with a line group in it. Each line starts with =>, and some have a condition:

title: George
---
=> George: oh hi
=> George: another Monday, I hate Mondays <<if $day == .Monday>>
=> George: I suppose I should make breakfast <<if $time == .Morning>>
=> George: Weird how tomorrow is Monday, right? <<if $day == .Wednesday && $time == .Evening>>
    Player: did you want the devs to have to write out even more sample content?
    George: it would be nice, yes
    Player: well they aren'that gonna do that
    George: boo
=> George: I wonder if I am in any other samples
===
George’s node, from BasicSaliency.yarn.

The Dialogue Runner picks one line from the group with the same strategy as the node groups. The lines with no condition can always be picked. The Wednesday evening line has two conditions joined with &&, so it has the highest complexity, and the first time you talk to George on a Wednesday evening, he says it. The indented lines under it only run when that line is picked.

Changing the time from C#

The Yarn Project has Generate Variables Source File turned on, with Variables Class Name set to BasicSaliencyVariableStorage and Variables Parent Class set to InMemoryVariableStorage. Yarn Spinner writes BasicSaliencyVariableStorage.cs from the variables the scripts declare, with a C# enum for each Yarn enum, and a property for each variable. See Generating a typed variables class.

public enum Day {

    Monday = 0,

    Tuesday = 1,

    Wednesday = 2,
}

public partial class BasicSaliencyVariableStorage : Yarn.Unity.InMemoryVariableStorage, Yarn.Unity.IGeneratedVariableStorage {
    public Day Day {
        get => this.GetEnumValueOrDefault<Day>("$day");
        set => this.SetValue("$day", value.GetBackingValue());
    }
Lines 1–8 A C# copy of the Yarn enum. Yarn stores each case as a number, starting from 0, in the order the cases are declared.
Line 10 The class is an In Memory Variable Storage, with a property for each variable.
Lines 11–14 $day as a Day. Reading it gets the variable, and setting it stores the new value.
Part of the generated BasicSaliencyVariableStorage.cs.

A Basic Saliency Variable Storage component is on the Dialogue System, and the Dialogue Runner’s Variable Storage is set to it. Game code can then read and change the day and the time without knowing the variable names.

The switch is a GameObject called Time Advancer, with a Box Collider set to Is Trigger, and a Time Advancer component. Its Variable Store is the Basic Saliency Variable Storage, and its Label is the sign’s text. When something walks into it, it moves time on through the Variable Storage, and updates the sign:

private void AdvanceTime()
{
    if (variableStore == null)
    {
        Debug.Log($"Can't advance time: {nameof(variableStore)} is not set");
        return;
    }

    if (variableStore.Time == TimeOfDay.Morning)
    {
        variableStore.Time = TimeOfDay.Evening;
        return;
    }

    variableStore.Time = TimeOfDay.Morning;
    switch (variableStore.Day)
    {
        case Day.Monday:
            variableStore.Day = Day.Tuesday;
            break;
        case Day.Tuesday:
            variableStore.Day = Day.Wednesday;
            break;
        case Day.Wednesday:
            variableStore.Day = Day.Monday;
            break;
    }
}
Lines 9–13 Morning becomes evening.
Lines 15–27 Evening becomes the next morning, and the day moves on.
Part of TimeAdvancer.cs.

OnTriggerEnter() calls AdvanceTime(), then UpdateLabel(), which writes “It is Tuesday morning.” or similar on the sign.

The next time you talk to someone, the when: headers are checked against the new values. For more on custom Variable Storage, see Variables and Storage.

Things to try

  • Select the Dialogue System, and on its Dialogue Runner, set Saliency Strategy to Best. Run the scene and talk to Barry a few times. The Best strategy doesn’t count how many times each node has been seen, so Barry says the same line every time.
  • Add another node to Barry’s node group with two conditions, such as when: $time == .Morning and when: $day == .Tuesday. It has a higher complexity than his other morning nodes, so it’s the first thing he says on a Tuesday morning.
  • Add a line to George’s line group with <<if $time == .Evening>> at the end, and talk to him in the evening.
Next step Custom Saliency A custom saliency strategy that makes some content more likely than the rest.