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 the projectFileVersion value from 2 to 3. 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. The always condition has a complexity of zero.
  • 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, or null 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 to QueryBestContent 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.

The Inspector for a Yarn Project, showing the settings for generating a variables class.

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.

Next
Next

Melbourne Games Week 2024