Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
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.
Yarn Spinner for Rust is the set of components and scripts that make Yarn Spinner work inside a Bevy project.
See the Rust section in the Beginner's Guide for a quickstart into how to setup everything you need to before reading on.
In this section, you’ll learn how to work with Yarn Spinner for Rust more in-depth.
Yarn Spinner for Rust v0.2 works with Bevy version 0.13
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 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.
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.
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.
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 VariableStorage
. To implement your own, check out the documentation. Once you have it, you can use it by calling DialogueRunnerBuilder::with_variable_storage
when building your Dialogue Runner.
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.
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 YarnFileSource
s. 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 Localization 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 Localization.
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 setup.
Once you have the YarnProject
, you can use it to spawn a DialogueRunner
which in turn can, well, run dialogues
The main way to actually manipulate the state of your dialog is through a DialogueRunner
. You create it from a YarnProject
(see Compiling Yarn Files) 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 Dialogue View, 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 Quick Start, is DialogueRunner::start_node
, which will tell the DialogueRunner
to start running from the provided node.
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 AssetProvider
s. 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.
You can define your own commands, which allow the scripts you write in Yarn Spinner to control parts of the game that you've built.
Functions are units of code that Yarn scripts can call to receive a value. In addition to the built-in functions that come with Yarn Spinner, you can create your own.
Quickly get started with a simple scene.
We will now go through the steps to setup a new Bevy project running Yarn Spinner dialogues
Run the following in your terminal to create a new crate with the required dependencies:
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.
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:
You can learn about our recommended editor, Visual Studio Code with the official Yarn Spinner Extension at: Fundamentals.
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 Dialogue View 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 DialogueRunner
. To do this, we use the YarnProject
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 node 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:
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.
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: 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":
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:
Let's translate some of this. Change the string "Hello World!" in this file to "Hallo Welt!", which is German, and save it:
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.
Common questions and solutions for using Yarn Spinner in Rust using Bevy.
Yarn Spinner doesn't do text rendering, you have to use existing Bevy plugins.
Yarn Spinner doesn't handle text rendering. You'll need a separate wavy text plugin.
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.
Don't forget the $
when writing the variable's name!
It is not currently possible to save or restore the specific line that the dialogue is running.
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.
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.
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:
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.
Learn about Dialogue Views, which present dialogue content to the user.
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.
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:
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<()>, ...)
Note that YS only processes the text data. You must still code the actual markup effect yourself. See .
Wrap the variable (or any expression) in curly braces ({
, }
) to evaluate and output it. For more info, see .
To read Yarn variables from Rust, use . To write Yarn variables from Rust, use
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.
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 .
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!
While Bevy as a whole has assets, Yarn Spinner can associate specific assets with lines. These are always , such as voiceovers.
A 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 .
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 like this:
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.
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:
If you need functions that have side effects, e.g. for manipulating the game world, use instead.
Our contains many small programs showcasing individual features
has its to learn from it
The 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.
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.
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.
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 .
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 :
In contrast to , 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 .