Advanced Saliency

In this sample, you build a short scene out of storylets. You pick who plays the two roles, which room the scene is set in, and what kind of scene it is. Then you walk into the room and talk to the two characters. Every line they say comes from a node group, and the when: headers on each node decide which one runs.

When you run it, you start in the setup room, next to the Explainer. Around the room are four pillars, each with a sign and a button. The signs show the current choices: “The Primary role is played by Alice”, “The Secondary role is played by Barry”, “It is set inside a Office” and “It will be a Interogation scene”. Walk into a pillar to change its choice. Talk to the Explainer to start the scene, then walk down the corridor into the scenario room, where the two characters are waiting.

PLACEHOLDER — Screenshot needed
The Advanced Saliency sample in Play mode: the green player character and the Explainer in the setup room, with the four pillars and their signs, and the corridor leading to the scenario room.

Running it

Open the The Room scene in the Advanced 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.

The four pillars are:

Pillar’s signChangesChoices
The Primary role is played byWho plays the primary roleAlice, Barry, George, Liz
The Secondary role is played byWho plays the secondary roleAlice, Barry, George, Liz
It is set inside aThe roomOffice, Pub, Church, Mansion
It will be a … sceneThe scenarioInterogation, Explore, Rescue, Date
The pillars in the setup room. Each bump moves to the next choice, and goes back to the first after the last.

The Explainer won’t start the scene if the same character is in both roles. Every time you start a scene, the scenario room is rebuilt, so you can change the choices and play again.

How it works

There are three parts to the sample:

  • The Yarn Scripts, which hold every scenario as a set of node groups.
  • A generated Variable Storage, which lets the pillars change the Yarn variables.
  • The Level God, a component that builds the room when the Yarn Script tells it to.

The sample leaves the Dialogue Runner’s Saliency Strategy at the default, Random Best Least Recently Viewed. See Saliency and Storylets for how it picks.

The Room
Dialogue System Dialogue RunnerIn Memory Variable StorageThe Room Variable StorageLink Markup Handler
Scenario Room
Setup Room
bridge
Camera Rig Simple Camera Follow
Player Character ControllerSimple CharacterCharacter AppearanceMouth ViewButton Bump
Alice Character ControllerSimple CharacterCharacter AppearanceMouth ViewDialogue Interactable
Barry Character ControllerSimple CharacterCharacter AppearanceMouth ViewDialogue Interactable
George Character ControllerSimple CharacterCharacter AppearanceMouth ViewDialogue Interactable
Liz Character ControllerSimple CharacterCharacter AppearanceMouth ViewDialogue Interactable
Explanation Character Character ControllerSimple CharacterCharacter AppearanceMouth ViewDialogue Interactable
Level God Level God
void
Scenario Pillar Box ColliderValue Updater
column Mesh FilterMesh RendererMesh Collider
Canvas CanvasCanvas Scaler
Image Canvas RendererImage
Text (TMP) Canvas RendererTextMeshPro - Text (UI)
Scenario Pillar (1) Box ColliderValue Updater
Scenario Pillar (2) Box ColliderValue Updater
Scenario Pillar (3) Box ColliderValue Updater
The main parts of the sample scene. The props are left out. Barry, George and Liz are set up the same way as Alice, and the other pillars the same way as the first.

Scenario Room, Setup Room and void are three copies of the same arena, and bridge is the corridor between the first two. The four characters start in void, out of the way, until a scene needs them. The Explainer is an Explanation Character whose Dialogue Interactable starts the RoomSetup node.

The choices are Yarn variables

Room.yarn declares an enum for each choice, and a variable to hold it. These enums use strings for their values:

<<enum Character>>
    <<case Alice = "Alice">>
    <<case Barry = "Barry">>
    <<case George = "George">>
    <<case Liz = "Liz">>
<<endenum>>
The Character enum, from Room.yarn.
<<enum ScenarioState>>
    <<case NotStarted>>
    <<case Started>>
    <<case Complete>>
<<endenum>>

<<declare $primary = Character.Alice>>
<<declare $secondary = Character.Barry>>
<<declare $scenario = Scenario.Interogation>>
<<declare $Room = Room.Office>>

<<declare $speak_to_primary = false>>
<<declare $speak_to_secondary = false>>
<<declare $scenario_state = .NotStarted>>
Lines 1–5 How far through the scenario the player is. This enum has no values given, so its cases are stored as numbers.
Lines 7–10 The four choices the pillars change. The Scenario and Room enums are declared above these, the same way as Character.
Lines 12–14 Where the player is in the current scene. These are reset every time a scene starts.
The rest of the declarations, from the RoomSetup node in Room.yarn.

The Yarn Project has Generate Variables Source File turned on, with Variables Class Name set to TheRoomVariableStorage. Yarn Spinner writes TheRoomVariableStorage.cs, a subclass of In Memory Variable Storage with a C# enum for each Yarn enum and a property for each variable, such as Primary, Room and ScenarioState. A Yarn enum with string values is stored as its string, so setting Primary stores a name like "Alice", and GetBackingValue() turns a C# enum value back into that string. See Generating a typed variables class.

A The Room Variable Storage component is on the Dialogue System, and the Dialogue Runner uses it as its Variable Storage.

Each pillar has a Box Collider set as a trigger, and a Value Updater. The Value Updater’s Observation says which of the four variables it changes, Storage is the The Room Variable Storage, and Textfield is the sign’s text. The Player has a Button Bump component. When the player walks into a pillar, Button Bump finds the pillar’s Value Updater and calls UpdateValue(), which sets the variable to the next choice and updates the sign:

case ValueObserved.Room:
    {
        var allRooms = Enum.GetValues(typeof(Room));
        for (int i = 0; i < allRooms.Length; i++)
        {
            var iRoom = (Room)allRooms.GetValue(i);
            if (iRoom == storage.Room)
            {
                var newRoom = (Room)allRooms.GetValue((i + 1) % allRooms.Length);
                storage.Room = newRoom;
                break;
            }
        }

        break;
    }
Part of UpdateValue() in ValueUpdater.cs.

See Variables and Storage for more on custom Variable Storage.

Starting a scene

Talking to the Explainer runs the RoomSetup node. It can show the current choices, and if the roles are played by different characters, it offers to play the scenario. Choosing to play jumps to ResetAndPlay:

title: ResetAndPlay
---
<<set $speak_to_secondary = false>>
<<set $speak_to_primary = false>>
<<set $scenario_state = .NotStarted>>
<<start_level>>
===
Lines 3–5 Reset the variables that track the player’s progress, so the scenario starts from the beginning.
Line 6 A Command that builds the scenario room.
The ResetAndPlay node, from Room.yarn.

The Level God adds the start_level Command in its Start(), with runner.AddCommandHandler("start_level", SpawnLevel). In Start() it also remembers where each of the four characters is. SpawnLevel() does four things:

  1. Destroys the room it built last time, if there is one.
  2. Puts all four characters back where they started, in void.
  3. Looks up the current room’s RoomLayout in its Spawns list, finds the primary and secondary characters by name, and moves them to the layout’s spots.
  4. Creates the layout’s room from its Environment Prefab.
var primary = GameObject.Find(variableStorage.Primary.GetBackingValue());
var secondary = GameObject.Find(variableStorage.Secondary.GetBackingValue());

if (config.primary != null)
{
    primary.transform.SetPositionAndRotation(config.primary.position, config.primary.rotation);

    if (primary.TryGetComponent<SimpleCharacter>(out var character))
    {
        character.SetLookDirection(config.primary.rotation, immediate: true);
    }
}
Finding and placing the primary character, from SpawnLevel() in LevelGod.cs.

Each room has a Room Layout asset in the sample’s Layouts folder, such as Pub.asset. A layout has a Primary and a Secondary, each with a Position and a Rotation, and an Environment Prefab for the room, such as Pub.prefab. While a layout is selected, its custom editor shows the room in the Scene view, with handles you can drag to move the two spots. To make a new layout, choose AssetsCreateYarn SpinnerSamplesAdvanced Saliency Room Layout. See Commands for more on adding Commands.

Every character goes to Primary or Secondary

Each character’s Dialogue Interactable starts the node group with the character’s name. The scenarios are written for the primary and secondary roles, not for particular characters, so Room.yarn has two short nodes for each character that jump to the right group:

title: Alice
when: $primary == .Alice
---
<<jump Primary>>
===
title: Alice
when: $secondary == .Alice
---
<<jump Secondary>>
===
Lines 2–4 If Alice plays the primary role, run the Primary node group.
Lines 7–9 If Alice plays the secondary role, run the Secondary node group.
Alice’s two nodes, from Room.yarn. Barry, George and Liz have the same pair.

A character who isn’t in either role has nothing that passes, so the Dialogue Interactable doesn’t show the speaking indicator over them.

Scenarios as storylets

Each scenario is a set of nodes in the Primary and Secondary node groups, in its own file, such as Room-Exploration.yarn and Room-Date.yarn. Every node checks $scenario, and most also check $scenario_state, and whether the player has talked to the other character yet. As the player talks to each character, the nodes change these variables, and different nodes start to pass.

title: Primary
when: $scenario == .Explore
when: $scenario_state == .NotStarted
---
Player: ok time to wander around aimlessly looking for stuff
{$primary}: Great idea
Player: ok, any idea where to look?
{$primary}: I think {$secondary} had some ideas
Player: gotcha

<<set $scenario_state = .Started>>
===
Lines 2–3 Only in the Explore scenario, before it has started.
Line 6 {$primary} is the name of whoever plays the primary role, so the same node works for every character.
Line 11 Start the scenario. This node stops passing, and the Primary nodes for a started Explore scenario start to pass.
The first node of the Explore scenario, from Room-Exploration.yarn.

The Interrogation, Rescue and Date scenarios work the same way. Explore also uses a line group, with a condition on each line, to change what the secondary character says depending on the room.

More specific nodes win

The default strategy picks the candidate with the highest complexity, from the ones seen the fewest times. The sample uses this to give some combinations their own version of a scenario. Room-Explore-Mansion-Liz-Alice.yarn has a different Explore scenario, for when it’s set in the Mansion and Alice and Liz play the two roles:

title: Primary
when: $scenario == .Explore
when: $Room == .Mansion
when: $primary == .Alice || $primary == .Liz
when: $secondary == .Alice || $secondary == .Liz
when: $scenario_state == .NotStarted
---
Player: hey
{$primary}: Can't talk, I am too busy uncovering the Bone Secrets.
Player: that sounds cool
{$primary}: it will be, yes.
<<set $scenario_state = .Started>>
===
Lines 2–6 Five conditions, two of them with ||, so this node’s complexity is 7. The ordinary Explore node above has a complexity of 2.
The first node of the Mansion version, from Room-Explore-Mansion-Liz-Alice.yarn.

With that setup, both versions pass, and neither has been seen. The Mansion version has the higher complexity, so it wins. Every other node in the Mansion version has the same extra conditions, so they win over the ordinary Explore nodes for the rest of the scene.

Room-Interrogation.yarn does the same for one character. It adds a node to Alice’s own node group, with when: $primary == .Alice, the Interrogation conditions, and when: once. Its complexity of 4 beats the complexity of 1 on Alice’s node that jumps to Primary, so the first time Alice plays the primary role in an Interrogation, she gives a short speech of her own.

The view counts aren’t reset when a scene starts. Once you’ve played the Mansion version of Explore more times than the ordinary one, the ordinary Explore nodes have been seen fewer times. The default strategy looks at view counts before complexity, so it picks them instead.

Things to try

  • Select the Dialogue System, and on its Dialogue Runner, set Saliency Strategy to Best. The Best strategy ignores view counts, so the Mansion version of Explore plays every time you choose it.

  • Add a node to the Date scenario for one room, such as:

    title: Secondary
    when: $scenario == .Date
    when: $scenario_state == .NotStarted
    when: $Room == .Pub
    ---
    {$secondary}: Fancy seeing you two in here.
    ===
    A new node for Room-Date.yarn.

    It has one more condition than the ordinary Secondary node for a Date that hasn’t started. In a Date in the Pub, the first time you talk to the secondary character, they say the new line.

  • Select the Office layout in the Layouts folder, and drag the Primary and Secondary handles in the Scene view to move where the characters stand in the office.

Next step Background Chatter Characters who talk among themselves, with their lines floating above their heads, while the player keeps moving.