Paris Buttfield-Addison Paris Buttfield-Addison

Yarn Spinner 3.0: What To Expect

Hey, everyone!

It’s been an incredible two years (nearly) since the release of Yarn Spinner 2.0, and we’ve been thrilled to see the tool develop into something that’s used to power thousands of incredible games.

We’ve been working away at new features for Yarn Spinner 3.0, and we wanted to give you a preview of what’s coming. We don’t currently have a date for Yarn Spinner 3.0, but we currently expect to ship it in the first half of the new year.

💡 All examples in this post are work-in-progress. The features may or may not ship at all, and features that do ship may not have the same syntax or the exact same behaviour as what’s described here.

To show your support in the mean time, send people to the Patreon, and buy Yarn Spinner, plus the new Add-Ons, at Itch!

Supporting sophisticated narrative structures

🗣 We want your feedback on these features! Let us know in the comments or in the Yarn Spinner Discord.

Storylets

A storylet is a ‘piece’ of a story that can be embedded into the larger narrative, and that the game’s systems decide which one to use depending on the current situation.

Storylets are great for any situation where you have a range of possible things that the player could encounter, and you want the game to present the ‘best’ one.

In Yarn Spinner, storylets will take the form of a node that has a condition on it:

title: SpeakToGuard
when: $guard_friendly == true
---
// The guard likes us
Guard: Halt, traveller!
Player: Why, hello there!
Guard: Ah, my friend! You may pass.
===

title: SpeakToGuard
when: $guard_friendly == false
---
// The guard doesn't like us
Guard: Halt, scum!
Guard: None shall pass this point!
===

As you can see, both of these nodes have the name SpeakToGuard. When the SpeakToGuard node is run, then we’ll either see friendly lines, or unfriendly lines, depending on the value of the $guard_friendly variable.

In the previous example, there was always exactly one possible choice of node to run - either the guard is friendly, or she’s not. What should happen if there are multiple possible nodes that could run?

title: SpeakToGuard
when: $guard_friendly == true
---
// The guard likes us
Guard: Halt, traveller!
Player: Why, hello there!
Guard: Ah, my friend! You may pass.
===

title: SpeakToGuard
when: $guard_friendly == true
when: $helped_king == true
---
// The guard likes us, AND we helped the King
// (both conditions must be true)
Guard: Greetings, traveller!
Guard: A friend of the king is always welcome!
Guard: You may pass!
===

title: SpeakToGuard
when: $guard_friendly == false
---
// The guard doesn't like us
Guard: Halt, scum!
Guard: None shall pass this point!
===

Imagine that we speak to the guard, and the guard is friendly, and we helped the king. In this situation, the first two nodes could run. Which one should the player see? When more than one node can run, the game’s saliency strategy is used to answer this question.

A saliency strategy is a fancy name for ‘the way to decide which piece of content should appear’. Here are some example saliency strategies:

  • Choose the first one, every time.

  • Choose randomly.

  • Choose whichever one we’ve seen the least (and if that doesn’t narrow it down to one, choose randomly.)

  • Choose the first item that we haven’t seen yet. If we’ve already seen them all, choose randomly.

  • Choose whichever one has the most specific condition. If that doesn’t narrow it down to one, choose the one we’ve seen the least often. If that still doesn’t narrow it down to one, choose randomly.

As you can see, there’s several ways to answer the question “which content should the player see right now”. There’s no single answer that applies to all games, and even a single game might have different needs over the course of the game.

To solve this, Yarn Spinner 3.0 will allow you to create your own custom saliency strategy that’s specific to the needs of your game, as well as letting you choose from several common built-in strategies. You’ll also be able to change your saliency strategy at any time.

Of course, having to write an entire new node might be a little too heavy if you just want to choose between a bunch of single lines. That’s why we’re also adding a lightweight version of storylets, called line groups.

Line Groups

A line group is like an option group, but instead of the player choosing what to say, the system does.

A line group is made of items that begin with =>. Each item is a possible line that could appear, and only one of the items will actually run.

=> Guard: Halt!
=> Guard: Stop right there!
=> Guard: No entry!
=> Guard: Stop right there, you criminal scum! <<if $is_criminal>>
=> Guard: Halt, you brigand! <<if $is_criminal>>
=> Guard: Thief! Stop right there! <<if $is_criminal>>

When the dialogue reaches the line group, any lines that have a condition get checked. Any line whose condition equals ‘false’ is discarded, and the rest are then sent to the saliency strategy, just like storylets. In fact, line groups work very much like storylets, embedded directly into your node.

Line groups aren’t just for single lines, either! Just like how you can nest lines inside options, you can also nest any other content inside an item in a line group. If that item is selected, then its contents are run:

=> Guard: Another day, guarding the bridge. 
    Guard: Wish I'd get a transfer. // runs only if the above line runs
=> Guard: Quiet on the road today.

We think that storylets and line groups are going to be a great way to create rich, dynamic and reactive conversations, and we can’t wait to see what you make with them!

Smart Variables

A smart variable is a new kind of variable in Yarn Spinner that determines its value at runtime, rather than setting and retrieving a value that’s stored in memory. Smart variables let you take a complex expression, and re-use it across your project.

To create a smart variable, declare it using the declare statement and provide an expression, rather than a single value:

// Declare a new variable, $player_can_afford_pie, which
// is a boolean value that is 'true' when the player has
// more than 10 money

<<declare $player_can_afford_pie = $player_money > 10>>

Once a smart variable has been declared, you can access it just like any other variable.

<<if $player_can_afford_pie>>
    Player: One pie, please.
    PieMaker: Certainly!
<<endif>>

Smart variables are read-only - that is, you can’t change the value of the smart variable directly. The compiler will produce an error if you try to do this.

Smart variables can be used in Yarn scripts, as well as in your game’s code! They’re treated just like other variables:

VariableStorage variableStorage = /* your game's variable storage */

variableStorage.TryGetValue("$player_can_afford_pie", out bool canAffordPie);

Smart variables can depend on other smart variables, too!

Smart variables are an incredible way to simplify your dialogue and make it easier for other members of your team to write dialogue that interacts with complex game state!

Enum Support

An enum is a type of variable whose value is equal to one of a certain set of values. Enums are great for when you want a variable to represent a state in your game:

// Create a new enum called Food.
<<enum Food>>
  <<case Apple>>
  <<case Orange>>
  <<case Pear>>
<<endenum>>

// Declare a new variable with the default value Food.Apple
<<declare $favouriteFood = Food.Apple>>

You can use an enum value just like any other value:

// You can set $favouriteFood to be any of the Food types:
<<set $favouriteFood to Food.Orange>>

// You can use enums in if statements, like any other type of value:
<<if $favouriteFood == Food.Apple>>
  I love apples!
<<endif>>

// You can even skip the name of the enum if Yarn Spinner can 
// figure it out from context!
<<set $favouriteFood = .Orange>>

Enums are a great way to keep your dialogue tidy, and to keep ‘magic numbers’ out of your code!

‘Once’ support

In Yarn Spinner 3.0, you’ll be able to mark content as something that should only ever be seen once:

<<once>>
  Guard: Who are you?
  Guard: I've never seen you before.
  Player: I'm new!
<<else>>
	Guard: Ah, it's you.
<<endonce>>

The very first time a once keyword is reached, it’s treated as though it’s the value true. On all other occasions, it’s treated as false. Behind the scenes, Yarn Spinner will create a new variable and keep it updated for you!

The once keyword can also be used in line groups, in options, and on individual lines:

=> Guard: I once was an adventurer like you, but then I took an arrow 
to the knee. <<once>> // Only show this line one time!
=> Guard: Greetings.

// Only let the player ask the character's name once
-> What's your name? <<once>>
-> I should go.

// The player will only introduce themselves once
Player: Hi! I'm the player! <<once>>
Player: I'm here to save the day!

Jump And Return

In Yarn Spinner, you can jump to another node by using the <<jump>> instruction. This works like a goto instruction in other languages: control leaves the current node, and immediately moves to the new node. However, there’s no way to return to the same point you jump'ed from - in Yarn Spinner, you can only jump to the start of a node.

Jump and return allows you to come back to the point that you jumped from:

title: Guard
---
Guard: Have I told you my backstory?
-> Yes.
-> No?
    <<jump_and_return Guard_Backstory>>
		// Note: 'jump_and_return' is not the final syntax
Guard: Anyway, move along.
===
title: Guard_Backstory
---
Guard: It all started when I was a new recruit.
// ... excessively long backstory goes here ...
Guard: Want to hear more?
-> Yes.
-> No, thanks.
    // Return early
    <<return>>
Guard: Great! After I graduated...
// ... more backstory ...

// The node will automatically return when it reaches the end
===

💡 The syntax and keywords for the jump-and-return feature have not yet been finalised. We’re seeking community feedback on how the syntax should look. Let us know in the Yarn Spinner Discord!

When you jump-and-return, Yarn Spinner remembers your place, and then jumps to another node. When the end of that node is reached, or a <<return>> instruction is reached, Yarn Spinner jumps back to where you originally jumped from.

Jump-and-return is great for when you want to take the player to a long piece of content, and then return back to where they came from. It’s especially great if that content is optional - that way, you can keep the critical path tidy, and not have to keep long stretches of optional content in a single node. It’s also really good for sharing content between multiple nodes, because you can jump-and-return to a node from multiple different places.

Integrate with game engines better

In Yarn Spinner 3.0, we’re adding several new features that make it easier for game programmers to work with their narrative content.

New type system

The type system is a core part of the Yarn Spinner compiler. It’s responsible for making sure that any expressions and variables are dealt with in a consistent way, and that no logical errors (like ‘what is 3 divided by “fork”’) can happen.

In order to support new features like smart variables and enums, we’ve upgraded the type system to a new model. It’s more powerful, it’s faster, and it gives much better error messages.

Shadow Lines

When you’re building a complex game, you’ll often find yourself wanting to reuse lines of content. In Yarn Spinner, each line is unique, and that means that each asset - like voice over clips, animation data, and so on - also needs to be unique.

Shadow lines allow you to mark a line as a re-used copy (a “shadow”) of an existing line. It has the exact same text and assets, but it just appears in more than one place in your dialogue.

Consider this example:

title: Lounge
---
Player: Hello, barkeep!  #line:1
Barkeep: Hi there, how can I help? #line:2
Player: I should go. #line:3
===

title: Kitchen
---
Player: Greetings, chef! #line:4
Chef: What are you doing back here? #line:5
Player: I should go. #line:6
===

In this example, the lines line:3 and line:6 are the same, and to save on storage and having to re-record the same line, we want to have the same audio file for both. Prior to Yarn Spinner 3.0, you can’t really do this - the best you could do would be to either duplicate the audio file (wasting space), or to have a separate node just to contain the shared line.

Shadow lines solve this problem by letting you make one line the ‘shadow’ of another.

title: Lounge
---
Player: Hello, barkeep!  #line:1
Barkeep: Hi there, how can I help? #line:2
Player: I should go. #line:3
===

title: Kitchen
---
Player: Greetings, chef! #line:4
Chef: What are you doing back here? #line:5
Player: I should go. #shadow:3
===

When the game reaches the shadow:3 line, the localised line:3 will run. Only a single “I should go” will appear in the string table, and only one voice-over recording for the line needs to be stored. This reduces the number of items that need to be translated, and reduces game storage costs.

Shadow lines are like an instruction to Yarn Spinner to re-use an existing line’s text and assets. They use the same localisation pathway, and make managing a complex collection of dialogue easier.

“On variable change” event support

In Yarn Spinner 3.0, you can register to run code any time your variables change. You can also register to run code when a specific variable changes. For example, if you need to update a UI element whenever a Yarn variable changes, you’ll be able to tell Yarn Spinner to run your code when that variable is modified.

Async Dialogue Views

The current way that you build custom dialogue views is complex, and requires complexity both in the Dialogue Runner and in your code. This can lead to difficult-to-debug setups, and it makes it harder than it needs to be to customise Yarn Spinner to suit your game’s needs.

In Yarn Spinner 3.0, we’re moving to a new model for dialogue views, based on the C# async-await feature. It leads to a much simpler, much smaller, and much easier to understand way of building views.

Here’s a preview of what handling a line looks like:

public override async Task RunLineAsync(LocalizedLine line, CancellationToken token)
{
    var textToDisplay = line.Text.Text;

    // lineText is a TextMeshPro component; show it on the screen
    lineText.text = textToDisplay;
    
    // Wait for either of:
    // - the user has pressed a key
    // - the line has been cancelled
    while ((Input.anyKey || token.IsCancellationRequested) == false) {
        Task.Yield();
    }

    // Clean up
    lineText.text = "";
}

As you can see, it’s really simple to create a line view. Because cancellation tokens are easy to work with, it’s very easy to make a view that can handle a line being interrupted.

Yarn Spinner 3.0 will make use of the best available async support in your project.

  • If you have UniTask installed, Yarn Spinner will use that.

  • Otherwise, if your project is using Unity 2023, Yarn Spinner will use Unity’s Awaitables support.

  • Otherwise, Yarn Spinner will use the built-in System.Tasks support.

If you don’t want to write async methods, you can just work with the Task objects directly. It’s fast, lightweight, GC-friendly, easy to work with, and just better all-around.

Codegen variable storage interface

Yarn Spinner 3.0 will automatically generate a C# interface for your variable storage, based on the variable names you have.

One of the most common causes of problems is typos. A compiler can catch lots of these problems because it knows what words are and aren’t allowed, but it can’t catch problems caused by typos inside strings, and that’s currently the only way to work with Yarn variables from inside your C# code.

To solve this problem, Yarn Spinner will automatically generate code that provides C# properties for your variables:

// In Yarn:
<<declare $goldAmount as number>>
<<declare $playerName as string>>
<<declare $isDoorUnlocked as boolean>

// In C#:

var myStorage = GetComponent<MyGameVariableStorage>();

myStorage.SetValue("$goldAmount", 500); // old way
myStorage.goldAmount = 500; // new way

// Old way:
if (myStorage.TryGetValue("$playerName", string playerName)) {
   // Work with $playerName   
} else {
   // Couldn't get $playerName or its default value!
}

// New way:
string playerName = myStorage.playerName;

This new way of working with variables means that if you make a typo, or rename a variable, you’ll automatically get errors to alert you that you need to update your game’s code.

Type-checked commands

In Yarn Spinner, a command is how you tell the game to do something that isn’t “showing lines or options to the player”. Internally, we treat commands much like lines - they’re just strings, and we send them to the game. This means that any mistakes inside the command can’t be caught until runtime.

A big priority for us is to catch as many possible errors as early as we can, and that’s why we’re improving our ability to detect errors in commands, including having the wrong number of parameters, or providing the wrong type of parameters.

Parser errors for markup

Much like commands, markup is handled at run-time, which means that people can’t easily see if they’ve made a mistake while writing their script. In Yarn Spinner 3.0, we’ll show you error messages if you’re using markup that contains syntax errors.

Yarn Spinner for Unreal

For a while now, we’ve had an alpha version of Yarn Spinner for Unreal available on GitHub. We’ve been continuing to develop it, and it’s very close to being ready for beta access.

Our remaining steps before we’re ready to release it as a beta are:

  • Implementing the complete standard library of Yarn Spinner built-in functions and commands

  • Writing the documentation and preparing sample projects

  • Bug-fixes

In other words, it’s super close! As we mentioned, it’s not yet ready for a beta release, but if you’re comfortable jumping in without documentation and with bugs, take a look at the current development branch and have a play!

Supporting people not using a game engine at all

Not every user of Yarn Spinner is using it in a game engine like Unity, Unreal or Godot. One of our most popular ways to use Yarn Spinner is Try Yarn Spinner, our web-based playground for testing out Yarn Spinner features.

To help support people who want to use Yarn Spinner in places that aren’t popular game engines, we’ll be releasing a JavaScript version of Yarn Spinner’s runtime, which will be an NPM package you can use in any JavaScript project.

Looking Ahead

We’re thrilled with where Yarn Spinner is going, and we hope you are too. With these new features arriving in Yarn Spinner 3.0, we can’t wait to see what you build with our tools, and what games you create.

Also: we’re cooking up something new. We’re really excited about it. Stay tuned...

Talk more soon!

Jon, Tim, Paris, and Mars (and Ruby, pictured)

Read More