Yarn Spinner Extras Part 1: Variables in Text

Posted On: 2018-10-15

By Mark

While working on prototyping a new way to interact with the narrative, I've been working with the delightfully accessible Yarn dialog editor, and its corresponding tool set for using it in the Unity game engine: Yarn Spinner. Yarn is proving to be an excellent way to work with dialog: the editor is simple, intuitive, and clear, and the data can be stored in any one of several different common human-readable formats (json is my personal favorite.) I wrote a couple of years ago about the importance of the designer's user experience and I am pleased to say that Yarn and Yarn Spinner meet every one of my criteria for a good user experience when working with data. While working with it, I have, unfortunately, found several limitations that I needed to work around, but I thought I should document them so that others can benefit from it as well.

The first obstacle was interpolating variables. Consider the following example:

<< set $favorite_food to pizza >> 
[...]
Mother: I've cooked your favorite food, $favorite_food.
Given that code, I expected it to interpolate the $favorite_food, to produce:
Mother: I've cooked your favorite food, pizza.
but, unfortunately, it displays the text exactly as-is:
Mother: I've cooked your favorite food, $favorite_food.
This limitation is documented, along with a workaround, in an issue on their github page. Although there is existing code for a workaround I thought I should take the time to dive into the details of the workaround, as well as an alternate implementation.

The basic idea is that, even though Yarn doesn't interpolate for us, we can do the interpolation ourselves, as it is a relatively simple string replacement. As such, the first piece of code to write is a method to do the text replacement. I personally recommend adding this method to the VariableStorage itself, since we will need to use it in several different places throughout our project (more on this later). The actual code the for the implementation can vary, but I personally like using Regular Expressions (Regex)for this kind of work. With a regular expression, you can succinctly describe what sequence of characters you are looking for, and then tell the expression to find all the matches and perform the text replacement. Here is what I'm using:

//Regular expression will find any variable as defined by the following: starts with "$" and includes only the letters A-Z (upper or lower case)
private Regex VariableNameExpression = new Regex("\$[a-zA-Z]+");
public string SubstituteVariables(string sourceText)
{
    return VariableNameExpression.Replace(sourceText, GetVariableFromMatch);
}
private string GetVariableFromMatch(Match variableNameMatch)
{
    return GetValue(variableNameMatch.Value).AsString;
}

The nice thing about this structure is that we can easily add code to GetVariableFromMatch if, for example, we want to use variables to lookup localized strings instead of using them directly.

Once we have a working variable substitution, we can call it from any other script that we want. The first (and most obvious) place to use this is the RunLine method in the DialogUI ( such as the ExampleDialogUI.) This class will need a new field to reference to your VariableStorage (be sure to set it in the inspector later), and then you can use the SubstituteVariables method on the line.text. Store the result in a variable, and you can use it anywhere that you would normally use line.text:

public override IEnumerator RunLine (Yarn.Line line)
{
    [...]
    var textToDisplay = variableStorage.SubstituteVariables(line.text);
    foreach (char c in textToDisplay) {
    [...]
}

Once you've done this, you should be able to run our example again, and this time get:

Mother: I've cooked your favorite food, pizza.

Changing RunLine is great, but we should also change a few other places. RunOptions (also in the DialogUI) is responsible for rendering the options a player may choose from. We can use SubstituteVariables there as well, to allow us to use variables in the text for options, like [I'd like some $favorite_food|GetFood] (Note that we still cannot use the variable inside the target of the option, such as [Get Some Food |$favorite_food]. It's probably possible to do if by modifying the code for choice resolution inside the YarnSpinner's engine, but doing so would leave the Yarn Editor unable to visualize the dialog structure, so I don't intend to try unless I absolutely have to.) The changes to the code for RunOptions are the same pattern we used for RunLine (though the example RunOptions actually has a temporary variable, optionString, so we can re-use that):

public override IEnumerator RunOptions (Yarn.Options optionsCollection, Yarn.OptionChooser optionChooser)
{
    [...]
    foreach (var optionString in optionsCollection.options) {
        optionString = variableStorage.SubstituteVariables(optionString);
        [...]
}

Finally, if you're using Yarn Commands, you will want to remember to do variable substitutions in those as well. This might be a bit more tedious (since you have to do it for every command you create) but it's still the same pattern as all the previous example. So, for example, <<setSprite Food $favorite_food>> could allow us to set the sprite for the player's favorite food (just be sure to implement reasonable default behavior for when the variable is set to something that you didn't expect - because one day it will, and you don't want that player to experience a crash because of it.)

[YarnCommand("SetSprite")]
public void SetSprite(string spriteName)
{
    spriteName = variableStorage.SubstituteVariables(spriteName);
    //Actual implementation omitted since it is out of scope for this article
    [...]
}

That should cover all the places you might want to do this kind of variable substitutions. (Though, if you do have another place that needs it, you'll just need to apply this same pattern and it should work just fine.) That being said, this is only one of two valuable workarounds. Since it's getting a bit long, so I will make this one a two-parter. Come back next week for a workaround that lets us simulate return values from Yarn Commands.

Thank you,
Mark