All pages
Powered by GitBook
1 of 14

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Bevy

Yarn Spinner for Rust is the set of components and scripts that make Yarn Spinner work inside a Bevy project.

In this section, you’ll learn how to work with Yarn Spinner for Rust more in-depth.

Custom Commands and Functions

  • You can define your own , which allow the scripts you write in Yarn Spinner to control parts of the game that you've built.

  • are units of code that Yarn scripts can call to receive a value. In addition to the that come with Yarn Spinner, you can create your own.

Yarn Spinner for Rust's documentation for Yarn Spinner 3 is being updated.

Yarn Spinner for Rust v0.2 works with Bevy version 0.13

commands
Functions
built-in functions

Components

Learn about the Bevy components that you use when working with Yarn Spinner for Rust.

Yarn Spinner for Rust is made up of a number of components. The most important of these are the Dialogue Runner, which loads and runs your scripts, and the Dialogue Views that show content to your player.

In this section, you'll learn about how to work with each of these.

Bevy Samples

  • Our examples directory contains many small programs showcasing individual features

  • The demo has its code open sourced to learn from it

  • The Foxtrot template uses Yarn Spinner for Rust for its dialog. You can take a look at it to learn how Yarn Spinner is used in a more comprehensive 3D project.

Bevy Quick Start

Quickly get started with a simple scene.

We will now go through the steps to setup a new Bevy project running Yarn Spinner dialogues

Setting up the crate

Run the following in your terminal to create a new crate with the required dependencies:

cargo new yarnspinner_playground
cd yarnspinner_playground
cargo add bevy bevy_yarnspinner bevy_yarnspinner_example_dialogue_view

The dependency bevy_yarnspinner is for the Yarn Spinner Bevy plugin proper, while bevy_yarnspinner_example_dialogue_view gives us a nice default Dialogue View, so we can actually see the text we've written and have options to click on.

Adding the Yarn Files

We'll use a single Yarn file for this example. Inside the folder assets/dialogue, add a file named example.yarn with the following content:

# assets/dialogue/example.yarn
title: Start
---
Hello World!
Wow! My first ever Yarn script in Rust!

-> Gosh!
-> Incredible!
-> I'm amazed!

Anyway, time to get writing!
===

You can learn about our recommended editor, Visual Studio Code with the official Yarn Spinner Extension at: .

The main code

Add the following code to your src/main.rs.

Reiterating the comments in the code, let's take a look at some snippets.

This self-explanatory line initializes the plugin. When using the standard constructor with no options, Yarn files will be searched for in the directory <your game>/assets/dialogue/, where all files ending in .yarn will be compiled as soon as the game starts.

The plugin makes sure all components of Yarn Spinner work except for any actual graphics. You need to instantiate a for that:

Here we initialize the dialogue view shipped by the bevy_yarnspinner_example_dialogue_view crate. It offers some sensible defaults which you can see in the screenshots used throughout this guide. You can of course skip this and use your own dialogue view instead.

The method .run_if(resource_added::<YarnProject>() is our way of saying "run this system once as soon as our Yarn files are done compiling". Let's look at what will actually be run in that moment:

The main way of interacting with Yarn files during runtime and managing the flow of a dialog is through a . To do this, we use the resource we referenced in the run_if section above. It represents our compiled Yarn files, which we use to create a new dialog runner. We then point it to the named "Start" of our Yarn file. We use start_node for this, which will "move" the dialog runner to the provided node and start executing the dialog in the next frame, using the registered Dialogue View to actually present it on the screen. Finally, we spawn the dialog runner on an own entity into the Bevy world.

In the end, your file structure should look like this:

Run your game with cargo run and you should see the following:

Variable Storage

Variable Storage components are responsible for storing and retrieving the values of variables in your Yarn scripts. When a Yarn script needs to get the value of a variable, it asks the Variable Storage for it; when a Yarn script sets the value of a variable, the Variable Storage is given the value.

Each game has different requirements for how variables are stored, which means that Yarn Spinner doesn't make any assumptions how the information is actually stored on disk. Instead, you can create your own custom Variable Storage script that implements the methods that Yarn Spinner needs.

If you don't connect a Variable Storage to your Dialogue Runner, it will create an In-Memory Variable Storage when the game starts, and use that.

The In-Memory Variable Storage component is a Variable Storage component that stores all variables in memory. These variables are erased when the game stops.

Every game's data storage requirements are different. For this reason, Yarn Spinner is designed to make it straightforward to create your own custom component for managing how Yarn scripts store and load variables in ways that work with the other parts of your game.

Custom Variable Storage components are implementations of the trait . To implement your own, check out the documentation. Once you have it, you can use it by calling when building your Dialogue Runner.

Dialogue Views

Learn about Dialogue Views, which present dialogue content to the user.

Yarn Spinner itself handles only the hard logic behind the dialogue flow, but it doesn't actually draw anything to the screen. This is the job of Dialogue Views. They are plugins that react to s fired by the Dialogue Runner and display them to the player.

A Dialogue Runner can have multiple Dialogue Views. For example, you might have one Dialogue View that's designed to display lines of dialogue, and another that's in charge of displaying options to the player.

Because every game's needs are different, a Dialogue View is designed to be extremely customisable, and you can create your own custom dialogue views to suit the needs of your game.

However, because there are common patterns of how games work with dialogue, Yarn Spinner for Rust comes with a pre-built Dialogue View that handles common use cases: The . You'll see it used all over our examples. To use it, you simply add its plugin after the Yarn Spinner plugin proper:

And that's it! It will display whatever comes its way to the user and handle some basic input. As an added bonus, it will send out a whenever the active speaker has changed from its point of view, in case you want to e.g. rotate your camera there. If you do such an interaction, be sure to place your code in a Bevy system set that comes after the to avoid race conditions.

If you don't have a game save system, you can use the MemoryVariableStorage component. This is a simple Variable Storage component that's built into Yarn Spinner.

The In-Memory Variable Storage stores everything in memory; when the game ends, all variables that have been stored are erased.

In-Memory Variable Storage

The In-Memory Variable Storage component is intended to be a useful tool for getting started, and to be replaced with a custom variable storage that meets your game's needs.

However, if your game has no need to save and restore game state, then this class can be used in your final game, too.

Custom Variable Storages

VariableStorage
DialogueRunnerBuilder::with_variable_storage
Dialogue View
DialogueRunner
YarnProject
node
Advanced Scripting
file_system.png
hello_world.png
To create your own Dialogue View, simply create a plugin that handles the different variants of
that come up during play. These are regular
that you can handle using an
. Make sure that you run your plugin in a Bevy system set after the
to catch all events that were sent in a given frame.

For inspiration, check out the source code of the example Dialogue View.

Example Dialogue View

Custom Dialogue Views

DialogueEvent
Example Dialogue View
SpeakerChangeEvent
ExampleYarnSpinnerDialogueViewSystemSet
DialogueEvent
Bevy events
EventReader
YarnSpinnerSystemSet
// src/main.rs
use bevy::{prelude::*, asset::ChangeWatcher, utils::Duration};
use bevy_yarnspinner::prelude::*;
use bevy_yarnspinner_example_dialogue_view::prelude::*;

fn main() {
    let mut app = App::new();
    app.add_plugins((
        DefaultPlugins,
        // Add the Yarn Spinner plugin. 
        // As soon as this plugin is built, a Yarn project will be compiled 
        // from all Yarn files found under assets/dialogue/*.yarn
        YarnSpinnerPlugin::new(),
        // Add the example dialogue view plugin
        ExampleYarnSpinnerDialogueViewPlugin::new(),
    ))
    // Setup a 2D camera so we can see the text
    .add_systems(Startup, setup_camera)
    // Spawn the dialog as soon as the Yarn project finished compiling
    .add_systems(
        Update,
        spawn_dialogue_runner.run_if(resource_added::<YarnProject>()),
    )
    .run();
}

fn setup_camera(mut commands: Commands) {
    commands.spawn(Camera2dBundle::default());
}

fn spawn_dialogue_runner(mut commands: Commands, project: Res<YarnProject>) {
    let mut dialogue_runner = project.create_dialogue_runner();
    // Start the dialog at the node with the title "Start"
    dialogue_runner.start_node("Start");
    commands.spawn(dialogue_runner);
}
YarnSpinnerPlugin::new(),
ExampleYarnSpinnerDialogueViewPlugin::new(),
spawn_dialogue_runner.run_if(resource_added::<YarnProject>()),
fn spawn_dialogue_runner(mut commands: Commands, project: Res<YarnProject>) {
    let mut dialogue_runner = project.create_dialogue_runner();
    // Start the dialog at the node with the title "Start"
    dialogue_runner.start_node("Start");
    commands.spawn(dialogue_runner);
}
app.add_plugins((
    DefaultPlugins,
    YarnSpinnerPlugin::new(),
    ExampleYarnSpinnerDialogueViewPlugin::new(),
));

Compiling Yarn Files into Yarn Projects

Learn about Yarn Projects, which group your scripts together for use in a Dialogue Runner.

The YarnProject resource represents the set of all compiled Yarn files of your game. You cannot construct it yourself. Instead, it is inserted into the Bevy world for you when the compilation is finished. You can however steer how and when this is done.

Starting the Compilation Process

Generally, you'll want your game to compile the Yarn files as soon as possible. This is why the YarnSpinnerPlugin will start doing so by default when it is added to the app.

If for some reason you do not wish to start compilation right away, you can defer this process. To do this, construct the YarnSpinnerPlugin with YarnSpinnerPlugin::deferred() when adding it. Then, whenever you are ready to start the compilation, you can send a LoadYarnProjectEvent. Its construction methods are identical to the YarnSpinnerPlugin. In fact, when not running in deferred mode, the YarnSpinnerPlugin simply relays its setting to a LoadYarnProjectEvent and sends it.

If you look through the documentation of the [YarnSpinnerPlugin], you'll notice a few methods to modify its settings. The first few deal with where our Yarn files are coming from.

By default, Yarn Spinner will look in <game directory>/assets/dialogue. Yarn Spinner can only read files from the assets directory — or its equivalent, if you have changed this default in the AssetPlugin on platforms which support it— but you can change how the assets will be looked through.

The way to specify this is via YarnFileSources. This enum tells Yarn Spinner where one or more Yarn files come from and can be added to an AssetPlugin with AssetPlugin::add_yarn_source(). The enum variants should be self explanatory, but the two most common use-cases come with their own convenience constructors:

  • YarnFileSource::file(): looks for a Yarn file at a path inside under the assets directory.

  • YarnFileSource::folder(): recursively looks through a given subdirectory for Yarn files.

Since the Wasm and Android builds of Bevy have restrictions on their filesystem access, they cannot use YarnFileSource::folder() and must have all their Yarn files listed explicitly with YarnFileSource::file(). As such, the default behavior provided by YarnSpinnerPlugin::new() is not suitable for these platforms. To avoid it, use the AssetPlugin::with_yarn_source() constructor instead.

As you might have guessed by now, YarnSpinnerPlugin::new() is simply a shorthand for AssetPlugin::with_yarn_source(YarnFileSource::folder("dialogue")).

YarnSpinnerPlugin::with_development_file_generation() accepts a DevelopmentFileGeneration, which tells Yarn Spinner how aggressively to generate useful files on runtime. "Useful" refers to the developer and not the user. The default is DevelopmentFileGeneration::TRY_FULL, which will be DevelopmentFileGeneration::Full on platforms which support filesystem access, i.e. all except Wasm and Android. See the documentation for the full list of effects. Suffice it to say that this is not very important when developing without localization, but becomes vital otherwise. See the chapter for more.

Since these settings are intended for development, you can use YarnSpinnerPlugin::with_development_file_generation(DevelopmentFileGeneration::None) when shipping your game to optimize the runtime costs and avoid generating files that are useless to the player.

The settings accessed by YarnSpinnerPlugin::with_localizatons are important enough to warrant their own chapter. See .

Whether you used YarnSpinnerPlugin or LoadYarnProjectEvent, as soon as the compilation finished, a YarnProject resource will be inserted into the Bevy world. You can react to its creation by guarding your systems with .run_if(resource_added::<YarnProject>()), as seen in the .

Once you have the YarnProject, you can use it to spawn a DialogueRunner which in turn can, well,

Assets

While Bevy as a whole has assets, Yarn Spinner can associate specific assets with lines. These are always localised, such as voiceovers.

Using Metadata Instead of Assets

Before we jump into assets, let's first help you out if you don't care about localization. The mechanism in place for this is line metadata, which are strings you can add to Yarn lines after a hashtag:

title: Start
---
Granny: It's hard to believe that it's over, isn't it? #smiling
Granny: Funny how we get attached to the struggle. #laughing
Granny: Promise me that you'll take care of yourself, okay? #smiling
===

A Dialogue View will be able to read the metadata "smiling", "laughing", and "smiling" again from LocalizedLine::metadata and accordingly load things like character portraits. These annotations will also be written into the "comment" field of strings files, which are explained in the chapter Localisation.

Asset Providers

Assets are fetched from the filesystem by structs implementing AssetProvider. They need to be registered when creating a DialogueRunner. For example, if you use the audio_assets feature, you can register an asset provider for audio files by modifying the code found in the Quick Start like this:

fn spawn_dialogue_runner(mut commands: Commands, project: Res<YarnProject>) {
    let mut dialogue_runner = project
        .build_dialogue_runner()
        .add_asset_provider(AudioAssetProvider::new())
        .build();
    dialogue_runner.start_node("Start");
    commands.spawn(dialogue_runner);
}

The bundled example Dialogue View does not play any audio files, so you will need to write your own to make use of this feature.

The AudioAssetProvider itself is just a specialized . As the name suggests, it serves any assets based on their extension:

The (and, by extension, the AudioAssetProvider) will search for their assets in the directory assets/dialogue/<language>/<line-id.extension>. So, for example, an AudioAssetProvider serving up a voiceover for the line with the ID 41239 while the game is set to the language "de-CH" will search for assets/dialogue/de-CH/41239.mp3.

Finally, you can implement yourself with whatever custom behavior you desire. Check out the trait's documentation for the necessary methods.

Functions

As mentioned in the chapter , Yarn can access user-defined functions. A collection of functions is called a library and can be accessed through a DialogueRunner.

For an easy example, let's modify the code used in the to provide a simple pow function to Yarn:

The following snippet is of special importance:

The first parameter of add_function() is the name of the function as seen by Yarn, "pow" in this case. The second parameter is the Rust function that will be called in the background. Here, we reference the function definition of fn pow(...), but you could also register a lambda.

Commands

work very similar to Yarn , but use a different syntax and are able to modify the game world. As a consequence of their similarity, registering custom commands is very similar to registering .

Just as with Yarn functions, registration happens when creating a DialogueRunner. Let's again modify the example from the :

We call the command like this:

You will have seen one crucial difference to Yarn functions immediately. The parameters are not passed in directly to the Rust function, but are wrapped in an In struct. This is because Rust functions that are registered as commands are always valid Bevy systems. The In parameter just tells the function which values come from the Yarn file, but we can additionally query the Bevy world as we want:

which we call like this:

In contrast to

Settings

Yarn File Sources

Development File Generation

Localization

After the Compilation

Localization
Localization
setup
run dialogues
FileExtensionAssetProvider
FileExtensionAssetProvider
AssetProvider
Dialogue View
fn spawn_dialogue_runner(mut commands: Commands, project: Res<YarnProject>) {
    let image_provider = FileExtensionAssetProvider::new()
        .with_file_extensions(file_extensions! {
           Image: ["png", "jpg", "jpeg"],
        });
    let mut dialogue_runner = project
        .build_dialogue_runner()
        .add_asset_provider(image_provider)
        .build();
    dialogue_runner.start_node("Start");
    commands.spawn(dialogue_runner);
}
This pow function can now be called from the Yarn file like this:

Which will result in the following output:

Custom functions need to follow some rules. Don't worry, they're pretty lax.

  • Their parameter and output types need to be primitive types or String

  • Parameters are allowed to be references

  • Parameters can have the special type YarnValue, which stands for any input type. Additionally, functions are assumed to have no side effects. You can read the full list of requirements in the docs for YarnFn.

Here are some examples of valid functions:

If you need functions that have side effects, e.g. for manipulating the game world, use instead.

Registered Rust functions can have a maximum of 16 parameters. If you need more, you can wrap parameters in tuples:

Tuples are treated as separate parameters when calling the function from Yarn:

Since tuples can be nested, you can use have potentially infinite parameters.

fn spawn_dialogue_runner(mut commands: Commands, project: Res<YarnProject>) {
    let mut dialogue_runner = project.create_dialogue_runner();
    // Add our custom function to the dialogue runner
    dialogue_runner.library_mut().add_function("pow", pow);
    dialogue_runner.start_node("Start");
    commands.spawn(dialogue_runner);
}

fn pow(base: f32, exponent: f32) -> f32 {
    base.powf(exponent)
}
dialogue_runner.library_mut().add_function("pow", pow);

Function Registration

Functions
Quick Start
, commands cannot have any Yarn facing return types. The Rust functions however can use a return value to indicate that Yarn Spinner should wait a while before continuing the dialogue. This is useful for times when you want to change something in the world before the dialogue goes on, e.g. move the camera to another speaker. To do this, simply return a type implementing
, for example Arc<AtomicBool>. This way, you can keep a copy of the Arc and change its content to true whenever your transition is over.

For a practical example, check out how we at the end of the .

fn spawn_dialogue_runner(mut commands: Commands, project: Res<YarnProject>) {
    let mut dialogue_runner = project.create_dialogue_runner();
    // Add our custom command to the dialogue runner
    dialogue_runner
        .commands_mut()
        .add_command("print_addition", commands.register_system(print_addition));
    dialogue_runner.start_node("Start");
    commands.spawn(dialogue_runner);
}

fn print_addition(In((a, b)): In<(f32, f32)>) {
    print!("{a} + {b} = {c}", c = a + b)
}
title: Start
---
Let's print the addition of 1 and 3 in the console:
<<print_addition 1 3>>
===
fn spawn_dialogue_runner(mut commands: Commands, project: Res<YarnProject>) {
    let mut dialogue_runner = project.create_dialogue_runner();
    // Add our custom command to the dialogue runner
    dialogue_runner
        .commands_mut()
        .add_command("insert_resource", commands.register_system(insert_resource));
    dialogue_runner.start_node("Start");
    commands.spawn(dialogue_runner);
}

#[derive(Resource)]
struct Person {
    name: String,
    age: f32,
}

fn insert_resource(In((name, age)): In<(String, f32)>, mut commands: Commands) {
    commands.insert_resource(Person { name, age });
}
title: Start
---
Let's insert a resource into the Bevy world:
<<insert_resource "Bob" 42>>
===

Command Registration

The Rust functions serving as commands always require an In parameter. If your Yarn command doesn't accept any parameters, specify the first parameter in Rust like this: fn my_command(_: In<()>, ...)

Return Types and Long Running Commands

Commands
functions
custom functions
Quick Start
functions
TaskFinishedIndicator
implement a fade out
demo

Allowed Signatures

Size constraints

custom commands
title: Start
---
Two to the power of three is {pow(2,3)}
===
fn add(a: f32, b: f32) -> f32 {
    a + b
}

fn concat(a: &str, b: &str) -> String {
    format!("{a}{b}")
}

fn greet(name: &str, age: usize) -> String {
    format!("Hello {name}, you are {age} years old!")
}

fn format_anything(value: YarnValue) -> String {
    format!("Got the following value: {value}")
}
fn add((a, b): (f32, f32)) -> f32 {
    a + b
}
title: Start
---
Two plus three is {add(2, 3)}
===

Frequently Asked Questions / "How Do I...?"

Common questions and solutions for using Yarn Spinner in Rust using Bevy.

Text

How do I style text? How do I make some words bold, italic, colorful, etc.?

Yarn Spinner doesn't do text rendering, you have to use existing Bevy plugins.

How do I animate wavy text, like in Night In The Woods?

Yarn Spinner doesn't handle text rendering. You'll need a separate wavy text plugin.

How do I use Yarn Markup?

Markup lets you mark a range of text (words, phrases) in a generic way, for whatever use. You could use it to style text, add sentence markers, make clickable words, etc.

// Yarn script example of custom "wavy text" markup.
Oh, [wave]hello[/wave] there!

// After compiling, text will look like: "Oh, hello there!"
// And then the resulting markup data will look like:
// - name: "wave"
// - position: 4
// - length: 5

Note that YS only processes the text data. You must still code the actual markup effect yourself. See Markup.

Variables

How do I print the value of a variable in dialogue?

Wrap the variable (or any expression) in curly braces ({, }) to evaluate and output it. For more info, see Variables.

<<set $variableName to "a string value">>
The value of variableName is {$variableName}.
// This will appear as "The value of variableName is a string value."

How do I read / write Yarn variables from Rust?

To read Yarn variables from Rust, use VariableStorage::get(). To write Yarn variables from Rust, use VariableStorage::set()

Don't forget the $ when writing the variable's name!

let variable_storage = dialogue_runner.variable_storage_mut(); 
let test_variable: f32 = variable_storage.get("$testVariable")?.try_into()?;
variable_storage.set("$testVariable", (testVariable + 1).into())?;

How do I 'sync' variables between Yarn and Rust?

You could hack this with static variables. But we recommend avoiding any "sync" pattern, because then you'll have to track and maintain the same data in two different places. Programmers usually prefer a . Data should live in only one place. Variables should either live in Yarn or live in Rust, and not in both.

To save the current node, save the value of somewhere, e.g. to a .ron file. Then to restore it, call and pass in the saved node name.

To save variables, fetch them using , then use to read them all and store them again somewhere. Then to load variables, call .

For custom save systems, create your own by implementing and its methods. Study as an example.

It is not currently possible to save or restore the specific line that the dialogue is running.

To jump to a node from Yarn, use <<jump (nodeName)>>. See .

To jump to a node with Rust, just call , even if there's already dialogue running.

Jumping to a specific line in a node is currently not supported. Instead, .

Yarn Spinner automatically adds a #lastline tag to a line when the next step is a set of options. Create a that uses to check for "lastline" and perform the behavior you want.

To display anything in Yarn Spinner, use a plugin.

Create a custom dialogue view with a custom effect based on to detect the next text character and pause accordingly.

Write input code to detect clicking / tapping, then call .

This implementation will vary for every game, so we purposely do not attempt to design a one-size-fits-all generic NPC system. Here's some example pseudocode to make your own:

The math / code is a little complicated. Calculate the NPC's on-screen position, then convert this screen position to UI canvas space, and reposition the dialogue bubble.

There is no real technical limit on the number of Yarn scripts or the size of Yarn Projects. You decide how to organize your data, and every project has different needs. Some factors to consider:

  • Simplicity. Putting everything into one big script file or one big project file is simpler sometimes.

  • Ease of writing. Writers may prefer to think in terms of one file per scene, one file per chapter.

  • Localization. 1 Yarn Project = 1 CSV spreadsheet per language. When translating, it is usually easier to work with fewer files, rather than fragmenting the translation across many files. As a workaround for games that need multiple Yarn Projects, you may prefer to create a single editor-only Yarn Project that's just for generating string files and translations. See .

Please visit the for more information. Thanks for thinking of us!

Localisations

Localization is the process of translating and adapting content to a specific language, region or culture.

Yarn scripts are written in human-readable language. This is generally a single language, and (most of the time) will be written in the language that your development team primarily speaks. The language that a Yarn project is written in is called the base language.

However, if you want your dialogue to be understood by people who don't speak this language, you will need to translate it. Yarn Spinner is designed to make it easy to extract the user-facing text of your dialogue into a strings file, which can then be translated into a different language, and then loaded at run-time. You can translate your project into as many languages as you'd like, and Yarn Spinner will handle it for you automatically.

Yarn Spinner is also designed around the idea that a line of dialogue may have assets associated with it. Most commonly, this means an audio file that contains an actor performing the line, so that it can be used in your game as a voice-over. These assets are also localisable.

  • Localisation

Dialogue Runner and a High Level Overview

The main way to actually manipulate the state of your dialog is through a DialogueRunner. You create it from a YarnProject (see ) with either YarnProject::create_dialogue_runner() or YarnProject::build_dialogue_runner(). The first uses default configurations which should be alright for many use-cases, while the latter allows you to add or change functionality.

The actual navigation through a dialog is handled by a , which is responsible for back-and-forth interaction with the player. As such, most of the methods provided by a DialogueRunner are to be called by such a view. The one you will want to call yourself, as seen in the , is DialogueRunner::start_node, which will tell the DialogueRunner to start running from the provided

.

Variables need to be stored in some place. By default, they are kept in memory through the InMemoryVariableStorage. This means that when you quit and reopen the game, all variables used in Yarn files will be empty again. Of course, this is suboptimal when you want to allow the player saving and loading their game state. To accomplish this, you can go one of two routes:

  • Manipulate the variables in the variable store. Read then when saving and write them when loading. You can access the variable storage through DialogueRunner::variable_storage().

  • Directory use a variable storage that stores its variables in a persistent way, such as a database or a file. You can change the underlying variable storage through the builder API discussed later in this chapter.

For information on how to create your own variable storage, see the chapter Variable Storage

Yarn files can contain user-defined functions and commands. These can be accessed with DialogueRunner::library() and DialogueRunner::commands(). For more information, see the chapters Custom Functions and Custom Commands.

We make a distinction between text, which are the written words organized into lines contained in Yarn files or in localization files, and assets, which are supplemental data associated with a line. Assets are referenced over a Bevy Handle and can be used for things such as voiceover sound files or images that might need translation.

Of note is that using assets requires using localization, or at least thinking about it. As a consequence, language settings are split between text and assets. After all, a player might want to hear lines delivered in the original recorded language but read the text translated into their own language.

You can read more about how current language can be set for a DialogueRunner in the localization chapter.

Text is provided by a TextProvider. While it can be overwritten, the default StringsFileTextProvider will be a good choice for nearly all users. The only reason you might have to create an own TextProvider is if you want a very custom localization strategy, such as translating text automatically through AI.

Assets are provided by AssetProviders. In contrast to the TextProvider, you might very well create your own AssetProvider. For your convenience, Yarn Spinner already ships with an AudioAssetProvider that you can use for voice lines and a FileExtensionAssetProvider that can load any asset based on naming conventions and file extensions. See the chapter Assets.

Text and asset providers can be set through the builder API and accessed later with DialogueRunner::text_provider() and DialogueRunner::asset_providers(). If you know the exact type T of AssetProvider you want, you can call DialogueRunner::asset_provider::<T>() instead.

As mentioned in the beginning of this chapter, a DialogueRunner can be modified or extended on creation by using YarnProject::build_dialogue_runner(). In fact, YarnProject::create_dialogue_runner() is nothing but a shorthand for YarnProject::build_dialogue_runner().build().

You can use the builder API to inject your own implementations of traits used for the features presented in this chapter. DialogueRunnerBuilder::with_variable_storage changes the underlying VariableStorage and DialogueRunnerBuilder::with_text_provider the TextProvider. DialogueRunnerBuilder::add_asset_provider adds an AssetProvider to the set of asset providers called for each line presented to the player.

Dialog Flow

Compiling Yarn Files
Dialogue View
Quick Start

Variable Storage

Functions and Commands

Text and Assets

Builder API

node

How do I load and save data / variables / dialogue state? (Like for a save game system)

Control flow

How do I jump to a specific node? How do I switch nodes while dialogue is running?

How do I jump to a specific line in a node?

Interaction / UI

How do I show the last line of text when options are shown? How do I skip the last line of text before a set of options?

How do I show the character name / portrait? How do I customize dialogue display?

How do I make the Line View's Typewriter effect pause on punctuation?

How do I play a Yarn node when I click / tap on an object?

How do I play a Yarn node when I approach an object and press a button? (RPG-like talking to NPCs)

How do I position a speech bubble above an NPC's head, like in A Short Hike?

System

How many Yarn files should I have? Can my entire game be in one project or script? Or one project per scene? Is my project or file too big?

Other

How do I credit Yarn Spinner in my game?

"single source of truth"
DialogueRunner::current_node
DialogueRunner.start_node()
DialogueRunner.variable_storage()
VariableStorage::variables
VariableStorage::extend()
variable storage
VariableStorage
variable_storage.rs
Nodes, Lines, and Options
DialogueRunner.start_node("nodeName")
jump to the start of a node
Custom Dialogue View
StringInfo::metadata
Dialogue View
typewriter.rs
DialogueRunner.start_node()
Localizations
Crediting Yarn Spinner page
if (player presses SPACE)
    then find the nearest NPC
    get that NPC's dialogue node name
    call DialogueRunner.StartDialogue() with the NPC's dialogue node
    disable player movement
: A set of information that describes where to find text and assets for a given language.
  • Base language: The language that your Yarn script files are written in.

  • Strings file: A text document that contains translated versions of Yarn lines.

  • Line ID: A unique code that identifies a line of dialogue or an option in the original source text.

  • Localised line: The text of a line of dialogue, in a particular locale.

  • Localised line asset: An asset (for example, an audio clip) that's associated for a particular line, in a particular locale. For example, an audio clip containing the voiceover for the line "Hello there", in German.

  • We specify our supported localizations when creating the YarnSpinnerPlugin (or using deferred compilation):

    The base localization is the language in which your Yarn files are already written. In this case, we specified that our Yarn file was written in English as spoken in the USA. The translations are all languages you want to support. Here, we want to support German as spoken in Switzerland.

    Put the code shown above into the example used in the Quick Start and run the game.

    Now take a look at your Yarn file at assets/dialogue/example.yarn. You will see that your line of dialog will contain an autogenerated ID, for example:

    This ID uniquely references this line across translations. For the sake of clarity, we will use diff highlighting throughout this chapter. In case you're not familiar with this look, for our purposes the red line started by "- " shows how the line looked like before a change, while the green line started by "+ " shows how the line looks like after the change. The "- " and "+ " are just visual indicators and not actually part of the files, so don't let that confuse you!

    You will probably also have noticed a new file in your assets that was not there before, namely "de-CH.strings.csv":

    The strings file inside the directory

    This file is called a strings file, because it contains translations of each string of text of your Yarn files. Let's see what it contains:

    Since this is a CSV, let's open it in an application that renders the content as a table:

    You can see that our line from before is in there! Notice how the id matches across the files.

    This file will be populated with new entries as soon you change the Yarn files. Assuming that you are using hot reloading as described in the setup, run your app again in case you closed it or advanced the dialog. While you are greeted with the "Hello World!" message on screen, open the Yarn file and edit it. Let's add a new line:

    Save the file while the game is still running. You should see that our new line just got assigned an own line ID:

    In case you can't see this, your editor might still have the old state of the file cached. It usually helps to change focus, tab out to another window, or closing and reopening the editor. The strings file should now also contain a new entry:

    The strings file contains more strings now.

    Let's translate some of this. Change the string "Hello World!" in this file to "Hallo Welt!", which is German, and save it:

    Some professional German translation

    The game will currently happily ignore this as by default it uses the base language, which means it will take its text straight from the Yarn files. But we can easily switch the language:

    Run the game again and you should be greeted by this text:

    Hurray! See how painless localization can be?

    Languages are specified according to IETF BCP 47. You can add as many translations as you want. Each will receive an own strings file.

    To switch languages at runtime, simply retrieve a DialogueRunner through a Bevy query inside a system. When you use DialogueRunner::set_language() as shown above, you will set the language for both text and assets. You can be more granular by using DialogueRunner::set_text_language() and DialogueRunner::set_asset_language() separately instead. This allows you to support use cases such as showing the text in the player's native language and play voiceover sound in the original recorded language, which might be a different one.

    Since assets require using localization, they are searched for in folders named after the language they support. For the example used throughout this chapter, the assets for the base localization would be searched for in assets/dialogue/en-US/, while the assets for the de-CH translation will be searched at assets/dialogue/de-CH/. This is however more a convention than a rule, as a given AssetProvider is allowed to look for its assets wherever. The asset providers shipped by Yarn Spinner will additionally expect assets to be named after the line ID they belong to. For example, the AudioAssetProvider would look for the voice line reading our "Hello World!" line at assets/dialogue/en-US/13032079.mp3 for the base localization.

    To read more about how to use assets, read the chapter Assets.

    The strings file can be freely edited by a translator in the text and comment fields. While you can translate the texts yourself, the format being straightforward allows the translator to also be someone else that is not involved with the coding part of the game at all.

    You might have some questions regarding what happens when one person edits a Yarn file while another edits the strings file. As a general rule, the strings file will try to "keep up" with the Yarn file without ever destroying anything that was already translated.

    As you've seen, new lines will be amended. If the Yarn file has a line edited, it will be changed in the strings file as well if it was not yet translated. If there is already a translation, it will be marked by a "NEEDS UPDATE" prefix in the text. If a line was deleted in the Yarn file, it will also be deleted in the strings file if it was untranslated. Otherwise, it will be left untouched.

    Bottom line: if there's a translation, it will never be removed.

    Once you want to build your game for a release, you should disable the automatic file creation and editing. To do this, add the following line to the plugin creation:

    This will change the behavior of missing translations to simply fall back to the base localization.

    While you're on it, you might also want to disable Bevy's hot reloading.

    You may have wondered what the .into()s were for in the lines at the beginning of the chapter:

    They're here because a localization is not just a string with a language code, but an entire struct, namely Localization. You can construct this struct directly the path to the strings file and where assets are searched for.

    I just want to add voiceover in a single language. Why do I need to localise, too?

    The philosophy of Yarn Spinner's approach to localisation is: if you want your dialogue to be text-only, and in a single language, you don't need to do anything at all. If you want to do anything else, you will need to set up a localisation and manage it using Yarn Spinner's processes.

    We've found that most users who want to start using Yarn Spinner want to quickly get dialogue on the screen, and don't want to do lots of work to get the basics going. That's why we make the simple use-case (text only, a single language) as easy to use as we can.

    However, if you're building a game that's voice acted, it makes your life significantly easier if you build your systems with localisation in mind from the start. Additionally, if you have the resources to add voice-over to your project, you should also have the resources to translate your game to other languages (even if you only have voice-overs in a single language.)

    To that end, we designed it so that voiceover is intimately tied to localisation, so that you have to at least start thinking about localisation at the start of the process.

    Localisation Terminology

    YarnSpinnerPlugin::new().with_localizations(Localizations {
        base_localization: "en-US".into(),
        translations: vec!["de-CH".into()],
    })
    # assets/dialogue/example.yarn
    title: Start
    ---
    - Hello World!
    + Hello World! #line:13032079 
    ===
    language,id,text,file,node,line_number,lock,comment
    de-CH,line:13032079,Hello World!,example.yarn,Start,4,7f83b165,
    # assets/dialogue/example.yarn
    title: Start
    ---
    Hello World! #line:13032079 
    + This is a brand new line of text
    ===
    # assets/dialogue/example.yarn
    title: Start
    ---
    Hello World! #line:13032079
    - This is a brand new line of text
    + This is a brand new line of text #line:10414042 
    ===
    fn spawn_dialogue_runner(mut commands: Commands, project: Res<YarnProject>) {
        let mut dialogue_runner = project.create_dialogue_runner();
        dialogue_runner.start_node("Start");
        dialogue_runner.set_language("de-CH"); // Use our translation
        commands.spawn(dialogue_runner);
    }
    YarnSpinnerPlugin::new()
    // ...
    .with_development_file_generation(DevelopmentFileGeneration::None)
    YarnSpinnerPlugin::new().with_localizations(Localizations {
        base_localization: "en-US".into(),
        translations: vec!["de-CH".into()],
    })

    Using Localization the Easy Way

    Languages

    Assets

    File Editing Workflow

    Shipping the Game

    Customization