Variables and Storage

Yarn variables, like $trust or $visitor_name, hold values that your dialogue can check and change. They’re kept in a Variable Storage, which your Yarn Scripts and your C# code share. Yarn Scripts set variables with <<set>>, check them in <<if>>, and show them in lines with {$visitor_name}. Your game’s C# code reads and changes the same variables, and can be told when they change.

A variable set by one side is seen by the other. Your game can set $visitor_name before a conversation, and the dialogue uses it. The dialogue can set $lamp_fixed, and your game lights the lamp.

flowchart LR
  yarn["Your Yarn Scripts<br>#60;#60;set#62;#62;, #60;#60;if#62;#62;, {$variable}"]
  storage[("Variable Storage<br>$trust = 2<br>$visitor_name = Sam")]
  game["Your C# code<br>SetValue, TryGetValue,<br>AddChangeListener"]
  save["Save file in<br>Application.persistentDataPath"]
  yarn <-->|read and change| storage
  game <-->|read and change| storage
  storage <-.->|save and load| save
Your Yarn Scripts and your C# code both read and change the variables in the Variable Storage. The Dialogue Runner can save them to a file and load them back.
<<declare $trust = 0>>
<<declare $lamp_fixed = false>>
<<declare $visitor_name = "stranger">>
Rosa: Hello, {$visitor_name}.
<<set $trust to $trust + 2>>
<<if $trust >= 2>>
    Rosa: Thanks for the help.
<<endif>>
Three variables, declared with a default value, then set and checked.

Where variables are kept

Each Dialogue Runner has a Variable Storage. If you leave that setting empty, the Dialogue Runner adds an In Memory Variable Storage to its GameObject when the game runs. It keeps variables in memory while the game runs, and they’re lost when the game closes, unless you save them.

The Dialogue System prefab from GameObjectYarn SpinnerDialogue System already has an In Memory Variable Storage, next to its Dialogue Runner, and the Dialogue Runner’s Variable Storage is set to it:

Dialogue System Dialogue RunnerIn Memory Variable Storage
Canvas CanvasCanvas ScalerGraphic Raycaster
Line Presenter Line PresenterCanvas Group
Options Presenter Options PresenterCanvas Group
Line Advancer Line Advancer
The Dialogue System prefab. Its Dialogue Runner’s Variable Storage setting points at the In Memory Variable Storage on the same GameObject.

To use a different Variable Storage, add it as a component to a GameObject in your scene, and drag that GameObject into the Dialogue Runner’s Variable Storage field.

PLACEHOLDER — Screenshot needed
A Dialogue Runner in the Inspector, with its Variable Storage field set to the Dialogue System’s In Memory Variable Storage.

When something asks for a variable’s value, the Variable Storage looks in three places, in this order:

  1. the value it’s storing, if the variable has been set,
  2. the variable’s expression, if it’s a smart variable,
  3. the default value from the variable’s <<declare>> in Yarn.

Reading a variable before it’s been set gives you its declared default.

Reading and changing variables from C#

Get the Variable Storage from the Dialogue Runner, and use the variable’s name, including the $:

var storage = dialogueRunner.VariableStorage;

storage.SetValue("$visitor_name", "Sam");

storage.TryGetValue<float>("$trust", out var trust);
storage.TryGetValue<bool>("$lamp_fixed", out var lampFixed);
storage.TryGetValue<string>("$visitor_name", out var visitor);
Line 1 The Dialogue Runner’s Variable Storage.
Line 3 Set a variable. Use its whole name, including the $.
Lines 5–7 Read variables as a float, bool or string. A variable that hasn’t been set gives its default from <<declare>>.
Reading and changing Yarn variables.

TryGetValue<T>() returns true if it found the variable as that type, and puts the value in its out parameter. If the variable doesn’t exist, or has a different type, it returns false. Contains() tells you whether a variable has been set in the Variable Storage. It returns false for a variable that’s still at its declared default.

Yarn variables can only hold numbers, text, and true or false, so SetValue() takes a float, a string or a bool. Yarn numbers are kept as float, so read them with TryGetValue<float>(). The In Memory Variable Storage throws an exception if a variable’s name doesn’t start with $.

To give a variable a value the dialogue can use from its first line, set it before you start the dialogue. Start Automatically waits one frame before it starts, so a value you set in your own Start() method is ready in time.

Reacting when a variable changes

To keep something on screen up to date, like a counter, call AddChangeListener<T>() with the variable’s name and a method. Your method gets the new value, as that type, whenever the variable changes. That includes changes made by <<set>> in Yarn and changes made by your own code.

IDisposable trustListener;

void Start()
{
    var storage = dialogueRunner.VariableStorage;
    trustListener = storage.AddChangeListener<float>("$trust", OnTrustChanged);
    storage.TryGetValue<float>("$trust", out var trust);
    OnTrustChanged(trust);
}

void OnDestroy()
{
    trustListener?.Dispose();
}

void OnTrustChanged(float trust)
{
    trustLabel.text = $"Trust: {trust}";
}
Line 1 AddChangeListener() returns an IDisposable. Keep it, so you can stop listening.
Line 6 Call OnTrustChanged with the new value whenever $trust changes. The type in angle brackets has to match the variable’s type, so a number is a float.
Lines 7–8 Show the starting value. Adding a listener doesn’t call your method until the value changes.
Lines 11–14 Stop listening when this component is destroyed.
Lines 16–19 Gets the new value as a float.
Updating a label whenever $trust changes.

If the type in angle brackets doesn’t match the variable’s type, AddChangeListener() throws an exception.

To hear about every variable, call AddChangeListener() with a method that takes the variable’s name and its new value: storage.AddChangeListener((name, value) => Debug.Log($"{name} is now {value}"));. It also returns an IDisposable.

Smart variables don’t change by being set, so you can’t listen to them. AddChangeListener() throws an exception if you try.

Saving and loading

The Dialogue Runner can save every variable in its Variable Storage to a file in Unity’s persistent data path, and load them back:

dialogueRunner.SaveStateToPersistentStorage("dialogue_save.json");
dialogueRunner.LoadStateFromPersistentStorage("dialogue_save.json");
Saving and loading Yarn variables.

Both return true if they worked. Loading replaces all the current variables with the ones in the file.

Only the variables are saved, not where the dialogue is up to. To pick a conversation up again, save the name of the node you want to start from, and pass it to StartDialogue() after loading.

To save Yarn variables as part of your game’s own save file, use GetAllVariables(), which returns every variable that’s been set, in three dictionaries: one for numbers, one for text and one for true or false. Put them back with SetAllVariables():

var (floats, strings, bools) = storage.GetAllVariables();

storage.SetAllVariables(floats, strings, bools);
Getting every variable, and putting them back.

SetAllVariables() clears the Variable Storage first. To keep the variables that are already there, pass false as a fourth argument. To remove every variable, call Clear().

Seeing variables while you test

The In Memory Variable Storage can show you every variable while the game runs.

In the Editor, select the GameObject with the In Memory Variable Storage, and unfold Debug Variables in the Inspector. In Play Mode, it lists every variable, like $trust = 2 (Single).

PLACEHOLDER — Screenshot needed
An In Memory Variable Storage in the Inspector during Play Mode, with Debug Variables unfolded, listing $visitor_name, $trust and $lamp_fixed with their values and types.

To show the same list in the game:

  1. Add a TextMeshPro text object to your scene, such as GameObjectUIText - TextMeshPro.
  2. Drag it into the In Memory Variable Storage’s Debug Text View field.

Both only list variables that have been set, not ones still at their declared default.

Typed access with a generated class

Yarn Spinner can generate a Variable Storage class with a property for each variable, so you don’t have to type variable names as text. $trust becomes Trust, $visitor_name becomes VisitorName, and each property has the matching C# type. See Generating a typed variables class.

Add the generated class to a GameObject in your scene, and set it as the Dialogue Runner’s Variable Storage. By default it’s based on the In Memory Variable Storage, so it works the same way. Then:

var variables = (YarnVariables)dialogueRunner.VariableStorage;
variables.VisitorName = "Sam";
if (variables.Trust >= 2)
{
    OpenTheDoor();
}
Using the generated YarnVariables class.

Writing your own Variable Storage

To keep Yarn variables somewhere else, write a class that extends VariableStorageBehaviour. This one keeps them in a dictionary of save data, so they’re saved and loaded along with the rest of your game:

using System.Collections.Generic;
using Yarn;
using Yarn.Unity;

public class SaveDataVariableStorage : VariableStorageBehaviour
{
    public Dictionary<string, object> saveData = new Dictionary<string, object>();

    public override void SetValue(string variableName, string stringValue) => Store(variableName, stringValue);
    public override void SetValue(string variableName, float floatValue) => Store(variableName, floatValue);
    public override void SetValue(string variableName, bool boolValue) => Store(variableName, boolValue);

    void Store<T>(string variableName, T value)
    {
        saveData[variableName] = value;
        NotifyVariableChanged(variableName, value);
    }

    public override bool TryGetValue<T>(string variableName, out T result)
    {
        switch (GetVariableKind(variableName))
        {
            case VariableKind.Stored:
                if (saveData.TryGetValue(variableName, out var value) && value is T typedValue)
                {
                    result = typedValue;
                    return true;
                }
                return Program.TryGetInitialValue(variableName, out result);
            case VariableKind.Smart:
                return SmartVariableEvaluator.TryGetSmartVariable(variableName, out result);
            default:
                result = default;
                return false;
        }
    }

    public override void Clear() => saveData.Clear();

    public override bool Contains(string variableName) => saveData.ContainsKey(variableName);

    public override (Dictionary<string, float> FloatVariables, Dictionary<string, string> StringVariables, Dictionary<string, bool> BoolVariables) GetAllVariables()
    {
        var floats = new Dictionary<string, float>();
        var strings = new Dictionary<string, string>();
        var bools = new Dictionary<string, bool>();
        foreach (var pair in saveData)
        {
            if (pair.Value is float f) floats[pair.Key] = f;
            if (pair.Value is string s) strings[pair.Key] = s;
            if (pair.Value is bool b) bools[pair.Key] = b;
        }
        return (floats, strings, bools);
    }

    public override void SetAllVariables(Dictionary<string, float> floats, Dictionary<string, string> strings, Dictionary<string, bool> bools, bool clear = true)
    {
        if (clear) Clear();
        foreach (var pair in floats) SetValue(pair.Key, pair.Value);
        foreach (var pair in strings) SetValue(pair.Key, pair.Value);
        foreach (var pair in bools) SetValue(pair.Key, pair.Value);
    }
}
Line 5 Extend VariableStorageBehaviour. It’s a MonoBehaviour, so you add it to a GameObject.
Line 7 Where the variables are kept. Here, a dictionary that the rest of your save system can write to disk.
Lines 9–11 SetValue is called whenever a variable is set, from Yarn or from your code. There’s one for each type a Yarn variable can have.
Lines 13–17 Store the value, then call NotifyVariableChanged. Without it, change listeners never hear about the new value.
Lines 19–36 TryGetValue returns a variable’s value. GetVariableKind says whether it’s a stored variable or a smart variable. A stored variable that hasn’t been set falls back to its declared default from Program, and a smart variable is worked out by SmartVariableEvaluator. The Dialogue Runner sets both of these for you.
Line 38 Clear removes every variable.
Line 40 Contains says whether a variable has been set.
Lines 42–54 GetAllVariables returns every variable that’s been set, sorted by type. Saving uses it.
Lines 56–62 SetAllVariables puts variables back, clearing the old ones first unless clear is false. Loading uses it.
A Variable Storage that keeps Yarn variables in the game’s save data.

To use it, add SaveDataVariableStorage to a GameObject in your scene and set it as the Dialogue Runner’s Variable Storage.

A Variable Storage needs those eight methods: the three SetValue methods, TryGetValue, Clear, Contains, GetAllVariables and SetAllVariables. Change listeners, and the Dialogue Runner’s saving and loading, are built on them.

An example

Before the dialogue starts, this script sets $visitor_name to “Sam”. Rosa greets the player by name, and the player’s choice raises $trust, which a label in the corner shows. Setting $lamp_fixed lights the lamp, and the variables are saved when the dialogue ends.

title: VariablesDemo
---
<<declare $trust = 0>>
<<declare $lamp_fixed = false>>
<<declare $visitor_name = "stranger">>
Rosa: Hello, {$visitor_name}. What brings you here?
-> I can help with the lamp.
    <<set $trust to $trust + 2>>
    Rosa: That's kind of you.
-> Just looking around.
    <<set $trust to $trust + 1>>
    Rosa: Mind the stairs, then.
Rosa: Let me get the lamp going.
<<set $lamp_fixed to true>>
<<if $trust >= 2>>
    Rosa: Thanks for the help, {$visitor_name}.
<<else>>
    Rosa: See you around, {$visitor_name}.
<<endif>>
===
The Yarn Script.
using System;
using TMPro;
using UnityEngine;
using Yarn.Unity;

public class VariablesDemo : MonoBehaviour
{
    [SerializeField] DialogueRunner dialogueRunner;
    [SerializeField] TextMeshProUGUI trustLabel;
    [SerializeField] TextMeshProUGUI savedLabel;
    [SerializeField] Light lampLight;

    IDisposable trustListener;
    IDisposable lampListener;

    void Start()
    {
        var storage = dialogueRunner.VariableStorage;

        storage.SetValue("$visitor_name", "Sam");

        trustListener = storage.AddChangeListener<float>("$trust", OnTrustChanged);
        storage.TryGetValue<float>("$trust", out var trust);
        OnTrustChanged(trust);

        lampListener = storage.AddChangeListener<bool>("$lamp_fixed", OnLampFixedChanged);

        dialogueRunner.onDialogueComplete.AddListener(OnDialogueComplete);
        dialogueRunner.StartDialogue("VariablesDemo");
    }

    void OnDestroy()
    {
        trustListener?.Dispose();
        lampListener?.Dispose();
    }

    void OnTrustChanged(float trust)
    {
        trustLabel.text = $"Trust: {trust}";
    }

    void OnLampFixedChanged(bool lampFixed)
    {
        lampLight.enabled = lampFixed;
    }

    void OnDialogueComplete()
    {
        if (dialogueRunner.SaveStateToPersistentStorage("variables_demo.json"))
        {
            savedLabel.text = "Saved to " + Application.persistentDataPath;
        }
    }
}
Lines 8–11 Parts of the scene, set in the Inspector.
Lines 13–14 The two change listeners, kept so they can be stopped.
Line 18 The Dialogue Runner’s Variable Storage.
Line 20 Set $visitor_name before the dialogue starts, so Rosa’s first line uses it.
Lines 22–24 Keep the trust counter up to date, and show the starting value.
Line 26 Light the lamp when $lamp_fixed changes.
Lines 28–29 Save when the dialogue ends, then start it.
Lines 32–36 Stop both listeners when this component is destroyed.
Lines 38–41 Gets the new value of $trust.
Lines 43–46 Gets the new value of $lamp_fixed, and turns the lamp’s light on when it’s true.
Lines 48–54 Save every variable to the persistent data path, and show where.
VariablesDemo.cs. The Dialogue Runner’s Start Automatically is off, so this script sets $visitor_name first and then starts the dialogue.
Next step Smart Variables Variables that are worked out from other variables each time they're read.