Custom Saliency Strategies

When the dialogue reaches a node group or a line group, the Dialogue Runner asks its saliency strategy to pick one candidate. Saliency and Storylets covers the strategies that come with Yarn Spinner. If none of them pick the way you want, you can write your own.

What a strategy does

A saliency strategy is a class that implements IContentSaliencyStrategy, from the Yarn.Saliency namespace. It has two methods:

  • QueryBestContent(content) is given every candidate, including the ones that don’t pass, and returns the one to run. It returns null if nothing should run.
  • ContentWasSelected(content) is called after a candidate has been picked, before it runs.
sequenceDiagram
  participant Runner as Dialogue Runner
  participant Strategy as Your strategy
  Runner->>Strategy: QueryBestContent(content)
  Strategy-->>Runner: the candidate to run, or null
  Runner->>Strategy: ContentWasSelected(content)
  Note over Runner: The picked candidate runs
What the Dialogue Runner asks the strategy when the dialogue reaches a node group or a line group.

Each candidate is a ContentSaliencyOption, with the properties described in Looking at a node group from C#. QueryBestContent() must return one of the candidates it was given, or null. If it returns anything else, Yarn Spinner throws a DialogueException.

The strategy isn’t given anything else. If it needs more, such as the Variable Storage or the node headers, pass them in when you create it. dialogueRunner.Dialogue.GetHeaderValue(nodeName, headerName) reads a node’s header, and dialogueRunner.YarnProject.lineMetadata.GetMetadata(lineID) reads a line’s tags.

An example

This strategy picks at random, but never picks the same candidate twice in a row:

using System.Collections.Generic;
using System.Linq;
using Yarn.Saliency;

public class NoRepeatSaliencyStrategy : IContentSaliencyStrategy
{
    string lastPicked;

    public ContentSaliencyOption QueryBestContent(IEnumerable<ContentSaliencyOption> content)
    {
        var valid = content.Where(c => c.FailingConditionValueCount == 0).ToList();
        if (valid.Count == 0)
        {
            return null;
        }

        var choices = valid.Where(c => c.ContentID != lastPicked).ToList();
        if (choices.Count == 0)
        {
            choices = valid;
        }

        return choices[UnityEngine.Random.Range(0, choices.Count)];
    }

    public void ContentWasSelected(ContentSaliencyOption content)
    {
        lastPicked = content.ContentID;
    }
}
Line 5 A plain C# class. It doesn’t need to be a MonoBehaviour.
Line 9 Called with every candidate, including the ones that don’t pass. Return the one to run, or null to run nothing.
Lines 11–15 Keep the candidates that pass. If there are none, nothing runs.
Lines 17–21 Leave out the one picked last time, unless it’s the only one left.
Line 23 Pick one of the rest at random.
Lines 26–29 Called after a candidate has been picked and is about to run. Remember it for next time.
NoRepeatSaliencyStrategy.cs.

Code that only checks whether anything can run, such as has_any_content() and HasSalientContent(), calls QueryBestContent() too, and nothing runs afterwards. Don’t change any state in QueryBestContent(). Make changes in ContentWasSelected(), which is only called when a candidate is about to run.

If your strategy uses view counts, like the built-in Least Recently Viewed strategies, keep them in the Variable Storage so they’re saved with your other variables. Each candidate’s ViewCountKey is a variable name you can use for its count. Read it with TryGetValue<float>() in QueryBestContent(), and add 1 to it with SetValue() in ContentWasSelected().

The same strategy is used for node groups and line groups. Use ContentType if it needs to treat them differently.

Using your strategy

In the Inspector, set the Dialogue Runner’s Saliency Strategy to Custom.

PLACEHOLDER — Screenshot needed
The Inspector for a Dialogue Runner, with Saliency Strategy set to Custom, highlighted.

In code, give the Dialogue Runner your strategy by setting Dialogue.ContentSaliencyStrategy:

void Start()
{
    dialogueRunner.Dialogue.ContentSaliencyStrategy = new NoRepeatSaliencyStrategy();
}
Using NoRepeatSaliencyStrategy, from a script in the scene.

Set it before the dialogue reaches a node group or a line group. Setting it before StartDialogue() is called is the simplest way to be sure.

Saliency Strategy has to be Custom. Every time StartDialogue() is called, the Dialogue Runner replaces the strategy with the one chosen in Saliency Strategy, unless it’s set to Custom. If you set your own strategy while Saliency Strategy is still Random Best Least Recently Viewed, it’s thrown away as soon as the dialogue starts.

If Saliency Strategy is Custom but you never set a strategy, the Dialogue Runner uses Random Best Least Recently Viewed.

Another example

The Custom Saliency sample has a strategy that picks at random, but makes some candidates more likely than others. Each node gets a weight header, and each line in a line group gets a #weight tag. The strategy is a MonoBehaviour that reads node headers with Dialogue.GetHeaderValue(), and line tags from the Yarn Project’s lineMetadata.

Next step Async and Cancellation Commands and Presenters that wait, and what happens when the dialogue is stopped.