Yarn Spinner Extras Part 2: Return Values

Posted On: 2018-10-22

By Mark

Last week I wrote about Yarn and Yarn Spinner,which are handy tools for creating interactive narratives for Unity. I covered how to work around some limitations with how variables are used in the framework. This week, I intend to cover Yarn Commands, and the workaround I am using to simulate return values. If you haven't already done so, you should definitely read last week's post, since this post uses the string interpolation that I covered there.

Yarn Commands are pieces of code that you write that the Yarn engine is able to call. They are typically denoted with the YarnCommand attribute and assigned a particular commandName: [YarnCommand(commandName)]. Yarn Commands allow for lots of simple, extendible features, such as moving characters or swapping sprites in the middle of a dialogue. They can be used for advanced things as well, but they are somewhat limited because they cannot return any values to the dialogue. For example, consider this snippet from a character introduction, where the speaker has trouble if the player's name is too long:

<<if <<length stringUtilities $player_name>> > 10 >>
  <<setSprite speakerFace nervous>>
  Nice to meet you, <<substring stringUtilities $player_name 0 10>>... uh... sorry...
<<else>>
  <<setSprite speakerFace happy>>
  Nice to meet you, $player_name!
<<endif>>

This example dialog is a little busy, but it showcases several different uses of commands. setSprite is a simple command, and it works without any changes: it calls the setSprite Yarn Command on the speakerFace object to do some action of our design (presumably setting the sprite for the character's expression.) By contrast, the length command wouldn't work as written, since it performs some action of our design (gets the length of the player name) and then returns a number that is immediately used in a calculation (whether or not the name is more than 10). substring similarly won't work, since it returns a value (the first 10 letters of the player name) - though in this case the return value is intended to be displayed in the the text.

Fortunately, all of this can be fixed with a simple workaround, but before I dive into the details, I have two asides to mention:

  1. Firstly, the examples here are assuming that various Yarn Commands are setup correctly and attached to objects with the listed names (stringUtilities and speakerFace). The string utilities are just wrappers around built-in methods on strings: Length and Substring, so the code isn't worth including here.setSprite is used extensively in Yarn Spinner's Complex Dialog Examples so I won't go over the implementation details for that here either.
  2. Secondly, the solution I am suggesting below is a workaround. If the YarnSpinner implementation is updated to support return values from commands (or if you implement your custom commands as extensions to the built-in Yarn commands) then that might be a better alternative. The solution below is what I've been using in my current prototype, and it has served me well for that purpose. (That being said, I intend to explore extending the built-in commands at a later date. I haven't used them myself yet, but I am hoping they can help avoid some of the pitfalls I describe below.)

Now that those are out of the way, we can get into the details of how this workaround actually works:

Rather than trying to use return values directly, we can instead designate a variable as always holding the return value of whatever was last called. This variable can be set in the Yarn Command, and then used at a later time in the dialog. (If you're familiar with assembly, you probably recognize this as the same pattern for how the eax register is typically used - and are probably also familiar with a lot of the pitfalls it entails.) Using a single variable has the advantage that it has a low likelihood of having the same name as an existing variable and it is easier to remember than a bunch of different variables. For these examples (and in my own personal work) I will be using $RetVal as this variable, since it is self-describing and easy (for me at least) to remember. (If you are working in a team, make sure that everyone agrees on what variable to use - otherwise issues can creep in.)

Using this approach, unfortunately, requires that the dialog is aware of this convention, so it can become a bit more tedious and/or cluttered. For example, the first line

<<if <<length stringUtilities $player_name>> > 10 >>
would instead become two lines
<<length stringUtilities $player_name>>
<<if $RetVal > 10 >>
One line calls the command, and the second line tests to see if the result is greater than ten. If you need to use multiple return values, this might become even more cluttered, as you'd then need to store intermediary variables. For example, if the code for the speaker defined the max length name they could remember, the dialog code would then become:
<<maxMemorableNameLength speaker>>
<<set $max_length to $RetVal>>
<<length stringUtilities $player_name>>
<<if $RetVal > $max_length >>

In addition to making the dialog harder to read, there are some other, far more dangerous pitfalls to watch out for. The main issue with this approach is: if a command doesn't set the $RetVal, but the dialog tries to use it, it will instead get whatever the last command happened to set it to. (This can happen if the Yarn Command normally doesn't set it, or if the object with the Yarn Command is missing or disabled.) This is an issue that can occur with any system that uses static variables (which is what we're basically doing with $RetVal), and in any such situation it can produce behavior way outside the expected possibilities (such as sequence breaking or getting stuck.) It's also worth mentioning that you definitely should not use this for anything senstive, like security or user authentication (I can't imagine anyone would but, just in case.) Similarly, if you create any Yarn Commands that interact with the operating system (such as writing files, executing programs, or accessing the internet) it is critically important to properly validate any input parameters that they use (You should always do this, even if you don't use $RetVal, but I am mentioning it here since using $RetVal as an unvalidated input in one such command could produce genuinely dangerous bugs.)

A second possible issue is related to data types: Yarn seems pretty flexible with data types (for example, <<if 1>> is considered true), so you may get unexpected behavior if the data type the command returns differs from the data type the dialog author was expecting. Yarn's behavior is generally pretty reasonable here, but like any other weakly-typed language, there is the possiblity for some pretty unexpected stuff (for example if a Command sets $RetVal to a string, but then the dialog tries to do arithmetic operations with it.)

Now that I've covered both how to do it and what pitfalls to watch out for, it's time to circle back to the original example. The first change to make is to make the commands set the $RetVal (this can be done using the SetValue method on the variable store.) After that, it's time to update the dialog to use the variable:

<<length stringUtilities $player_name>>
<<if $RetVal > 10 >>
  <<setSprite speakerFace nervous>>
  <<substring stringUtilities $player_name 0 10>>
  Nice to meet you, $RetVal... uh... sorry...
<<else>>
  <<setSprite speakerFace happy>>
  Nice to meet you, $player_name!
<<endif>>

That's it! Hope this was useful, and if you'd like to see more about Yarn or other programming topics, please let me know.

Thank you,
Mark