Yarn Spinner 3 Beta 1
Welcome to Yarn Spinner 3! We’re really excited about this release, and can’t wait to see what you think of it. Before we get started, here’s some important information about the release.
Things To Know First About The Beta
- Yarn Spinner 3 for Unity requires Unity version 2022.3 and above.
- If you’re upgrading an existing project, your Yarn Project assets will automatically re-import. If you get compile errors in Yarn Spinner 3 that were working in Yarn Spinner 2, please let us know in the Discord.
- If you’re upgrading from an earlier version of Yarn Spinner and you want to use new language features in your project, you will need to modify your
.yarnproject
files by opening it in a text editor and changing theprojectFileVersion
value from2
to3
. Newly created Yarn Project files will default to the new version. - The previous samples for Yarn Spinner 2 have moved to a new repository. The beta ships with a single sample, “Characters”; more will be added as we get closer to final release.
- The documentation for Yarn Spinner 3 is still in progress; not all API documentation has been written, and not all of the instructional material has been updated for Yarn Spinner 3.
- The best way to support Yarn Spinner’s continued development is to purchase it on Itch!
How To Install Yarn Spinner 3 Beta
To install the beta for Yarn Spinner 3 in your Unity project, follow these instructions:
- Open the Window menu and choose Package Manager.
- Click the
+
button at the top left, and choose “Add package from Git URL”. - Enter the following text and press enter:
https://github.com/YarnSpinnerTool/YarnSpinner-Unity.git#beta
You’ll also want to install the pre-release version of Yarn Spinner for Visual Studio Code. To install the prerelease, follow these instructions:
- In Visual Studio Code, open the View menu and choose Extensions.
- Type
yarn spinner
in the search box at the top of the pane. - Select Yarn Spinner, and wait for the page to appear.
- In the main view, click Switch to Pre-Release Version.
Language Features
Once statements
You can use a once
statement to run content only one time. When the script reaches a once
statement, it checks to see if it’s run before. If it has, it skips over it.
Note:
once
statements are great for making sure that the player never sees certain content more than once. For example, you might want a character to never introduce themselves to the player twice.
once
statements look like this:
<<once>>
// The guard will introduce herself to the player only once.
Guard: Hail, traveller! Well met.
Guard: I am Alys, the guard!
<<endonce>>
You can also use an else
clause in a once
statement to run other content, if the main content has already been run:
<<once>>
Guard: Hail, traveller! Well met.
<<else>>
Guard: Welcome back.
<<endonce>>
Finally, you can use once if
to run content a single time, but only when a certain condition is true. In all other cases, it will be skipped (or the else
content will be run, if there is any).
<<once if $player_is_adventurer>>
// The guard knows the player is an adventurer, so say this line,
// but only ever once!
Guard: I used to be an adventurer like you, but then I took an arrow in the knee.
<<else>>
// Either the player is not an adventurer, or we already saw the
// 'arrow in the knee' line.
Guard: Greetings.
<<endonce>>
The once
statement can also be used with lines and options.
If you add once
(or once if
) to a line, that line will only appear once, and will be skipped over every other time it’s encountered:
Guard: Who are you? <<once>> // Show this line only one time
Guard: Go on, get lost!
Similarly, if you add it to an option, that option will only be selectable once, and will be marked as unavailable after it’s been selected.
-> What's going on? <<once>>
Guard: The kingdom is under seige!
-> Where can I park my horse? <<once if $has_horse>>
Guard: Over by the tavern.
-> Lovely day today!
Guard: Uh huh.
-> I should go.
Guard: Please do.
Note:
once
statements are really useful when you want to show long, detailed content the first time it’s encountered, but you don’t want to show it every time. This means that players don’t need to mash the ‘skip line’ button over and over when they realise that they’re starting to see a long run of lines they’ve already seen.<<once>> // Show long, character-establishing lines the first time Guard: There's nothing new to report! Guard: I've been at this post for hours, and I'm so bored. Guard: I can't wait for the end of my watch. <<else>> // Show a more condensed version all other times Guard: Nothing to report! <<endonce>>
once
statements keep the information about whether they’ve been run or not in a variable that’s stored in your Dialogue Runner’s Variable Storage, just like any other variable. The variable isn’t directly accessible from your Yarn scripts.
Detour
The detour
statement lets you run content from a different node, and then return back to where you came from.
When you detour
into a node, Yarn Spinner runs the content from that node just as if you’d used a jump
statement. When you reach the end of the node, or reach a return
statement, Yarn Spinner will return to just after the detour
statement.
Here’s an example of using the detour
statement.
title: Guard
---
Guard: Have I told you my backstory?
-> Yes.
Guard: Oh. Well, then.
-> No?
<<detour Guard_Backstory>>
Guard: Anyway, you can't come in.
===
title: Guard_Backstory
---
Guard: It all started when I was a mere recruit.
// (five minutes of exposition omitted)
===
If the player replies ‘No?’ to the guard’s question, Yarn Spinner will detour to the Guard_Backstory
node and run its contents. When the end of that node is reached, Yarn Spinner will return to the Guard
node, and resume from just after the detour
statement.
You can return early from a detoured node by using the return
statement. Doing so will return to just after the detour
statement, as though the end of the node had been reached.
Note:
If Yarn Spinner reaches a
return
statement, and it hasn’t detoured from another node, it will stop the dialogue (that is, it will behave as though you had written a ``stop` command.)
When you detour into a node, that node can itself detour into other nodes.
If a detoured node uses a jump
command to run another node, the return stack is cleared. If you detour
into a node, and that node jumps
to another node, Yarn Spinner won’t return to your original detour
site.
Line groups
Line groups let you create a collection of lines that Yarn Spinner will choose from. When Yarn Spinner encounters a line group, it will select one of them, and run it. Line groups are collections of lines that begin with a =>
symbol.
Note:
Line groups are great for running ‘barks’ - collections of short lines that need to run in response to an in-game event. It can be useful to think of them like Yarn Spinner’s existing options
->
syntax, but instead of the player choosing which content to run, the computer picks it for you.
> => Guard: Halt!
=> Guard: No entry!
=> Guard: Stop!
You can attach conditions to lines in a line group, to ensure that they only run when it’s appropriate to do so. Conditions can be any true or false expression, and can also be combined with the once
keyword to ensure that a line can only run once. Finally, a line in a line group can have additional lines belonging to it, which will run if the item is selected.
=> Guard: Greetings, citizen.
=> Guard: Hello, traveller.
Guard: Stay vigilant. // runs after 'Hello, traveller.'
=> Guard: Hail, adventurer! <<if $player_is_adventurer>>
=> Guard: I used to be an adventurer like you, but then I took an arrow in the knee. <<once if $player_is_adventurer>>
Node groups
Node groups let you create a collection of nodes that Yarn Spinner will choose from. To create a node group, you create multiple nodes that all share the same name, and ensure that each of the nodes have at least one when:
header. The when:
header tells Yarn Spinner about the conditions that must be met in order to run the node.
title: Guard
when: once
---
Guard: You there, traveller!
Player: Who, me?
Guard: Yes! Stay off the roads after dark!
===
title: Guard
when: always
---
Guard: I hear the king has a new advisor.
===
title: Guard
when: $has_sword
---
Guard: No weapons allowed in the city!
===
Node groups are combined into a single node that performs the appropriate checks and then runs one of the node group’s members. You start dialogue with a node group using its name. You can also use the jump
or detour
statements to run a node group from somewhere else in your Yarn scripts.
Node groups are similar to line groups in their behaviour, but give you more room to create longer passages of content. Your C# code can also check to see how many (if any) nodes can run.
Saliency
Saliency lets you control how line groups and node groups select which content to run.
When a line group or node group needs to run content, it needs to make a decision about which item in the group to choose. The method for making this decision is called a saliency strategy.
Saliency strategies are provided with the following information about each item:
- How many of its conditions passed (that is, the
when:
clauses on a node group, or the single condition on a line group) - How many of its conditions failed
- The total complexity of all of its conditions
- Complexity is calculated as the total number of boolean operators (and, or, not, xor) present in a condition, plus 1 if the condition is a
once
condition. Thealways
condition has a complexity of zero.
- Complexity is calculated as the total number of boolean operators (and, or, not, xor) present in a condition, plus 1 if the condition is a
- A unique key that identifies the content.
Yarn Spinner has several built-in saliency strategies.
- First: The first item in the group that has not failed any of its conditions is selected.
- If the items of a node group are all in the same file, the ordering of the group is the order in which they appear in the file. If a node group’s nodes are split up across more than one file, the ordering of the nodes is not defined. the node that is considered ‘first’ is not defined.
- Best: The items that have not failed any conditions are sorted by their total complexity score, and the first item that has the highest score is selected.
- Best Least Recently Viewed: The items that have not failed any conditions are sorted by score, and then by how many times they have been selected by this strategy. If there is more than one best item remaining, the first of these is selected.
- Random Best Least Recently Viewed: The items that have not failed any conditions are sorted by score, and then by how many times they have been selected by this strategy. If there is more than one best item remaining, a random one of these is selected.
A saliency strategy is given a collection of possible options, and returns either one of them that should be run, or null
to indicate that none of them should run.
Creating Custom Saliency Strategies
Your C# code can create custom saliency strategies. To do so, create a type that implements the IContentSaliencyStrategy
interface, and assign it to your Dialogue
object’s ContentSaliencyStrategy
property.
IContentSaliencyStrategy
has two required methods.
ContentSaliencyOption? QueryBestContent(IEnumerable<ContentSaliencyOption> content)
determines which of a collection of options, if any, should run. It should return the best content from the available options, ornull
if none of them should run. This is the main method in which you write the specific logic for your custom strategy.- Calling this method does not indicate that the content has been selected; rather, it is a query to determine what should be selected. Your implementation of this method should not change any state, and should be read-only. The content returned by this method is not guaranteed to actually be run.
void ContentWasSelected(ContentSaliencyOption content)
is called to indicate that a specific piece of content returned by a previous call toQueryBestContent
has been selected. This method should update any appropriate state to represent this fact, such as by updating the total number of times the content has been selected.
Querying If Any Content Can Run
You can use a node group to check to see if any of its items can run, This can be useful when determining whether to show if a character should be marked as ready to talk, or whether any of its nodes have a special tag (for example, if a character has important information to discuss, as opposed to more general conversation.)
Enums
Enums let you create variables whose value is constrained to a pre-defined list of possibilities. An enum (short for ‘enumeration’) is useful when you have a variable that needs to have a wider range of possible values than simply true
or false
, but needs to be more specific than a number
or string
.
// 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 set $favouriteFood to the 'apple', 'orange' or 'pear'
// cases, but nothing else!
<<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 = .Pear>>
Smart variables
Smart variables let you create variables whose value is determined at run-time, rather than setting and retrieving a value from storage. Smart variables give you a simple way to create more complex expressions, and re-use them across your project.
To create a smart variable, declare it using the declare
statement and provide an expression, rather than a single value:
// A boolean value that is 'true' when the player has more than 10 money
<<declare $player_can_afford_pie = $player_money > 10>>
Smart variables can be accessed anywhere a regular variable would be used:
// Run some lines if the player can afford a pie
<<if $player_can_afford_pie>>
Player: One pie, please.
PieMaker: Certainly!
<<endif>>
Shadow Lines
Shadow lines let you reuse the same line in multiple places, without having to create duplicate copies. Shadow lines are copies of other lines, but don’t create a duplicate entry in the string table. This can be useful if you want to re-use an existing line in more than one place, which can be important when each line has in-game assets like voice-over recording.
Shadow lines are marked using the #shadow:
hashtag. When you use the #shadow:
tag, you specify the line ID of another line, which is called the source line. The source line must have an explicit #line:
hashtag to identify it.
Here’s a simple example of using shadow lines. The script contains six lines, but only 5 string table entries will be created, because the line “I should go” is shadowed.
title: Tavern
---
Ava: Hello, barkeep!
Guy: Hi there, how can I help?
Ava: I should go. #line:departure
===
title: Kitchen
---
Ava: Greetings, chef!
Guy: What are you doing back here?
Ava: I should go. #shadow:departure
===
Shadow lines are required to have the same text in the Yarn script as their source line, but are allowed to have different hashtags.
Unity Features
Async Dialogue Views
Dialogue Views, which are the components that receive content from Yarn Spinner and present it to the player, now use async/await to manage their lifecycle.
Using async/await has a number of advantages:
- Writing dialogue views is a lot simpler, and involves fewer steps and callbacks. This also means that there’s fewer places where bugs can occur.
- Dialogue views can now be told that to cancel their delivery of the current content, or to speed up their delivery. Cancellation is just a signal to finish up faster, which means that dialogue views can be very flexible.
- Async dialogue views are able to cooperate with each other in a much simpler way than previously.
Existing Views Already Work
Your existing (non-async) dialogue view code will automatically seamlessly work with Yarn Spinner 3, and you don’t need to change it if you don’t want to. (We recommend updating them to use the new async API, since it’s a much nicer development experience, but you don’t have to!)
Async Systems
There are several options for using async/await in Unity, depending on which version of Unity you’re using, and what packages you have installed. Yarn Spinner will automatically use the first one in this list that’s available.
> Note:
For the most part, you only need to care about this if you’re writing a custom dialogue view.
- UniTask is a free, open-source, third-party implementation of Tasks that is lightweight, performant, designed to work well with Unity’s run loop, and available for all versions of Unity that Yarn Spinner 3 supports. Yarn Spinner will always use UniTask if it’s installed in your project, no matter what version of Unity you’re using.
- Awaitables are available on Unity 2023.3 and above. They’re lightweight like UniTask, but have fewer features for working with them.
- .NET Tasks are available on all versions of Unity that Yarn Spinner 3 supports, and are used as a fallback if no other option is available. They work well, but can be less performant than the other two choices, and require additional care to avoid bugs (such as making sure that you catch any exceptions you throw, and that your tasks stop if the Editor leaves Play Mode.) .NET Tasks are only used if your project uses Unity 2022 and don’t have UniTask installed.
> Note:
Our recommendation is that you use UniTask. (We actually recommend it even if you aren’t using Yarn Spinner. It’s really good.)
Yarn Spinner uses the YarnTask
type to create a very thin wrapper around the selected async system that’s compatible with all of them. YarnTask
objects are implicitly convertible to and from other task types, and you can mix and match them with any task type you like, anywhere in your project.
Creating a new Dialogue View
To create a new dialogue view, you subclass AsyncDialogueViewBase
and implement the required methods. Then, create an instance of it in your scene, and add it to your Dialogue Runner’s list of dialogue views. When the dialogue runner needs to present content to the player, it will send it to all of the dialogue views, and wait for them to finish before showing the next piece of content.
This means that dialogue views control the timing of lines. If you aren’t ready for the dialogue runner to move on, wait until you’re ready before returning.
> Note:
You can create a new dialogue view class by opening the Assets menu and choosing Create → Yarn Spinner → Dialogue View Script.
Running Lines
The RunLineAsync
method receives a line from the Dialogue Runner, presents it to the player, and returns when it’s finished presenting the line.
Note:
When we say ‘presenting a line,’ we mean whatever a Dialogue View needs to do in order for the player to experience it. For example, a dialogue view might show on-screen text and wait for a button to be clicked, or it might start playing audio and wait for it to finish playing.
In addition to the line, RunLineAsync
also receives a LineCancellationToken
object. You can use this object to be notified when the rest of the dialogue system wants your line view to speed up its presentation, or to finish up and get ready for the next piece of content.
Here’s a minimal implementation of RunLineAsync
that shows the line text in a TextMeshPro text view and waits for a signal to move on:
public override async YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token)
{
// Show the line
this.textView.text = line.Text.Text;
// Wait until the NextLineToken becomes cancelled,
await YarnTask.WaitUntilCanceled(token.NextLineToken);
// Hide the line
this.textView.text = "";
// All done!
}
The Dialogue Runner will wait until the last dialogue view returns from the RunLineAsync
call before showing any additional content. If you’re writing a dialogue view that doesn’t need to present lines, its RunLineAsync
method should just return immediately without doing anything else.
Running Options
Running options is similar to lines. When the dialogue runner needs to present options to the player, it calls RunOptionsAsync
on all of its dialogue views. The difference between RunLinesAsync
and RunOptionsAsync
is that RunOptionsAsync
needs to return a value, indicating whether an option was selected (and, if it was, which one.)
The dialogue runner will all of its dialogue views return. If a dialogue view doesn’t need to handle options, its RunOptionsAsync
method should return null
without doing anything else.
Note: If all of the dialogue views return
null
, then the dialogue runner will log an error to the console, because it won’t know which option to choose.
Much like RunLineAsync
, RunOptionsAsync
receives a cancellation token, which represents whether the dialogue runner no longer needs the dialogue view to choose an option. This can happen if the entire dialogue is cancelled, or if a different dialogue view selected an option. When then token becomes cancelled, your dialogue view should return null
as quickly as possible.
Here’s a very simple implementation of RunOptionsAsync
that uses the keyboard to select an option:
public override async YarnTask<DialogueOption?> RunOptionsAsync(DialogueOption[] dialogueOptions, CancellationToken cancellationToken)
{
while (cancellationToken.IsCancellationRequested == false) {
// Get input
if (Input.GetKeyDown(KeyCode.Num1)) { return dialogueOptions[0]; }
if (Input.GetKeyDown(KeyCode.Num2)) { return dialogueOptions[1]; }
if (Input.GetKeyDown(KeyCode.Num3)) { return dialogueOptions[2]; }
// Wait until the next frame
await YarnTask.Yield();
}
// The token became cancelled. Return null to indicate that we
// didn't choose anything.
return null;
}
Starting and Stopping Dialogue
Dialogue views get notified of when the dialogue runner starts and ends dialogue, by calling OnDialogueStartedAsync
and OnDialogueCompleteAsync
. Your dialogue view can use this to get ready for showing dialogue, like changing the camera, or showing on-screen effects, and to finish up after showing dialogue.
The dialogue runner will wait for all of its dialogue views to return from OnDialogueStartedAsync
before showing any lines. (It won’t wait for calls to OnDialogueCompleteAsync
, because there’s nothing left that needs to happen.)
Handling Interruptions
Both RunLineAsync
and RunOptionsAsync
receive cancellation tokens that represent a request to finish up and get ready for the next piece of content. It’s up to your line view to decide what ‘finish up’ means: fading away on-screen UI, stopping audio playback, or something else. Your dialogue view is allowed to take as much time as it needs to finish up, but it should generally try to do it quickly, because the player is waiting for the next piece of content.
RunLineAsync
also receives a cancellation token that represents a request to speed up the current line’s presentation, but not necessarily finish up and move on. For example, if the speed up cancellation token becomes cancelled, a dialogue view that shows on-screen text one character at a time (a so-called ‘typewriter effect’) might increase the speed of the delivery, or skip to the end and show all text immediately.
Your code can signal that you want to speed up or advance to the next piece of content by calling the RequestHurryUpLine
and RequestNextLine
methods on your Dialogue Runner. Doing this will cancel the tokens provided to RunLineAsync
and RunOptionsAsync
, indicating that they should hurry up or advance.
You can also cancel the entire dialogue by calling Stop()
on the Dialogue Runner. This will cancel the tokens, and also signal that dialogue is complete.
Async Line Providers
Line Providers are components that the Dialogue Runner uses to convert line IDs to user-facing content. They take the unique identifier for a line and the user’s current locale, and determine the appropriate text and asset (for example, voice-over audio clip) that should be shown to the user.
Note:
Yarn Spinner comes with two types of line providers that cover most needs: the Built-In Localisation Line Provider fetches localised content from Yarn Spinner’s built-in localisation system, and the Unity Localisation Line Provider fetches localised content from the Unity Localization system. Unless you’re using a different localisation system than these two, you don’t need to write your own.
Line Providers now also follow the async/await pattern; for example, the GetLocalizedLineAsync
method now returns a YarnTask<LocalizedLine>
, and is able to asynchronously load the resources it needs for a given line.
For more information, see the documentation for LineProviderBehaviour
.
Codegen Variable Storage
Yarn Spinner for Unity can now generate a C# class that contains properties for all of your Yarn Project’s variables and smart variables. You can use this generated class instead of the default InMemoryVariableStorage
class to provide compile-time checked access to your variables, instead of referring to variables using strings. If you have your own variable storage class that you use for storing variables, you can make the generated class be a subclass of that.
To generate a variable storage class, turn on ‘Generate Variables Source file’ in the Inspector for your Yarn Project. You can specify its name, its namespace, and which type of variable storage class it should subclass.
Generated variable storage classes create properties for each of the variables in your Yarn Project. The generated variable storage class will also create C# enum
declarations for any enums that your Yarn Project declares.
Custom Markup Processing
Yarn Spinner allows you to use markup in your lines to control the presentation and content of your lines. There are two different types of markup:
- Replacement markup is markup that affects the contents of a line.
- Temporal markup is markup that affects the delivery of a line.
When a line is delivered, the dialogue runner requests a LocalizedLine
from the Line Provider with a given line ID. The Line Provider fetches the content for the line (for example, from a localised string table), and then passes that content to each of its registered replacement markup handlers to produce the final string. The dialogue runner then sends this content to all of its dialogue views, each of which pass the content to their temporal markup handlers to control the pacing of the delivery.
> Note:
Replacement markup is used for several built-in features of Yarn Spinner, including the
> select
> and> plural
> markers.
Replacement markup can be used to inject new content into a line, or modify existing content in a line. For example, the built-in [style]
marker is a replacement marker that rewrites all of the text inside of it to include a TextMeshPro <style>
tag.
Temporal markup can be used to perform actions in the middle of a line. For example, the built-in [pause]
marker stops the delivery of a line for a certain amount of time before continuing, allowing you to control the pacing of a line.
Because markup is stored in the content that the line provider fetches, it’s controllable on a per-locale basis. This means that you can have different markup for different languages, depending on the individual needs of each language.
> Note:
The specifics of handling temporal markup are up to individual line views. The built-in Line View class combines temporal markup with its typewriter effect; your own custom dialogue views may need to handle it differently, based on their own needs.
For more information on registering replacement and temporal markup, see the documentation for Line Providers, TemporalMarkupHandler
, and IAttributeMarkerProcessor
.
Melbourne Games Week 2024
We're thrilled to be at Melbourne International Games Week 2024! Come by our talk at GCAP (today, Monday, 7 October 2024, at 12 midday in Room 110), and chat to us, or book a meeting with Jon, Tim, or Paris on MeetToMatch.
We're showing off Story Solver, our upcoming product! Get in touch for a demo, or to chat Yarn Spinner consulting, integration, or ask us questions!
We're around the whole week, so if you miss at at GCAP, just send us an email, or find us around the place. We'll also be at Megadev!
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)