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.
Running it
Open the The Room scene in the Advanced Saliency sample’s folder and press Play.
| Key | What it does |
|---|---|
| W, A, S, D or the arrow keys | Move |
| Space | Talk to the character you’re next to |
| Space, or the button in the dialogue box | Go to the next line |
The four pillars are:
| Pillar’s sign | Changes | Choices |
|---|---|---|
| The Primary role is played by | Who plays the primary role | Alice, Barry, George, Liz |
| The Secondary role is played by | Who plays the secondary role | Alice, Barry, George, Liz |
| It is set inside a | The room | Office, Pub, Church, Mansion |
| It will be a … scene | The scenario | Interogation, Explore, Rescue, Date |
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.
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>><<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>>Scenario and Room enums are declared above these, the same way as Character.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;
}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>>
===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:
- Destroys the room it built last time, if there is one.
- Puts all four characters back where they started, in void.
- Looks up the current room’s
RoomLayoutin its Spawns list, finds the primary and secondary characters by name, and moves them to the layout’s spots. - 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);
}
}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 . 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>>
===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>>
==={$primary} is the name of whoever plays the primary role, so the same node works for every character.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>>
===||, so this node’s complexity is 7. The ordinary Explore node above has a complexity of 2.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
Officelayout in theLayoutsfolder, and drag the Primary and Secondary handles in the Scene view to move where the characters stand in the office.