Custom Saliency
This sample gives the Dialogue Runner its own saliency strategy, WeightedSaliencySelector. It picks content at random, with some content more likely to be picked than the rest. Each node gets its weight from a weight: header, and each line in a line group gets its weight from a #weight: tag.
When you run it, there are two characters to talk to. Alice has a node group of five nodes, and Barry has a line group of three lines. Each node and line says how many chances it has of being picked. Talk to them several times to see which comes up most.
Running it
Open the Custom Saliency scene in the Custom 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 |
In the Hierarchy, Alice is Nodegroup NPC, and Barry is Linegroup NPC.
How it works
The Weighted Saliency Selector component, on the Custom Saliency Selector GameObject, gives the Dialogue Runner the new strategy. When the dialogue reaches Alice’s node group or Barry’s line group, the Dialogue Runner asks the Weighted Saliency Selector to pick. See Custom Saliency Strategies for how a strategy works.
Weights in Yarn
Alice’s node group has five nodes. The first two are:
title: Alice
when: always
---
<<detour AliceIntro>>
Alice: This node only has one chance of being selected.
Alice: You should revisit me to see if I show another.
===
title: Alice
when: always
weight: 2
---
<<detour AliceIntro>>
Alice: This node has two chances of being selected
Alice: You should revisit me to see if I show another.
===weight: header, so the strategy gives this node a weight of 1.when: once. Alice introduces the sample the first time you talk to her, and never again.The other three nodes have weights of 3, 4 and 5. The weights add up to 15, so the first node has a 1 in 15 chance of being picked, and the last has a 5 in 15 chance.
Barry has one node, which uses line groups:
title: Barry
when: always
---
<<if !visited("Barry")>>
Barry: I am showing off custom weighted saliency.
=> Barry: But doing it through line groups instead of nodes. <<if visited("Alice")>>
=> Barry: where each line in the line group has it's own weighting. <<if !visited("Alice")>>
Barry: See [link="6ec709ebde00a4bf0b6a4fda5550476c"]WeightedSaliencySelector.cs[/link] to see how it works.
<<endif>>
=> Barry: This line has a weight of 1 #weight:1
=> Barry: This line has a weight of 2 #weight:2
=> Barry: This line has a weight of 3 #weight:3
===visited("Barry") is false.#weight: tag. The weights add up to 6, so the last line is picked half the time.The strategy
WeightedSaliencySelector is a MonoBehaviour that implements IContentSaliencyStrategy. Its QueryBestContent() works like a dice roll:
public ContentSaliencyOption? QueryBestContent(IEnumerable<ContentSaliencyOption> content)
{
List<(ContentSaliencyOption option, int min, int max)> ranges = new();
int diceSize = 0;
foreach (var element in content)
{
if (element.FailingConditionValueCount > 0)
{
continue;
}
string? weightString = null;
if (element.ContentType == ContentSaliencyContentType.Node)
{
if (runner != null)
{
weightString = runner.Dialogue.GetHeaderValue(element.ContentID, WeightKey);
}
}
else
{
var lineKey = WeightKey + ':';
string[]? metadata = null;
if (runner != null && runner.YarnProject != null)
{
metadata = runner.YarnProject.lineMetadata?.GetMetadata(element.ContentID);
}
metadata ??= Array.Empty<string>();
foreach (var tag in metadata)
{
if (tag.StartsWith(lineKey))
{
weightString = tag.Substring(lineKey.Length).Trim();
break;
}
}
}
if (Int32.TryParse(weightString, out int weight))
{
if (weight < 1)
{
weight = 1;
}
ranges.Add((element, diceSize, diceSize + weight - 1));
diceSize += weight;
}
else
{
ranges.Add((element, diceSize, diceSize));
diceSize += 1;
}
}
if (ranges.Count == 0)
{
return null;
}
int roll = UnityEngine.Random.Range(0, diceSize);
foreach (var (option, min, max) in ranges)
{
if (roll <= max && roll >= min)
{
return option;
}
}
return null;
}- It skips every candidate with a failed condition.
- It finds each of the others’ weight, and gives it a range of numbers as wide as its weight. With weights of 1, 2 and 3, the ranges are 0, 1 to 2, and 3 to 5.
- It picks a random number from 0 up to the total weight, less 1, and returns the candidate whose range it’s in.
- If no candidate passes, it returns
null, so nothing runs.
Because candidates that fail are left out before the roll, a candidate’s chance depends on which others pass at the time.
For a node, ContentID is the node’s name, and Dialogue.GetHeaderValue() reads its weight header. For a line, ContentID is the line’s ID. The Yarn Project’s lineMetadata gives the line’s tags, and the strategy looks for the one that starts with weight: and takes the rest of it. If there’s no weight, or it isn’t a whole number, the weight is 1. A weight less than 1 becomes 1.
The strategy doesn’t remember anything between picks, so its ContentWasSelected() does nothing. This matters because the Dialogue Interactable on each character calls QueryBestContent() to decide whether to show the speaking indicator. A strategy that changed something in QueryBestContent() would change it every time the player walked near a character.
The built-in strategies count how many times each candidate has been seen. This one doesn’t, so it can pick the same node or line several times in a row. when: once on AliceIntro still works, because once is a condition, and the strategy never picks a candidate whose conditions fail.
Setting the strategy
void Start()
{
if (runner == null)
{
runner = FindAnyObjectByType<Yarn.Unity.DialogueRunner>();
if (runner == null)
{
throw new InvalidOperationException($"Failed to find a {nameof(DialogueRunner)} in the scene!");
}
}
runner.Dialogue.ContentSaliencyStrategy = this;
}The component’s Runner is empty in the scene, so Start() finds the Dialogue Runner itself. Then it sets the Dialogue Runner’s Dialogue.ContentSaliencyStrategy to the component.
Every time StartDialogue() is called, the Dialogue Runner replaces the strategy with the one chosen in its Saliency Strategy setting, unless that setting is Custom. The sample’s Dialogue Runner is left at Random Best Least Recently Viewed, so the Weighted Saliency Selector is thrown away as soon as you start talking to someone, and the weights are ignored. With Saliency Strategy set to Custom, the Dialogue Runner keeps the strategy that Start() set. See Custom Saliency Strategies.
Things to try
- With Saliency Strategy set to Custom, change Alice’s last node from
weight: 5toweight: 50. She’ll say “This node has five chances of being selected” nearly every time. - Change
#weight:1on Barry’s first line to#weight:10. It becomes the line he says most often. - Set Saliency Strategy back to Random Best Least Recently Viewed, which ignores the weights. Alice says each of her five nodes once before she repeats any of them.