Smart Variables

A smart variable is declared with an expression instead of a value. Its value is worked out from that expression every time something reads it, so it’s always up to date with the variables it’s made from.

<<declare $lamp_oil = 3>>
<<declare $lamp_hours = $lamp_oil * 2>>
<<declare $lamp_needs_oil = $lamp_oil < 5>>
Line 1 An ordinary variable. It’s declared with a value, and it’s stored in the Variable Storage.
Lines 2–3 Smart variables. They’re declared with an expression that uses another variable, and they aren’t stored.
One ordinary variable, and two smart variables worked out from it.

Only $lamp_oil is kept in the Variable Storage. When the oil goes up, $lamp_hours and $lamp_needs_oil change with it, without anything setting them.

flowchart LR
  oil[("Ordinary variable<br><br>$lamp_oil = 3")]
  hours["Smart variable<br><br>$lamp_hours<br>= $lamp_oil * 2<br>= 6"]
  needs["Smart variable<br><br>$lamp_needs_oil<br>= $lamp_oil #60; 5<br>= true"]
  oil --> hours
  oil --> needs
Setting $lamp_oil is enough. The two smart variables are worked out from it whenever they’re read.

Using smart variables in Yarn

Use a smart variable anywhere you’d use any other variable: in a line, in an <<if>>, or in another expression.

<<declare $lamp_oil = 3>>
<<declare $lamp_hours = $lamp_oil * 2>>
<<declare $lamp_needs_oil = $lamp_oil < 5>>
Rosa: There's only enough oil for {$lamp_hours} more hours.
<<set $lamp_oil to $lamp_oil + 4>>
Rosa: Thanks. Now it'll last {$lamp_hours} hours.
<<if $lamp_needs_oil>>
    Rosa: It'll need topping up soon.
<<endif>>
Smart variables in lines and a condition.

You can’t <<set>> a smart variable. The Yarn Project won’t compile, and the error says what the variable is always equal to. Change the variables it’s made from instead.

For more on writing smart variables, see Smart Variables in the Yarn docs.

Using smart variables from C#

There’s nothing to set up in Unity. Read a smart variable the same way as any other variable, with TryGetValue() on the Dialogue Runner’s Variable Storage, and you get its value as it is right now:

var storage = dialogueRunner.VariableStorage;
storage.TryGetValue<float>("$lamp_hours", out var hours);
storage.TryGetValue<bool>("$lamp_needs_oil", out var needsOil);
Reading two smart variables from C#.

The Dialogue Runner gives its Variable Storage what it needs to work out smart variables when it creates its Dialogue. If you write your own Variable Storage, subclass VariableStorageBehaviour and you don’t need to do anything extra for smart variables.

A few things work differently from ordinary variables:

  • Don’t set them from C#. SetValue() doesn’t stop you, but it stores a value under the smart variable’s name, and from then on that stored value is used in place of the expression. Change the variables it’s made from.
  • They don’t tell you when they change. AddChangeListener() throws an ArgumentException if you give it a smart variable’s name, and listeners added for every variable are only called when a variable is set, which smart variables never are. To keep something up to date, read the smart variable again when you need it, for example every frame, or when one of the variables it’s made from changes.
  • They aren’t saved. GetAllVariables(), and the Dialogue Runner’s SaveStateToPersistentStorage(), only save stored variables. After loading, smart variables are worked out again from the loaded values, so they come back correct.
  • The In Memory Variable Storage’s Debug Variables view doesn’t list them, because it only shows stored variables.

In a generated variables class

If your Yarn Project has Generate Variables Source File turned on, Yarn Spinner writes a C# class with a property for each variable. Add that class to a GameObject as the Dialogue Runner’s Variable Storage. A smart variable’s property only has a getter, so you can read it but not set it:

var variables = (YarnVariables)dialogueRunner.VariableStorage;
variables.LampOil += 4;
Debug.Log($"{variables.LampHours} hours of oil");
Reading smart variables from a generated class called YarnVariables.

Snake case names are turned into Pascal case, so $lamp_hours becomes LampHours. See Variables and Storage for how to set up a generated class.

An example

This script shows the oil level and the hours of oil left in a TextMeshPro text, and makes a light brighter as $lamp_hours goes up. It reads all three variables every frame, because the smart variables don’t say when they change.

using TMPro;
using UnityEngine;
using Yarn.Unity;

public class LampDisplay : MonoBehaviour
{
    [SerializeField] DialogueRunner dialogueRunner;
    [SerializeField] TMP_Text statusText;
    [SerializeField] Light lamp;

    void Update()
    {
        var storage = dialogueRunner.VariableStorage;
        storage.TryGetValue<float>("$lamp_oil", out var oil);
        storage.TryGetValue<float>("$lamp_hours", out var hours);
        storage.TryGetValue<bool>("$lamp_needs_oil", out var needsOil);

        statusText.text = $"$lamp_oil = {oil}\n$lamp_hours = {hours} (smart)\n$lamp_needs_oil = {needsOil} (smart)";

        lamp.intensity = Mathf.Clamp01(hours / 14f);
    }
}
Lines 7–9 Objects in the scene, set in the Inspector.
Line 11 Smart variables don’t tell you when they change, so read them every frame.
Lines 13–16 Read the stored variable and the two smart variables, the same way.
Line 18 Show the values.
Line 20 Make the lamp brighter the more hours of oil it has, up to full at 14 hours.
A script that reads one stored variable and two smart variables every frame.
Next step Saliency and Storylets How the Dialogue Runner picks one line or node when several could run.