Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Learn about the Welcome Sample, a launching point to explore some basics of Yarn Spinner for Unity.
The Welcome sample is the launching point for the Yarn Spinner samples. It doesn't cover anything in of itself, it is a quick tour of every sample.
Welcome has Capsley standing in front of a projector showing slides of each sample in the package. Each sample is clustered into one of four categories:
Basic
Presenters
Extending
Advanced

Learn about the Feature Tour Sample, which shows off many of the different capabilities of Yarn Spinner.
The Feature Tour Sample is an ideal starting point for newcomers to Yarn Spinner. It presents a series of conversations along a corridor, each demonstrating a different capability of Yarn Spinner.
Are you ready to navigate this narrative gauntlet?
Nodes, Lines and Options
and
Storylets
A Dialogue Interactible component and node-character associations
The feature tour consists of a long corridor with multiple rooms. Each room contains one or more interactive characters, with each character showcasing a specific Yarn Spinner feature. While this sample doesn't demonstrate every Yarn Spinner capability, it covers the most commonly used ones.
At the heart of this feature tour is a class, Dialogue Interactible. This component is attached to each interactive character and associates a specific dialogue node with that character. If you're curious about how a particular feature works, you can identify which node contains that feature through this component.
For example, if you want to see which node provides dialogue for a specific character, select that character in the scene and examine the Dialogue Interactible component.
In this example, we can see the node is Room4_Variables_2. Since all dialogue in this sample is contained in a single file (Tour.yarn), you simply need to open that file and locate the node to understand its implementation.



Learn about the samples we provide for Yarn Spinner for Unity.
Yarn Spinner for Unity ships with a range of samples covering common patterns for things you might want to do in your game.
To install the Yarn Spinner for Unity Samples, you'll need to have a Unity project with Yarn Spinner for Unity installed, so head over to Installation for Unity first.
Once Yarn Spinner for Unity is installed, you can install the Yarn Spinner for Unity Samples Package.
The best way to install the samples is to use the Samples button on the inspector of any Dialogue Runner, Yarn Project, or Yarn Script, or by choosing the Window menu -> Yarn Spinner -> and clicking Install Samples Package.
Clicking on this will work out the best way to install the samples depending on how you installed Yarn Spinner itself.
While the best way to install samples is the above, depending on your environment and device setup this might not be possible. In those circumstances you will need to manually install the samples, which is what this section is about.
How you manually install the Samples depends on how you installed Yarn Spinner:
The Yarn Spinner Samples are another Unity Asset Store asset.
This package is free but does have a dependancy on the main Yarn Spinner for Unity asset.
Open your browser and navigate to
Click on the "Add to My Assets" button.
Your browser should now offer a popup asking if you want to open the samples in Unity.
Once the Yarn Spinner Samples package is installed in your project, you can install each individual Sample by opening the Unity Package Manager through the Window menu -> Package Manager, and locating the samples.
To use each sample, locate the "Packages - Yarn Spinner Pty Ltd" section of the sidebar, and then the "Samples" tab.
If you cannot locate the Samples tab, ensure that you are looking at the entry for "Yarn Spinner Samples" under the "Packages - Yarn Spinner Pty Ltd" heading, not under the "Packages - Asset Store" heading (even if your copy of Yarn Spinner for Unity and Yarn Spinner Samples comes from the Asset Store).
To work with each individual Yarn Spinner for Unity Sample, click the Import button next to the Sample that you want to install, inside the Unity Package Manager. For example, to add the sample to the project, click its Import button:
You'll find the Sample in the Project pane:
You can open the Unity Scene (in this case Main) to explore the sample.
- a small scene in which a character explains Yarn Spinner's inbuilt samples and what each of the others include.
- walks through the various major features of Yarn Spinner.
- demonstrates the creation of pools of lines or nodes which can be drawn from based on current game state to deliver dynamic, contextual content.
- demonstrates basic customisation of dialogue views with a custom font, view background, and continue button texture.
There's a lot of new features for storylets and saliency in Yarn Spinner. Learn about them!
Storylets and Saliency are a new feature of Yarn Spinner 3 that has quite a few different pieces to making it work. Because of this we have multiple samples dedicated to demonstrating these.
The samples are not about the concept of storylets and saliency, if you are new to this and wondering what this all means, then we highly recommend our Storylets and Saliency Primer.
The samples assume you are comfortable now with both the concepts of storylets and salient content and writing them in Yarn Spinner.
There are three samples around using storylets and saliency in Yarn Spinner for Unity:
Basic Storylets and Saliency goes over the basics of using storylets in your games
looks at how you can create custom saliency strategies
demonstrates using storylets and interpolation together to build entire dynamic stories and events.
The asset store will now bounce you back to Unity and start installing the samples.
The Yarn Spinner Samples are available on Itch as part of the Yarn Spinner for Unity asset.
Open your browser and navigation to https://yarnspinner.itch.io/yarn-spinner
At the top of the page you should see a little section telling you you own the asset and offering a "Download" button.
Click on the "Download" button and it will take you to the Download page.
Click on the "Download" button for the second bundle and save it some where convenient.
Open the downloaded unitypackage and import the package into your Unity project.
Inside Unity open the package manager from the menu Windows -> Package Manager
In the top left corner press the + button and from the dropdown that appears select Install package from Git URL
In the textfield that appears enter: https://github.com/YarnSpinnerTool/YarnSpinner-Unity-Samples.git
Unity will now install the samples project after a little while. Once it is done you can now browse and install the samples directly from the package manager.
Select the samples package in the middle column and click on the Samples tab in the main window.
And just like that you can now explore the samples!
Inside your browser navigate to the samples site: https://github.com/YarnSpinnerTool/YarnSpinner-Unity-Samples
Click on the Code button and from the dropdown select the Download Zip option
Save this zip somewhere useful and unzip it
This folder now contains the samples package and it's time to add it to your Unity project.
Back inside Unity open the package manager from the menu Windows -> Package Manager
In the top left corner press the + button and from the dropdown that appears select Install package from disk
Navigate to where you downloaded and unzipped the samples and select the package.json file inside that folder
Unity will now install the samples project after a little while. Once it is done you can now browse and install the samples directly from the package manager.
select the samples package in the middle column and click on the Samples tab in the main window.
And just like that you can now install and explore the samples!
Create a Phone Chat View - demonstrates more elaborate customisation of dialogue views such that they are styled like a text message conversation on a phone screen. This includes how to add lines or options to the screen when a new line is received, as opposed to the default view which replaces the previous line or options.
Make Options That Timeout - demonstrates creation of custom options view and behaviour such that the player is given only a limited amount of time to choose once dialogue options are presented.
Voice Over and Localisation - demonstrates localising a dialogue view
Background Chatter - demonstrates the use of multiple Dialogue Runners to allow different types of NPC background conversations simultaneously with each other or during primary dialogue.
Inline Events - demonstrates the use of Action Markup to insert command-like triggers in the middle of dialogue line delivery.
Replacement Markup - demonstrates the use of Replacement Markup to insert text styling or dynamic content into dialogue lines as they are presented.
Custom Saliency Strategies - demonstrates the creation of a custom way to score and choose between content in node or line groups at runtime.
Advanced Saliency - demonstrates the use of node groups, line groups, and dynamic line content together to make a fully dynamic scene.














Learn how to implement a custom saliency strategy for your narratives.
Yarn Spinner 3 introduced a new system for selecting which content should be presented next, called Saliency Strategies. Whenever it's time to select the next piece of salient content from node groups or line groups, Yarn Spinner consults its saliency strategy to determine which piece should be chosen.
This guide explores an implementation provided in our Custom Saliency Strategies Sample.
While we provide several built-in saliency strategies that cover common scenarios, we can't anticipate every possible need. That's where custom strategies come in, which is the focus of this sample. We'll demonstrate how to create a new custom saliency strategy to meet specific requirements.
Creating new saliency strategies through the IContentSaliencyStrategy interface
Reading line metadata
Reading node headers
The IContentSaliencyStrategy interface requires implementing just two methods: QueryBestContent and ContentWasSelected.
QueryBestContent: Called when Yarn Spinner asks, "If I were to run this block of content, what would be selected?"
ContentWasSelected: Called when Yarn Spinner informs the strategy, "This specific piece has been chosen"
When Yarn Spinner needs to select a piece of salient content, it first asks its strategy what content would be selected (QueryBestContent) and then selects it and informs the strategy of this selection (ContentWasSelected).
You might wonder why this functionality is split into two methods, especially when the first call is often immediately followed by the second. For many saliency strategies, it won't matter - querying and selecting can effectively be the same operation, with the only required state being the list of content to analyze.
However, other strategies require maintaining state that would be affected if selection and querying were combined. For example, the built-in "Best Least Recently Viewed" strategy tracks which content it has shown previously to avoid showing the same piece of content twice in a row. Each time it selects content, it marks it as seen, which deprioritizes that specific content for future selections.
This design creates a clear separation: QueryBestContent is non-mutating, while ContentWasSelected can modify state. The alternative would be to prevent querying available content altogether, but this would be too limiting - it would prevent you from checking whether content is available, which might be useful for showing indicators that an NPC has something to say.
The sample includes a custom saliency strategy in WeightedSaliencySelector.cs. This strategy assigns a custom weight to each storylet, as specified by the writer, and uses these weights to determine the probability of each storylet being shown.
Each piece of salient content is given a range proportional to its weight, and then one is chosen randomly from the combined range of all weights. In practical terms, given the following line group:
"I am line A" will be shown approximately two-thirds of the time, while "I am line B" will appear approximately one-third of the time.
When asked for the best content, the strategy first filters out any content with failing conditions. After this filtering, we're left with content where all conditions (line conditions or when headers) have evaluated to true. The next step is to determine the weighting for each piece of content.
Our weighted saliency strategy supports both line groups and node groups, which require slightly different approaches:
For node groups, we look for the weight header in the node headers:
For line groups, we look for the weight tag in the metadata:
Once we have the string values for weights, we convert them to integers. If the conversion fails, or if there isn't a weight value in the headers or metadata, we assign a default weight of one.
With the weights established, we build a list of ranges representing those weights. Finally, we generate a random number within the combined size of all ranges and use that to select which content to present.
Learn how to trigger events using markup, inline with your dialogue.
Yarn Spinner version 3 introduces a powerful new way to trigger events during dialogue presentation. While previous versions required custom implementations for in-line actions, v3 provides the IActionMarkupHandler interface. This system notifies handlers about key line presentation events, enabling you to respond at specific moments.
This approach offers tremendous flexibility for creating various inline events. Our sample demonstrates two practical applications: character movement and facial expression changes during dialogue presentation.
This guide explores the contents of the Inline Events Sample.
The new IActionMarkupHandler interface
The new ActionMarkupHandler class
Using these with the Line Presenter to trigger events
Two implementations: character movement and animation changes
The IActionMarkupHandler interface provides a structured approach to handling in-line actions during dialogue presentation. At its core, the system works by having the Line Presenter display dialogue one character at a time, notifying handlers throughout the process. This creates multiple opportunities to trigger events at precise moments.
The interface defines methods corresponding to different stages of the line presentation lifecycle:
Initial Preparation (OnPrepareForLine): Called immediately after the line is received but before any UI elements appear.
Display Start (OnLineDisplayBegin): Called just before characters begin appearing, but after UI elements are visible.
Character Display (OnCharacterWillAppear): Called each time a character in the line is about to be displayed.
Notably, OnCharacterWillAppear is an asynchronous method. The line presentation system will await each Action Markup Handler's completion before continuing, allowing events of any duration to occur at any point in the dialogue.
Yarn Spinner also provides ActionMarkupHandler, a MonoBehaviour implementation of this interface, for situations where you need component-based functionality. The Line Presenter includes a serialized field for directly connecting ActionMarkupHandler subclasses, as demonstrated in our sample.
Our sample showcases two types of action markup: moving the player character and changing NPC facial expressions. Both are implemented as ActionMarkupHandler subclasses and connected to the Line Presenter through its Event Processors field.
The movement implementation resides in the MoveEvent.cs file as an ActionMarkupHandler subclass. Most of the functionality is contained in the OnPrepareForLine method.
This method scans all markup in the current line, searching specifically for the move markup tag. When found, it extracts the name property from the marker and uses it to locate a corresponding in-scene marker with the same name. The target location and the marker position within the text are stored in a dictionary for later use.
During line presentation, the OnCharacterWillAppear method checks if the current character position corresponds to a stored location. If so, it moves the player character to that position before continuing.
This allows for dialogue lines such as: Player: So what, just part way through I stop talking... [move name="far" /] ...and move on over?
When presentation reaches position 49, it will pause, the player character will move to the GameObject named "far", and then the dialogue will resume.
The facial expression system is implemented in EmotionEvent.cs as another ActionMarkupHandler subclass. The primary logic is also in the OnPrepareForLine method.
This implementation first uses the character name from the dialogue line to find a corresponding GameObject. It then retrieves the SimpleCharacter component (a sample-specific type, not part of Yarn Spinner core) from this GameObject. After these preparations, it processes all markup in the line, looking specifically for emotion tags. The emotion property value is extracted and stored for later use.
During presentation, OnCharacterWillAppear checks if there's a cached emotion corresponding to the current position. If found, it uses this value to change the character's facial expression animation. Unlike the movement example, this change happens instantly without pausing dialogue progression.
This enables dialogue lines like: Alice: Yes... which I would have shown [emotion="angry" /] had you not interrupted me.
When presentation reaches position 32, the facial expression of the "Alice" GameObject will instantly change to whatever animation corresponds to "angry" while the dialogue continues uninterrupted.
Learn how to visually theme one of our provided Dialogue Presenters.
The default Dialogue Presenters that come with Yarn Spinner have their own specific look, but they're designed to be re-themed without requiring any custom code.
The Theming Default Presenters sample and guide demonstrates how to modify the built-in line and options presenters to use a sliced sprite and different font for a complete visual overhaul—all without writing any code.
Modifying existing dialogue presenters
Learn to add voice and localisation to your Yarn Spinner-powered projects.
Dialogue in games often extends beyond simple text on screen. Many projects incorporate voice acting alongside written dialogue, and sometimes even more complex assets. This raises important questions about handling localisation—as the number of assets per line increases, so does the complexity of ensuring everything works together seamlessly.
This sample demonstrates Yarn Spinner's approach to localisation, asset association, and audio playback in a cohesive system.
The scene features a character floating in space, conversing with a disembodied voice. The main character's dialogue includes synchronized audio that drives lip movement animations. Unlike most samples, this one includes two identical scenes that differ only in their localisation implementation: one uses Yarn Spinner's built-in localisation system, while the other leverages Unity's Localisation package.
Associating and playing audio with dialogue lines
=> I am line A #weight:2
=> I am line B #weight:1weightString = runner.Dialogue.GetHeaderValue(element.ContentID, WeightKey)var lineKey = WeightKey + ':';
foreach (var metadata in runner.YarnProject.lineMetadata.GetMetadata(element.ContentID))
{
if (metadata.StartsWith(lineKey))
{
weightString = metadata.Substring(lineKey.Length).Trim();
break;
}
}Display Completion (OnLineDisplayComplete): Called after all characters in the line have been shown.
Dismissal (OnLineWillDismiss): Called just before the line will be dismissed.
Importing custom fonts into TextMeshPro
The nightmare hellscape that is Unity UI
This guide walks you through the creation of a project we provide, completed, as a sample.
If you haven't already installed Yarn Spinner, follow the instructions at Installation for Unity. Once installed, we'll start by building out a basic scene:
Make a new scene in Unity.
From Samples/Shared Assets/Prefabs drag the Basic Arena into the scene
From Samples/Shared Assets/Prefabs add a Camera Rig into the scene
From Samples/Shared Assets/Prefabs drag a Player prefab into the scene
From Samples/Shared Assets/Prefabs drag an NPC prefab into the scene and rename it to be Alice
Add a default dialogue system to the scene, in the Hierarchy right click Yarn Spinner -> Dialogue Runner
Create a new Yarn Spinner project Assets -> Create -> Yarn Spinner -> Yarn Project and name it Rethemed Dialogue
Create a new Yarn script Assets -> Create -> Yarn Spinner -> Yarn Script and name it Alice
Delete the previous camera called Main Camera
Replace the contents of the Alice Yarn script with the following:
This is a very simple node with just a few lines of dialogue and a single option. All we need is to show off the main features of lines and options, so feel free to change it, but for now this is sufficient.
Select the Camera Rig and in the inspector drag the Player into the target field
Select Alice and in the Inspector set the Dialogue field to use the new Rethemed Dialogue project
Select Alice and in the Inspector select the Alice node in the dropdown
Select Alice and in the Dialogue Runner field drag in the Dialogue System object from the hierarchy
Select the Dialogue System and in the Inspector set the Yarn Project to use the Rethemed Dialogue project
Arguably the easiest but most impactful change you can make is to use a different font. This will immediately make the UI look different with minimal effort. Selecting a font is a very important stylistic choice for your game, as it will influence everything about how you design the rest of your UI.
For this sample, we've chosen a pixel font that works well with our desired aesthetic. At this stage, we're just adding the font to the project; we'll connect it to the various presenters later.
Grab the PixelifySans font from Google Fonts.
Add the font into the Project assets
Go to Window -> TextMeshPro -> Font Asset Creator
Drag the PixelifySans-Bold font into the Source Font field
Click Generate Font Asset
Click Save and save the TMP version of the font into your project
Repeat these steps for the PixelifySans-Regular font
With this done, our fonts are ready to be used. We'll return to them when we start making layout changes, but first we also need some sprites.
Our retheming will rely on a sprite sheet that we'll slice. Let's set that up now. We'll need the Sprite Editor for slicing, which you might already have installed.
Open the Package Manager by going to Window -> Package Manager
In the side bar select the Unity Registry option
Wait for Unity to populate the manager
Find the package called 2D Sprite
Install it
Download the sprite sheet, and add it into the project.
Select the sprite sheet and in the Inspector change it's Texture Type to be Sprite (2D and UI)
Set the sprite mode to be Multiple
Set the Pixels per Unit to be 20
Set the Filter Mode to be Point (no filter)
Click Apply
Select the sprite sheet and in the Inspector click on the Open Sprite Editor button
Now we need to slice the sprite sheet into individual sprites and define the regions. Without doing this, the sprites would stretch in undesirable ways and look distorted. We need to identify and slice eight different sprites, which is somewhat tedious to describe, but thankfully the Sprite Editor makes the process relatively straightforward.
In the top bar of the editor, in the Slice dropdown configure it to be an Automatic slice with a Center pivot and click the Slice button
This does most of the work for us, using the transparent space in the sprite sheet to identify the eight different sprites. Now that we have the individual sprites identified, we need to define which regions can stretch and which cannot. Without this step, the sprites will scale oddly.
Select the left-topmost sprite
Name it Button-Filled-Up
Set it's L, R, T, and B to all be 7
Select the sprite on the right of the Button-Filled-Up sprite
Name it Button-Empty-Up
Set it's L, R, T, and B to all be 7
Select the sprite on the right of the Button-Empty-Up sprite
Name it Button-Continue-Up
Set it's L, R, T, and B to all be 7
Select the sprite on the right of the Button-Continue-Up sprite
Name it Option-Selected
Select the sprite below the Button-Filled-Up sprite
Name it Button-Filled-Down
Set it's L, R, T, and B to all be 7
Select the sprite on the right of the Button-Filled-Down sprite
Name it Button-Empty-Down
Set it's L, R, T, and B to all be 7
Select the sprite on the right of the Button-Empty-Down sprite
Name it Button-Continue-Down
Select the bottom-leftmost sprite
Name it Background
Set it's L, R, T, and B to all be 16
Click the Apply button
Now all our sprites are neatly sliced and ready to use. This was quite a bit of slicing, but fortunately most sprites use the same slice settings. If you don't want to do all this work or made a mistake, you can grab the pre-sliced version from the Sample folder. What we've accomplished here is defining which parts of each sprite can stretch and which parts should remain fixed.
Next, we'll modify the Line Presenter to use our custom sprites instead of the default visuals.
Select the Dialogue System prefab in the Hierarchy
Expand out the Canvas -> Line View and select the Background gameobject
In the Image component of the Inspector change the Source Image to be our freshly sliced Background sprite
In the Image component of the Inspector change the Color to be fully opaque white
In the Image component of the Inspector change the Image Type dropdown to be Sliced
In the Hierarchy select the Line View game object
In the Vertical Layout Group component set the Left and Right padding to be 40, the Top to be 24, The bottom to be 115 and set the Spacing to be 20
Now we need to make the line presenter use our custom font.
Select the Text Mesh Pro field inside of Dialogue System -> Canvas -> Line View -> Character Name
In the Inspector on the Text Mesh Pro component change the Font Asset field to point to the Pixelify-Sans font
Set the font size to be 48
Select the Text Mesh Pro field inside of Dialogue System -> Canvas -> Line View -> Text
In the Inspector on the Text Mesh Pro component change the Font Asset field to point to the Pixelify-Sans font
Set the font size to be 50
Expand the Extra Settings section and find the Margins
Set the margins to be Left 20, Top 0, Right 20, and Bottom 0
With that, our line presenter is finished! We can test it to see how it looks in action.
We're halfway there! Now we need to apply similar changes to our options presenter. This has a slight additional step—we'll need to make a prefab for our option items—but otherwise the process is similar to what we did for the line presenter. Let's start by creating the options item prefab, which requires the most work.
Right click on the Options Presenter Dialogue Systemm -> Canvas -> Options View and add a new UI -> Text - TextMeshPro child game object
Rename it to be Option Item
Select the Option Item and in the Inspector find the Text Mesh Pro component
Change the Font Asset to point to Pixelify-Sans font
Set the Font Size to be 50
Expand out the Extra Settings
Set the margins to be Left 30, and Top, Right and Bottom to all be 0
Add a new Image child game object to the Option Item
Rename it to be called Selection Indicator
Make it so it is a full height stretch but with a fixed width of 20
Select the Options Item in the Hierarchy
Add an Options Item component onto the game object
In the Text field drag the Option Item game object in
In the Selection Image field drag in the Selection Indicator game object
Expand out the Selected section of the inspector
Set the Selected sprite to be the Option-Selected sprite
Drag the Option Item game object out of the Hierarchy into the Assets to make it a prefab
Delete the existing Option Item game object, it has served it's purpose
With that done, our final steps are to modify the Options Presenter to use our new Option Item.
Select the Options View game object in the Hierarchy
In the Inspector find the Options View Prefab field inside the Options Presenter component
Replace the existing prefab with our Option Item prefab we made above
Select the Background child game object of the option presenter
In the Inspector find the Image component
Set the Source Image to be our Background sprite
Set the Color to be fully opaque white
Set the Image Type to be Sliced
Select the Last Line child game object
In the Inspector on the Text Mesh Pro component change the Font Asset field to point to the Pixelify-Sans font
Change the Font Size to be 48
Set the Font to be using Bold
Expand the Extra Settings and change the Margins to be Left, Right and Top to be 0, and set Bottom to be 14
And with that, our Options Presenter is also rethemed!
With these straightforward changes, we've completely transformed the look of the default dialogue presenters without writing any code. Hopefully, you now feel confident in your ability to customize the visuals of built-in presenters. Take your newly styled dialogue system for a spin and see how it enhances your game's visual identity!

title: Alice
---
Player: Hello
Alice: Hello, this is a sample showing off retheming the base dialogue presenters
-> Neat
Alice: right?
-> Dull
Alice: rude!
===Understanding the Built-in and Unity Localisation line providers
Localising both text and associated assets
The line provider is the component responsible for managing assets associated with dialogue. It supplies the content of each line—both text and related assets—to the various presenters in your game. Any object that implements the ILineProvider interface can serve as a line provider, and the dialogue runner consults this provider when it's time to present a line.
The dialogue runner itself doesn't know the actual content of lines—it only knows which line to run via the line's ID. When a line needs to be displayed, the line provider retrieves the content from the strings table, parses any markup, and collects any associated assets. If you need custom modifications or behaviors that apply to multiple lines, creating a custom line provider is generally the most effective approach.
Yarn Spinner includes two line providers out of the box:
The built-in localised line provider (BuiltinLocalisedLineProvider) for projects without specific localisation workflows
The Unity localised line provider (UnityLocalisedLineProvider) for projects using Unity's localisation system
While these providers function differently internally, they present the same interface to Yarn Spinner, so you can generally use whichever best fits your workflow without worrying about the implementation details.
When the dialogue runner passes a line to your presenters, the LocalizedLine includes an optional Asset property that can contain any Unity Object (or null). Asset association is based on line ID, connecting relevant resources to specific dialogue lines.
Built In Localisation
The built-in localisation system requires two components for each language:
String Table: Essential for displaying lines. The base localisation (typically English) is automatically configured.
Assets Folder (optional): Contains resources for that localisation.
The system searches the assets folder and matches assets to line IDs in the project. For this sample, we have a folder named LipSync-en containing the lip-sync data for English localisation.
CALLOUT: This isn't a lip-sync sample so for now they know it's a scriptable object that conforms to the IAssetProvider interface. It has a list of visemes and timeframes for when to show that viseme. It also has a reference to the audio file that this lip-sync event is connected to. For more details check out LipSyncedVoiceLine.cs.
The Yarn Project has this folder connected as the Assets folder for the English locale:
This connection ensures that when a presenter requests an asset (as the voice over presenter does), it receives the lip-sync asset with the same name as the line ID.
Unity Localisation
The Unity Localisation approach differs significantly. Instead of referencing files on disk, it uses tables to manage content. The sample scene includes preconfigured string and asset tables, which the Unity Localisation Line Provider references.
Rather than maintaining a list of locales, the project simply references a table collection:
Similarly, the Unity Localisation Line Provider doesn't configure the current language—it simply references the necessary string and asset tables:
The base locale's string table column automatically populates with values from the Yarn files. For additional languages, you'll need to add content to the table manually, typically using one of Unity's importers. This differs from the built-in localisation, where you manually associate a string table in the project.
Asset association also works differently with Unity Localisation. Since asset tables handle this functionality, Yarn Spinner provides a wizard accessible via Window -> Yarn Spinner -> Add Assets To Table Collection:
This wizard automatically detects locales from global settings. You'll need to:
Connect the asset table to the Asset Table Collection field
Set the asset type
Drag asset folders for each locale into the appropriate slots
The system still matches assets to lines based on filename-to-line-ID mapping. After running the wizard, you can verify the configuration by examining the asset table:
At runtime, the Unity Localisation line provider uses these tables to find both text content and associated assets (like lip-sync data) for each line. The presenters receive complete, ready-to-use lines without needing to understand how the provider works.
IAssetProvider
The IAssetProvider interface addresses the challenge of handling unknown asset types. Default presenters (including the voice over presenter) are designed to work both independently and in combination, so they can't make assumptions about what kinds of assets they'll receive.
While most often the asset will be an audio file, many situations require something more complex. In this sample, we use a custom LipSyncedVoiceLine scriptable object that implements IAssetProvider. This object contains both an audio file and viseme timing data.
Through the IAssetProvider interface, different presenters can request specific asset types in a consistent way without worrying about implementation details. Conforming to this interface requires implementing just two methods: TryGetAsset<T> and GetAssetsOfType<T>.
If you're not using default presenters and don't need interoperability between assets and views, you can use whatever asset types you prefer. However, if you want compatibility between custom and default presenters, implementing IAssetProvider is highly recommended.
The remaining components of the sample are more straightforward, as the localisation and line providers handle most of the complexity. Two presenters are particularly relevant:
This presenter requires an audio asset to function. It first checks if the line has an associated asset, and whether that asset is audio. If so, it plays the audio directly.
If the asset isn't audio but implements IAssetProvider (as in this sample), the presenter requests an audio asset from the provider and then plays it. This demonstrates the primary purpose of the IAssetProvider interface—allowing different types of data to be bundled into a single container.
This component has a more straightforward implementation. Since we know the asset type in this sample, we can skip some checks and directly request lip-sync timing data from the IAssetProvider. With this data, the component modifies the character's face to match the appropriate visemes at the correct times during audio playback.
Learn how to build and use an advanced saliency system when you use Yarn Spinner for Unity.
There have been several guides and samples about saliency and storylets in Yarn Spinner 3, so why not add one more!?
While previous samples have deliberately limited the amount of dynamic content, the Advanced Saliency Sample explores what happens when you want highly dynamic content within the already dynamic nature of storylets themselves.
Dynamically building up Yarn content
Dynamic selection of Yarn content







Changing a scene on the fly in response to Yarn value changes
Challenges in writing content for these types of games
Unlike other samples, we need to provide some context before diving into how everything works. This sample demonstrates building a room where the characters, scenario, and even object layout are fully dynamic based on values set in Yarn. This approach is ideal for games where you have a team of characters accompanying the player or side-quests featuring previously encountered characters and locations, making the world feel more interconnected and responsive to player choices.
The structure for this sample includes four different scenarios: interrogation, exploration, rescue, or a date. Each scenario has two main non-player characters (primary and secondary), who can be any of four NPCs: Alice, Barry, George, or Liz. These scenarios can take place in one of four rooms: an office, pub, church, or mansion.
Even in this limited sample, there are up to 192 possible combinations - far too many to write individually for just four different scenarios. The situation becomes worse when you consider that each scenario likely needs multiple nodes, creating an enormous writing burden.
As you add more scenarios, characters, and locations, the combinatorial explosion quickly outpaces even the fastest writers. To address this, we template the scenarios and use interpolation to inject different values into these templates. While replaying the same scenario repeatedly might reveal patterns, in a typical playthrough where each scenario is seen only once or twice, the game appears fully responsive to the player's actions.
This approach works particularly well for games with varied side quests that need to adapt to the player's situation. Games like Watch Dogs: Legion and Weird West use similar techniques to populate their worlds with highly reactive missions. While you could apply this approach to a main storyline, it's less common for a game's primary path since it sacrifices some of the control that's valuable for telling a specific story.
Using interpolation and templated scenarios offers two additional benefits:
You can still write highly specific scenarios alongside templated ones. For instance, you could craft detailed content for Alice and Liz exploring a mansion without writing all 191 other combinations.
It facilitates easier scaling, allowing different writers to handle specific scenarios without interfering with others' work. More niche combinations can be assigned to writers who best understand those characters, enabling more straightforward assignment and scaling of writing tasks.
The Advanced Saliency Sample consists of several components: an initial configuration area where you can speak with Capsley for an explanation and interact with four buttons to configure the room, and a main room where the scenarios play out.
Each button updates a specific Yarn variable. These variables control who the primary character is ($primary), who the secondary character is ($secondary), what scenario will be played out ($scenario), and what room it will take place in ($room).
Walking onto a button triggers an update to the relevant variable that the button controls. This allows you to configure the scenario details, participants, and location.
In the distance lies "the void," where characters not needed for the current scenario wait. While these characters could be instantiated from prefabs, we originally designed the sample differently, requiring all characters to exist in the scene simultaneously. Although we later changed this approach, we found the notion of characters waiting on a distant island amusing, so we kept it.
The level god (LevelGod.cs) handles loading room props and placing characters. It manages a collection of room layouts (defined as scriptable objects) that specify prop placement, level content, and character positions. When it's time to run a scenario, the level god checks the variable storage, reads the current scenario, and loads the appropriate room layout. It then positions characters according to the layout specifications.
In an actual game, room geometry would likely be part of the general level design rather than defined in a configuration file. However, you would still need configuration for different scenarios, such as character placement and required props. You'd need some version of a "level god" to interpret this configuration and handle runtime setup.
Capsley is responsible for triggering the scenario. Speaking with Capsley runs the final setup code needed to load the scenario, after which the primary and secondary characters take over.
This sample contains more dialogue and uses significantly more interpolation than typical Yarn scripts. There's one main file (room.yarn) that sets up variables, contains Capsley's dialogue, and all bouncer nodes. Each scenario has its own file (room-Date.yarn,room-Exploration.yarn,room-Rescue.yarn,room-Interrogation.yarn), and there's an additional file demonstrating how to implement specific variable combinations (room-Explore-Mansion-Liz-Alice.yarn) alongside more generic ones.
We've organized the dialogue into two main node groups: Primary and Secondary, corresponding to the specific characters in those roles. These are accessed via bouncer nodes that apply across the entire sample rather than on a per-scenario basis. For example, if Alice is the Primary character, her bouncer node would be:
Perhaps the most noticeable difference from typical Yarn files is how speaker names are handled. Usually in Yarn, you define your speaker with their name followed by a colon, but here the speaker names are interpolated values. This addresses the issue of not knowing which character will speak a line, only that it will be either the primary or secondary character.
When this Yarn code runs, the names of characters in the $primary and $secondary roles are interpolated into the lines. If Alice is Primary and Liz is Secondary, this would be equivalent to:
This works because the enums have been given string backing values matching their character names.
With two node groups (Primary and Secondary), we need substantial filtering to prevent inappropriate storylets from appearing. The scenario serves as the main filter for node groups. To maintain clarity, each scenario has its own file, and every node in that file begins with the same filtering when clause. Additional clauses then track progression through the scenario. For example, the first node in the interrogation scenario when talking to the primary character has this header:
This isn't the only possible approach, but it's the most straightforward without requiring extensive custom code or limiting expressiveness. These combinations can become arbitrarily complex to accommodate highly specific situations:
These when clauses specify a scenario that's an exploration in a mansion with Alice and Liz as the main characters, where the scenario has started but the player hasn't spoken to the secondary character yet. While highly specific, breaking it into smaller pieces makes each condition manageable and understandable.
Unlike most samples intended as starting points for implementing Yarn Spinner features, this sample has limitations. Although it demonstrates a working approach to advanced, dynamic story saliency, it's more restricted than what you'd want in a full game. These limitations primarily exist to keep the sample approachable as a starting point.
As you use storylets more extensively, their structure will increasingly reflect your game's needs. Even this relatively simple sample mirrors the structure of its scene: the story is told by two characters following a consistent progression through the scenario because that's all the sample supports. While this sample provides a good starting point for thinking about these concepts, you'll need to make decisions about storylet structure, information access, and flow in your own implementation.
The progression options here are deliberately limited to keep the sample manageable. Progression follows a single path, which would be too restrictive for a full game. Each scenario in a real game would need its own progression system and a way to easily query its state, likely through a custom function.
The writing approach used for storylets and character name interpolation works adequately but can become tedious in larger projects. Writing {$primary}: and {$secondary}: at the start of lines is functional but potentially frustrating over time. In a larger project, you might want to use a dedicated line provider or custom view code to simplify this, allowing for something like:
While not a major improvement, this simplification adds up as your game scales.
Related to this is localization, which becomes considerably more challenging with heavy use of interpolation. The most important consideration is ensuring interpolated values undergo localization. In English, structures like The {$room} is on fire work without issue, but in languages where articles must agree with nouns, separating them causes problems. You'll likely need to process values through a localization function before injecting them rather than using direct interpolation.
Finally, we've overlooked how storylets are selected. A real game would need some form of drama manager to read the game state and configure scenarios appropriately. In this sample, we've simplified this with four buttons that act as a drama manager - understandable but not viable for a shipping game. Similarly, we use bouncer nodes to avoid rewriting dialogue interaction code, but in a larger project, it would make more sense to update each character to load the appropriate node group directly rather than bouncing through empty nodes.
title: Alice
when: $primary == .Alice
---
<<jump Primary>>
==={$primary}: date time!
Player: Date Time!
{$secondary}: DATE TIME!
{$primary}: why are you here?!Alice: date time!
Player: Date Time!
Liz: DATE TIME!
Alice: why are you here?!title: Primary
when: $scenario == .Interogation
when: $scenario_state == .NotStartedtitle: Primary
when: $scenario == .Explore
when: $Room == .Mansion
when: $primary == .Alice || $primary == .Liz
when: $secondary == .Alice || $secondary == .Liz
when: $scenario_state == .Started
when: $speak_to_secondary == falseA: date time!
Player: Date Time!
B: DATE TIME!
A: why are you here?!Learn how to use markup in your Yarn Spinner Scripts, and respond to it in Unity by styling your narrative's text.
Yarn Spinner 3 introduced a new system for performing in-line text replacements using markers. Previously, text replacement based on markers was entirely handled by the view displaying the line. In Yarn Spinner 3, we've implemented a cleaner approach with IAttributeMarkerProcessor and the related ReplacementMarkupHandler.
This guide explores the implementation of this in our Replacement Markup Sample.
These components connect directly to the line provider, allowing replacements to occur before the line reaches the views. This simplifies your code architecture and creates a clearer separation of responsibilities. This sample demonstrates four different applications of replacement markup:
Built-in common replacements and marker palettes
Dynamic markup that changes with each presentation to obscure text
Named replacement for in-line content highlighting
Injecting sprites and additional text content into a line
Using Replacement Markup through IAttributeMarkerProcessor and ReplacementMarkupHandler
Markup Palettes and the built-in replacement processors
Dynamically replacing text
The IAttributeMarkerProcessor interface is the primary mechanism for creating replacement markup. At its core, replacement markup works by registering with the line provider to handle specific markup tags. During line presentation, when the dialogue runner requests a line from the line provider, the provider calls its registered IAttributeMarkerProcessor instances to process the line.
Whenever a line provider encounters markup for which it has a registered processor, it calls the ProcessReplacementMarker method on that processor. This method receives four parameters:
marker: The marker being processed, containing position, range, and properties
childBuilder: A StringBuilder containing the text content of all child nodes, which you'll typically modify by adding text to the beginning and end
childAttributes: A list of markup inside your markup's children, which you can modify, offset, or even delete as part of your replacement
The method returns a list of LineParser.MarkupDiagnostic objects, which identify any unresolvable errors in the markup. These diagnostics can be displayed in the editor, facilitating easier debugging of invalid markup.
For convenience, we provide an abstract class ReplacementMarkupHandler, a MonoBehaviour that implements the IAttributeMarkerProcessor interface. This class includes a static ReplacementMarkupHandler.NoDiagnostics list for when your processor completes successfully without errors.
Understanding how markup processing works internally can be helpful when creating your own replacement markers. Consider this line in Yarn:
We would expect three markers:
b marker at position 8, with a range of 7
b marker at position 21, with a range of 18
i marker at position 21, with a range of 4
Yarn Spinner first models this as a tree, nesting the i marker inside the second b marker:
This tree structure simplifies manipulation for replacements without manual index and range adjustments. If markup is incorrectly nested, we can rewrite the tree to maintain a valid structure while preserving the same ranges.
To flatten this tree while performing replacements, we traverse it depth-first. When we encounter a markup node with a registered replacement handler, we process it and continue to the next sibling. This ensures that when a replacement processor is called, all of its children have already been processed.
A key benefit of this approach is that each replacement processor can assume its indices start at 0. Since all children have been processed, and the root node handles final assembly and position offsetting, you don't need to know your exact position in the tree. You can trust that your children have been handled, and your siblings' positions will be managed later during final assembly.
As we traverse back up the tree, each completed branch has its siblings' text and markup attributes merged, creating a progressively flattened line.
Once the root node is processed, all replacements are complete and the tree structure becomes irrelevant. The root node simply appends all its children's text into a single line, merges all markup into a single list, and delivers it to the dialogue runner for presentation.
The sample scene includes four different examples, each NPC demonstrating a different aspect of replacement markup.
This example showcases the two built-in markup replacement systems - the only ones in this sample that don't require custom code. It uses markup palettes for basic line styling and the style replacement system for TextMeshPro (TMP) styles.
The Markup Palette provides a straightforward way to add styling to lines. It's a scriptable object that connects a marker name to styling information, allowing you to write Yarn like: Player: Hello, I want this to be [fancy]important looking[/fancy].
Which translates to TMP tags: Player: Hello, I want this to be <color=#00ff00><b>important looking</b></color>.
Markup Palettes support common styles like color, bold, and italics, or you can define custom start and end tags, including offset information for child attributes. You can create your own palettes from Assets -> Create -> Yarn Spinner -> Markup Palette. The dialogue system prefab includes common tags like [b] for bold, [u] for underline, [i] for italics, and [s] for strikethrough.
The code that processes markup palettes is in PaletteMarkerProcessor.cs. It works by creating the appropriate TMP rich text tags based on the palette, prepending opening tags to the childBuilder parameter, and appending closing tags at the end.
The other built-in replacer handles TMP styles. This adds support for any styles defined in a TMP stylesheet. Since we don't know style names in advance, the Yarn syntax looks like: Player: Hello, I want this to be [style=h1]important looking[/style].
The code in StyleMarkerProcessor.cs translates this into appropriate <style> TMP tags.
This example demonstrates using markup with variables to dynamically obscure text. Each time you talk to the character, the amount of obscured text decreases until you can understand the entire line. The obscurity level changes via a Yarn variable interpolated into the markup:
The implementation is in ObscurityMarkupProcessor.cs, which randomly replaces characters with punctuation symbols based on the obscurity level. The processor retrieves the current level from the marker:
In this sample, we defined four obscurity levels: fully obscured, two-thirds obscured, one-quarter obscured, and no obscuration. The code checks if the value is 0, 1, or 2, with any other value resulting in no obscuration.
The actual obscuring happens in the Obscure method, which:
Collects the index of every non-whitespace character
Shuffles these indices (so different characters are revealed as obscurity decreases)
Uses the obscurity factor to determine which characters to replace with random punctuation
This example demonstrates replacement markup where the replacement is based on the child text rather than the markup itself. The processor reads the child text inside a marker and uses it to determine what color to apply to that section.
The design philosophy is that the writer has already included the context for highlighting - the character's name. This allows writing:
Instead of needing to specify:
Of course, there are cases where an explicit property is needed:
The implementation is in EntityColourer.cs. The processor handles both cases by first checking for an explicit property:
If none exists, it uses the child text content:
With the name determined, it performs a lookup in a name-to-color mapping and applies TMP color tags:
This example shows how to add new text and elements (specifically sprites) to a line. Marked regions are flagged as a specific type, gaining both a sprite and color when processed:
The implementation in SpriteReplacmentMarkerProcessor.cs is quite straightforward. It defines format strings for TMP rich tags:
It then uses a switch statement on the marker name to fill in the format strings:
Since this adds visible elements at the start of the markup text (the sprite and [ character), the processor must offset any attributes within that markup:
This shifts attributes down by two positions to accommodate the added characters.
Learn about our Background Chatter Sample, which shows off how to have your characters talk to each other in the background, not just with a player character.
One of the best ways to make a game's world come alive is to have characters engage in conversations with each other, not just with the player character. The Background Chatter sample demonstrates how you can use Yarn Spinner to create and manage these ambient conversations.
The Dialogue Runner is the core component that manages dialogue interactions in your scene. When a node runs, the Dialogue Runner sends lines and options to its Dialogue Presenters.
The Line View and Option View presenters in the built-in prefab are designed to be modal - they take center stage, assuming they have the player's full attention. During these conversations, the player is typically not moving around or performing other actions.
Background conversations, however, are non-modal: they happen while the player continues to explore and interact with the world. The player doesn't participate in these conversations, which proceed independently.
The most effective approach for implementing background conversations in Yarn Spinner is to create separate Dialogue Runners for each conversation. This approach offers several advantages:
Each background Dialogue Runner can have dedicated Dialogue Presenters designed specifically for ambient conversations
Background Dialogue Runners operate independently from the main "primary" Dialogue Runner
You can manage multiple simultaneous background conversations as needed
All Dialogue Runners in your scene can share the same Yarn Project and Variable Storage, maintaining a consistent dialogue state.
Line Presenters for background conversations have different requirements than those used for primary player interactions. These presenters should:
Make it clear that dialogue is taking place
Remain unobtrusive enough to avoid interfering with the player's focus
Usually avoid presenting options, since background conversations don't assume active player participation
Since your game might feature multiple simultaneous conversations, we recommend creating one Dialogue Runner for each conversation in your scene, allowing them to run in parallel.
The BackgroundChatter sample demonstrates how to create an environment where the player can move freely while hearing ambient conversations in the background.
To play the sample, open the BackgroundChatter/Main.unity scene and press play. Use the WASD keys to move around and the Space key to initiate conversations with characters.
The sample features a sequence of background conversations, each demonstrating different aspects of this system. Explanation characters (identifiable by their ties and coffee cups) stand near each demonstration. Speaking with them provides details about what each conversation demonstrates.
In this sample, each background conversation is represented by a "Chatter Group" - a spherical volume in the game world that can trigger a specific dialogue node when the player enters it. Each Chatter Group contains:
A Canvas with custom Line Presenters
Text elements that float above speaking characters
Player position tracking that cancels dialogue when the player leaves the volume
The Chatter Group Manager runs each Chatter Group on a timer, so that while the player remains within a group's volume, that group can potentially initiate dialogue.
The basic background conversation demonstrates a simple exchange between two characters. This conversation runs normally and cannot be interrupted by player actions.
The procedural conversation demonstrates using two line groups to create a dynamic back-and-forth between characters. The first line group contains only questions, while the second contains generic replies that work with any question. This approach allows you to create numerous possible conversation combinations with minimal writing effort.
The interruptable conversation demonstrates how players can interrupt characters engaged in a background conversation. While these characters participate in their ambient dialogue, they remain interactable. When the player initiates interaction, the background conversation pauses to prevent overlap with the primary dialogue. After the player's conversation concludes, the background conversation resumes.
This example shows how players can participate in what begins as a background conversation. When the player approaches the character, a conversation starts with the player as a participant. If the player leaves the Chatter Group's volume, the conversation ends and triggers an additional dialogue node. This allows the player character to deliver a parting line rather than abruptly ending the conversation.
This example demonstrates how background conversations can access variable storage and other Yarn Spinner features. When the player passes by these characters repeatedly, they progress through different parts of an ongoing conversation. This continuity is achieved by using a variable that updates each time the conversation runs.
The marketplace scene showcases background conversations in a more gameplay-oriented scenario. As you explore this area, you'll encounter various types of background conversations demonstrated elsewhere in the sample.

TMP Sprites and adding elements to a line
localeCode: The code for the current locale in which the line will be presented
This is [b]my line[/b] with [b][i]some[/i] markup within[/b] of it.root
│
├─ "This is "
│
├─ b
│ └─ "my line"
│
├─ " with "
│
├─ b
│ ├─ i
│ │ └─ "some"
│ └─ "markup within"
│
└─ " of it."Bob: [obscurity = {$obscurity}]Why hello there, it's nice to meet you friend.[/obscurity]marker.TryGetProperty("obscurity", out int value)Player: Hey there [name]Alice[/name], what up?Player: Hey there [name=alice]Alice[/name], what up?Player: I think [name=alice]she's[/name] just being nice [name]Bob[/name]!marker.TryGetProperty("name", out string value)childBuilder.ToString().ToLower()childBuilder.Insert(0, $"<color=#{UnityEngine.ColorUtility.ToHtmlStringRGBA(entity.colour)}><b>");
childBuilder.Append("</b></color>");Liz: well it is called a [fire]flame[/fire]thrower so I think you can work that one out yourself.var start = "<b>[<color=#{0}><sprite=\"effects\" name=\"{1}\">";
var end = "</color>]</b>";case "fire":
childBuilder.Insert(0, string.Format(start, ColorUtility.ToHtmlStringRGB(debuff), "fire"));
childBuilder.Append(end);
break;for (int i = 0; i < childAttributes.Count; i++)
{
childAttributes[i] = childAttributes[i].Shift(2);
}






Learn about the Basic Storylets and Saliency Sample, which shows off the fundamentals of Yarn Spinner's saliency systems.
Yarn Spinner 3 introduced storylets, a powerful new way to select which content should be presented to players.
Storylets allow you to break your narrative into modular chunks, with a saliency strategy determining which chunk should run next based on game state and other factors.
Yarn Spinner provides two different approaches to storylets, Node Groups and Line Groups, along with several Saliency strategies to fit your specific needs.
This guide focuses on getting you comfortable with the basics of storylets in Yarn Spinner.
Writing node group storylets
Writing line group storylets
Calling salient content
Interacting with generated variable storage systems
Our Basic Storylets and Saliency Sample demonstrates various things you can do with storylets in Yarn Spinner:
Content for three in-game days (Monday, Tuesday, and Wednesday)
Two time periods each day (morning and evening)
A time advancement mechanic in the center of the scene
Several characters showcasing different approaches to storylets
The time advancer controls what day and time the NPCs respond to. Each time you press the button, time advances one step through the sequence:
Monday morning → Monday evening → Tuesday morning → and so on.
When reaching Wednesday evening, it wraps back to Monday morning
The implementation is straightforward: when you collide with the trigger, it calls the AdvanceTime() method and updates the plinth label via UpdateLabel().
Both methods work by reading and modifying Yarn variables ($day and $time) through a generated variable storage class.
Yarn Spinner allows you to generate a variable storage class that provides convenient wrappers around your declared variables. The system automatically regenerates this file whenever you change your Yarn files or project settings.
This creates two properties on the class—Day and Time—which you can access directly in your code. The Yarn Spinner-generated code:
Handles all standard variable storage lookup operations
Performs type checking automatically
Significantly reduces the amount of code you need to write
Generates enums matching the types defined in the Setup node
Each NPC in the scene demonstrates a different aspect of storylets through their DialogueInteractible component.
This component determines which node runs when the player interacts with the character.
The component pulls available nodes directly from the attached Yarn project. When triggered, it simply tells the DialogueRunner to run the selected node—just like if you wrote <<jump npc_name>> in your Yarn Spinner Script.
Each NPC runs a single entry node, and the storylets system handles the rest. But how do storylets actually work in practice?
While storylet implementation depends heavily on your specific narrative needs, our sample focuses on a common use case: NPCs making contextual comments based on the current day and time.
This example functions as a "barks" demo—short, reactive dialogue lines—which provides an excellent introduction to storylets and salient content. Each NPC demonstrates a different approach to storylets, all defined in the BasicSaliency.yarn file.
George demonstrates the simplest approach using .
Each line in a block that starts with => represents one potential candidate for selection in that line group. Yarn Spinner chooses only a single line to present each time the line group is reached.
George's dialogue includes:
Two generic lines with no conditions (can run anytime)
Three conditional lines that filter based on the current day
One line with nested content that runs if that line is selected
For example, the line => another Monday, I hate Mondays <<if $day == .Monday>> will only be shown if the current day is Monday.
Liz demonstrates with the "always" condition.
A node group consists of multiple nodes sharing the same title. When selecting which node to run, Yarn Spinner examines the when headers to determine what should be shown.
Liz's nodes all use the when: always header, indicating they're always valid choices. This special header is particularly useful for fallback content.
One of Liz's nodes uses another special header, when: once, which tells Yarn Spinner this node should only be shown once. After being selected, it will never appear again—perfect for character introductions or one-time events.
Barry also uses node groups, but with conditional expressions in the when headers.
Each of Barry's nodes checks the time of day using expressions like when: $time == .Morning or when: $time == .Evening. This means that every time you talk to Barry, half of his potential dialogue nodes are filtered out based on the current time.
This approach lets you easily filter nodes to present only content appropriate to the current game state.
Alice demonstrates the most specific approach with multiple when conditions.
Each of her nodes contains multiple when clauses, creating highly specific combinations of day and time conditions. This gives Alice the most contextually appropriate responses but requires the most content creation.
This highlights an important tradeoff: greater specificity requires more storylets. However, this is also a strength of the system. You can:
Use fallback storylets (like Liz's) for situations that don't need specific responses
Omit storylets for combinations where characters have nothing important to say
Create a more precisely woven narrative landscape with less effort than traditional approaches
Most NPCs in our sample use node groups, meaning their nodes share the same title. While this would normally cause an error in Yarn Spinner, it's intentional with storylets.
Any nodes with the same name become part of the same node group. When that group is requested to run, Yarn Spinner selects one node to present.
Important requirements for node groups:
Each node in the group must share the same title
Every node must have at least one when header to be recognized as part of a group
If you don't have specific conditions, use when: always as a fallback
Learn more about .
With multiple potential storylets available, how does Yarn Spinner decide which one to show?
This is handled by the saliency strategy, which selects the most appropriate content while resolving ambiguity.
The default strategy (used in this sample) is "Random Best Least Recently Seen," which:
Removes any storylets with failing conditions
Counts the conditions on each viable storylet to determine its "complexity"
Selects the storylet with the highest complexity (most conditions)
If multiple storylets tie for complexity, deprioritizes any that have been recently seen
This approach typically provides the best experience in most narrative situations, balancing specificity with variety. Yarn Spinner includes other built-in strategies, and you can create custom strategies for precise control over content selection.
Storylets provide a flexible and powerful way to organize narrative content in Yarn Spinner. By breaking your story into modular chunks and using saliency strategies to select the most appropriate content, you can create dynamic, responsive narratives that adapt to your game state.
Whether you're implementing character barks, branching dialogues, or more complex narrative systems, storylets offer a streamlined approach to creating engaging, reactive stories in your games.
If there's still a tie, makes a random selection



Learn how to make options that timeout after a period of inactivity from the user.
This sample demonstrates how to create a custom dialogue presenter where dialogue options include a timeout bar. When options are presented, the timeout bar gradually shrinks. If the player doesn't select an option before the timer expires, the system automatically selects one for them.
The sample supports three variations of this functionality:
A hidden option that gets selected when time runs out
A visible option designated as the default that gets selected when time runs out
The currently highlighted option gets selected when time runs out
For simplicity, this guide focuses on implementing the first approach. To explore the other methods, check out the full sample.
Custom Dialogue Presenters
Using Metadata to control views
The nightmare hellscape that is Unity UI
If you want to see a finished version of this go to PATH TO SAMPLE.
If you haven't already install Yarn Spinner link to install guide. Once installed we will start by building out a basic scene.
Make a new scene in Unity.
From Samples/Shared Assets/Prefabs drag the Basic Arena into the scene
From Samples/Shared Assets/Prefabs add a Camera Rig into the scene
Replace the contents of the Alice Yarn script with the following:
This script contains a single node with three options. Only the first two will be visible to the player. The third option (marked with the #fallback metadata) will be hidden but selected automatically if the timer expires before the player makes a choice.
Select the Camera Rig and in the inspector drag the Player into the target field
Select Alice and in the Inspector set the Dialogue field to use the new Timeout Dialogues project
Select Alice and in the Inspector select the Alice node in the dropdown
Select Alice and in the Dialogue Runner field drag in the Dialogue System object from the hierarchy
At this point, you can talk to Alice and go through the dialogue, but the timeout functionality isn't implemented yet. Now we'll create our custom presenter by building a UI bar that shrinks over time, then develop a custom dialogue presenter that uses it.
First, we'll create the UI elements for our timer by modifying the existing canvas from the Dialogue System prefab.
Right click on the Dialogue System in the hierarchy and choose Prefab -> Unpack Completely This will disconnect this from being a prefab but will otherwise leave it in the state we want.
Expand the Dialogue System out in the hierarchy so we can see all the individual components we are interested in the children and grandchildren of the Option View gameobject.
Select the Last Line game object and delete it. While we could handle also showing the last line it would just add more logic to our view while not actually showing off the main feature of this sample. So instead we'll just drop it.
This bar will shrink as the time to select an option runs out.
Our UI work is now complete. Next, we'll write the code to make the bar shrink over time.
Create a new monobehaviour script and name it TimeoutBar.cs
Open up the new file and replace the imports with the following:
Add the following fields to the class:
These fields represent the bar UI element and its original size, which we'll use to reset it between option sets.
Replace the Start method with the following:
When Unity initializes the component, we store the default bar size in originalSize for later reference.
Add the following new method:
This method resets the bar to its original size. There are several ways to accomplish this, but using SetSizeWithCurrentAnchors is straightforward and lets Unity handle the rectangle's final dimensions.
Add the following new method:
This method gradually shrinks the bar over the specified duration. It first verifies that the bar exists, then enters a loop that continues until either the duration elapses or cancellation is requested. Each iteration updates the bar size using linear interpolation from its current size toward zero. After the loop, we ensure the bar is fully shrunk regardless of how it exited the loop.
Add that class as a component to our bar
Drag the bar gameobject into the Bar field in the TimeoutBar inspector and now the bar is done.
To see a complete version of this check out path to the file in the samples.
Now we'll create the custom dialogue presenter subclass. Since this involves substantial code, we'll approach it in sections.
Create a new presenter subclass Assets -> Create -> Yarn Spinner -> Create -> Dialogue View Scriptand name it TimeoutOptionsView. This creates a stubbed out subclass with all the necessary methods for a custom view.
Replace the imports with the following:
Add the following fields to the class:
These fields reference the UI elements we need to manage: the canvas group for fade effects, the option item prefab, the timer bar, and a list to cache option items. Other than timedBar, these are similar to what you'd find in the default options presenter.
Add the following fields to the class:
These fields control the timing aspects: how long the player has to select an option before timeout, and the duration of fade-in and fade-out animations.
Add the following field to the class:
This constant identifies the option that should be selected when the timer expires. We'll use it later when examining option metadata.
Replace OnDialogueStartedAsync with the following:
Replace OnDialogueCompleteAsync with the following:
These methods are called by the dialogue runner at the beginning and end of dialogue. Both hide and disable the options UI, then return a completed task.
Replace RunLineAsync method with the following:
Since this presenter only handles options, we can return immediately when asked to run a normal line.
Replace Start with the following:
Similar to the dialogue event handlers, we initialize the UI as hidden and non-interactive.
Finally add a new internal enum to the class:
This enum identifies which timeout behavior we're using. The default is None, meaning no timeout is needed for the current option set.
Now we'll implement the RunOptionsAsync method, starting with determining which timeout option to use and validating the option group.
Inside the RunOptionsAsync method add the following code:
These variables will track whether we need a timeout and which option should be selected if timeout occurs.
Add the following code:
This code examines all the options to find any with the fallback metadata tag. If it finds one that's available, it marks that option as the hidden fallback to be selected when the timer expires. We also count how many tagged options we've seen for validation purposes.
Add the following code:
This validation ensures that if we're using a hidden fallback, we have exactly one option tagged as such, and we've successfully identified which option it is. If either condition fails, we log an error and tell the dialogue runner that no option was selected.
Next, we'll set up the infrastructure to handle completion and cancellation. These mechanisms will be provided to the option items later.
Add the following code to create our completion source:
Add the following code to create our cancellation source:
These sources work together to handle the two possible outcomes: the player selects an option, or the dialogue is cancelled. Now we'll implement the cancellation handling:
Add the following encapsulated method:
This task monitors for dialogue cancellation and ensures proper cleanup by setting a null result on the completion source if needed. This connects the two sources: if dialogue is cancelled, this function sets the completion value on selectedOptionCompletionSource.
Now we'll create and configure the option items to display to the user:
Add the following code to instantiate new option items as needed:
This creates just enough option item prefab instances to handle the current option group, reusing any we've already created. The CreateNewOptionView method will be implemented later.
Add the following code to configure the option items:
This code configures each option view with its corresponding option data, skipping any that are unavailable or marked as hidden fallbacks. It then selects the first valid option to be highlighted initially.
Add the following code to enable or disable the timer bar:
This activates the timer bar only if we're using a timeout option, resetting it to full size and ensuring it appears at the bottom of the option list.
Now we'll display the UI to the player:
Add the following code to fade in the UI:
This fades in the UI and enables user interaction once the fade is complete.
Add the following code to start the timeout bar if needed:
If we're using a hidden fallback option, this starts the timer that will eventually select it. The BeginDefaultSelectTimeout method will be implemented later.
Add the following wait code:
This suspends execution until the player selects an option, the timer expires, or dialogue is cancelled.
After a selection is made, we need to clean up and exit:
Add the following code:
This cancels the completion source to prevent further selections and disables UI interaction.
Add the following code to fade out the UI:
This fades out the UI and hides all option views, though they remain cached for future use.
Add the following code to return the selected option:
This returns the selected option to the dialogue runner, or indicates that no option was selected if dialogue was cancelled.
Now we need to implement the methods we've referenced but haven't defined yet:
Add the following new method:
This creates a new option item instance, adds it to the canvas group, positions it at the end of the list, and disables it by default.
Add the BeginDefaultSelectTimeout method:
This method initiates the timer bar shrinking animation. When complete (if not cancelled), it sets the designated fallback option as the selected choice.
Finally, let's hook everything up in Unity:
Move back over to Unity and select the Options View in the hierarchy.
Add the TimeoutOptionsView.cs script as a new component to the Option View gameobject.
Delete the Options Presenter component.
Now we need to tell the dialogue runner about our custom presenter:
Select the Dialogue System game object in the hierarchy.
Remove the old (now missing) Options Presenter from the Dialogue Views field.
Add the new timeout options view into it's place.
Take it for a spin!
Congratulations! You've created a custom options presenter that supports timed dialogue choices. When the timer expires, the system automatically selects a predefined fallback option, adding a sense of urgency to dialogue interactions.
Samples/Shared Assets/Prefabs drag a Player prefab into the sceneFrom Samples/Shared Assets/Prefabs drag an NPC prefab into the scene and rename it to be Alice
Add a default dialogue system to the scene, in the Hierarchy right click Yarn Spinner -> Dialogue Runner
Create a new Yarn Spinner project Assets -> Create -> Yarn Spinner -> Yarn Project and name it Timeout Dialogues
Create a new Yarn script Assets -> Create -> Yarn Spinner -> Yarn Script and name it Alice
Delete the previous camera called Main Camera
Select the Dialogue System and in the Inspector set the Yarn Project to use the Timeout Dialogues project
Add a new empty child gameobject to the Options View and name it timer container, this will be what holds our timer bar. By default it will have inherited a slew of position and size properties from it's parent's vertical layout group, which is mostly what we want, but we do need it to have a specific height.
In the inspector add a new Layout Element component to the timer container.
Tick the Preferred Height field and set it to have a height of 30. Now this element will tell it's parent layout group that it is going to be 30 units high which is perfect.
Add a new empty gameobject to the timer container and name it timer bar.
Give the bar an image component.
Change the bars rectangle widget to be centre-aligned + vertical stretch.
Set the width to be whatever you like the look of, we found that 1480 units was a nice value.
From Packages/Yarn Spinner/Prefabs folder drag the Option Item prefab into the Option View Prefab field.
Drag the timer gameobject into the Timed Bar field.
Pick a duration for the Auto Select, Fade Up, and Fade Down durations.
title: Alice
---
Alice: Hello, this is the fallback timeout sample
-> Option 1?
Alice: option 1 was selected
-> Option 2?
Alice: option 2 was selected
-> Option 3? #fallback
Alice: option 3 was selected despite not being visible
===using System.Threading;
using UnityEngine;
using Yarn.Unity;[SerializeField] RectTransform bar;
private float originalSize = 0f;void Start()
{
if (bar != null)
{
originalSize = bar.sizeDelta.x;
}
}public void ResetBar()
{
if (bar != null)
{
bar.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, originalSize);
}
}public async YarnTask Shrink(float duration, CancellationToken cancellationToken)
{
if (bar == null)
{
return;
}
float accumulator = 0;
var currentSize = bar.sizeDelta.x;
while (accumulator < duration && !cancellationToken.IsCancellationRequested)
{
accumulator += Time.deltaTime;
var newSize = Mathf.Lerp(currentSize, 0, accumulator / duration);
bar.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, newSize);
await YarnTask.Yield();
}
bar.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, 0);
}using System.Threading;
using System.Collections.Generic;
using UnityEngine;
using Yarn.Unity;[SerializeField] CanvasGroup canvasGroup;
[SerializeField] OptionItem optionViewPrefab;
[SerializeField] TimeoutBar timedBar;
private List<OptionItem> optionViews = new List<OptionItem>();public float autoSelectDuration = 10f;
public float fadeUpDuration = 0.25f;
public float fadeDownDuration = 0.1f;private const string HiddenFallback = "fallback";public override YarnTask OnDialogueStartedAsync()
{
if (canvasGroup != null)
{
canvasGroup.alpha = 0;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
return YarnTask.CompletedTask;
}public override YarnTask OnDialogueCompleteAsync()
{
if (canvasGroup != null)
{
canvasGroup.alpha = 0;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
return YarnTask.CompletedTask;
}public override YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token)
{
return YarnTask.CompletedTask;
}void Start()
{
if (canvasGroup != null)
{
canvasGroup.alpha = 0;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
}enum TimeOutOptionType
{
None, HiddenFallback,
}int hasTimeOutMetadata = 0;
TimeOutOptionType defaultOptionType = TimeOutOptionType.None;
DialogueOption defaultOption = null;// we run through all the options quickly to see if there are any that are configured for timeouts
foreach (var option in dialogueOptions)
{
foreach (var metadata in option.Line.Metadata)
{
if (metadata == HiddenFallback)
{
// if the hidden fallback option has failed it's condition then we don't want it to impact the rest of the option group
if (!option.IsAvailable)
{
continue;
}
defaultOptionType = TimeOutOptionType.HiddenFallback;
defaultOption = option;
hasTimeOutMetadata += 1;
break;
}
}
}// now we do some error checking
// it is possible there are multiple options flagged as the fallback which we don't want
if (defaultOptionType == TimeOutOptionType.HiddenFallback)
{
// this means we need to have only found one default tagged line
if (hasTimeOutMetadata != 1)
{
Debug.LogError("Encountered more than one option with timeout tags");
// we return
return await DialogueRunner.NoOptionSelected;
}
// and we need to have the defaultOption value be not null
if (defaultOption == null)
{
Debug.LogError("Encountered have an option tagged as a fallback but have no option value set.");
// we return
return await DialogueRunner.NoOptionSelected;
}
}// A completion source that represents the selected option.
YarnTaskCompletionSource<DialogueOption> selectedOptionCompletionSource = new YarnTaskCompletionSource<DialogueOption>();// A cancellation token source that becomes cancelled when any option item is selected, or when this entire option view is cancelled
var completionCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);async YarnTask CancelSourceWhenDialogueCancelled()
{
await YarnTask.WaitUntilCanceled(completionCancellationSource.Token);
if (cancellationToken.IsCancellationRequested == true)
{
// The overall cancellation token was fired, not just our internal 'something was selected' cancellation token.
// This means that the dialogue view has been informed that any value it returns will not be used.
// Set a 'null' result on our completion source so that that we can get out of here as quickly as possible.
selectedOptionCompletionSource.TrySetResult(null);
}
}
// Start waiting
CancelSourceWhenDialogueCancelled().Forget();if (optionViews.Count < dialogueOptions.Length)
{
// we want to create as many new options as the difference between the current cached ones we have and the size of the option group
var newViews = dialogueOptions.Length - optionViews.Count;
for (int i = 0; i < newViews; i++)
{
var option = CreateNewOptionView();
optionViews.Add(option);
}
}// configuring all the dialogue items
for (int i = 0; i < dialogueOptions.Length; i++)
{
var optionView = optionViews[i];
var option = dialogueOptions[i];
if (option.IsAvailable == false)
{
// option is unavailable, skip it
continue;
}
// if we are set to have a hidden fallback option
// and that option is THIS option we are configuring the view for
// we want to skip over it, it will be visually represented by the timer bar
if (defaultOptionType == TimeOutOptionType.HiddenFallback && defaultOption != null && option.DialogueOptionID == defaultOption.DialogueOptionID)
{
continue;
}
optionView.gameObject.SetActive(true);
optionView.Option = option;
optionView.OnOptionSelected = selectedOptionCompletionSource;
optionView.completionToken = completionCancellationSource.Token;
}
// There is a bug that can happen where multiple options can be flagged as highlighted at once
// so if one is already selected then we use that, otherwise we highlight the first non-disabled option
int optionIndexToSelect = -1;
for (int i = 0; i < optionViews.Count; i++)
{
var view = optionViews[i];
if (!view.isActiveAndEnabled)
{
continue;
}
if (view.IsHighlighted)
{
optionIndexToSelect = i;
break;
}
// ok at this point the view is enabled
// but not highlighted
// so if we haven't already decreed we have found one to select
// we select this one
if (optionIndexToSelect == -1)
{
optionIndexToSelect = i;
}
}
if (optionIndexToSelect > -1)
{
optionViews[optionIndexToSelect].Select();
}// now we add in the timer bar if necessary or turn it off if it isn't needed
if (defaultOptionType == TimeOutOptionType.None)
{
if (timedBar != null)
{
timedBar.gameObject.SetActive(false);
}
}
else
{
// we always want it at the bottom regardless of how many option item views there are
if (timedBar != null)
{
timedBar.gameObject.SetActive(true);
timedBar.ResetBar();
timedBar.transform.parent.SetAsLastSibling();
}
}// fade up the UI now
await Effects.FadeAlphaAsync(canvasGroup, 0, 1, fadeUpDuration, cancellationToken);
// allow interactivity and wait for an option to be selected
if (canvasGroup != null)
{
canvasGroup.interactable = true;
canvasGroup.blocksRaycasts = true;
}// now we kick off the timer bar if needed
if (defaultOptionType == TimeOutOptionType.HiddenFallback)
{
BeginDefaultSelectTimeout(selectedOptionCompletionSource, defaultOption, completionCancellationSource.Token).Forget();
}// Wait for a selection to be made, or for the task to be completed.
var completedTask = await selectedOptionCompletionSource.Task;completionCancellationSource.Cancel();
// now one of the option items has been selected so we do cleanup
if (canvasGroup != null)
{
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}// fade down
await Effects.FadeAlphaAsync(canvasGroup, 1, 0, fadeDownDuration, cancellationToken);
// disabling ALL the options views now
foreach (var optionView in optionViews)
{
optionView.gameObject.SetActive(false);
}// if we are cancelled we still need to return but we don't want to have a selection, so we return no selected option
if (cancellationToken.IsCancellationRequested)
{
return await DialogueRunner.NoOptionSelected;
}
// finally we return the selected option
return completedTask;private OptionItem CreateNewOptionView()
{
var optionView = Instantiate(optionViewPrefab);
var targetTransform = canvasGroup != null ? canvasGroup.transform : this.transform;
if (optionView == null)
{
throw new System.InvalidOperationException($"Can't create new option view: {nameof(optionView)} is null");
}
optionView.transform.SetParent(targetTransform.transform, false);
optionView.transform.SetAsLastSibling();
optionView.gameObject.SetActive(false);
return optionView;
}internal async YarnTask BeginDefaultSelectTimeout(YarnTaskCompletionSource<DialogueOption> selectedOptionCompletionSource, DialogueOption option, CancellationToken cancellationToken)
{
if (timedBar == null)
{
return;
}
await timedBar.Shrink(autoSelectDuration, cancellationToken);
if (!cancellationToken.IsCancellationRequested)
{
selectedOptionCompletionSource.TrySetResult(option);
}
}





Learn how to make a Phone Chat view and explore our Phone Chat Sample.
The Phone Chat sample demonstrates how to create a Dialogue Presenter that shows conversations in a scrolling view, similar to how messages on a phone look.
Lines of dialogue are shown as bubbles in the scrolling view, and are kept on screen after they're delivered, allowing the user to look up and see previous messages. Additionally, as lines appear, they're shown with a "typing" indicator.
In this tutorial, you'll build a Dialogue Presenter that looks like this:
You can find a completed version of this project as part of our Samples.
We'll start by creating a new empty Unity project, and adding Yarn Spinner.
Open Unity Hub.
Click New Project, and create a new project. In this tutorial, we'll use the Universal 3D template, though the template itself doesn't really matter.
Follow the steps in to install Yarn Spinner in your project.
Next, we'll download the assets needed by this tutorial.
Download the assets for this tutorial, and import them into your project.
We're ready to start creating our scene!
Now that we've created the project, we'll begin by laying out the UI. We'll start from an empty scene, and build a canvas that will present the Phone Chat system.
Open the File menu, and choose New Scene. Select the Empty template.
Save the new scene somewhere in your project.
In our empty scene, we'll start by creating the camera that you view the scene with.
Create a new camera by opening the GameObject menu and choosing Camera.
Select the new camera, and in the Inspector, right click the Transform, and choose Reset.
Next, we'll create the canvas that the Phone Chat Presenter appears in.
Open the GameObject menu, and choose UI -> Canvas.
Select the new canvas in the Hierarchy, and find the Canvas Scaler component.
Set its UI Scale Mode to "Scale with Screen Size".
Set its Reference Resolution to 1920 by 1080.
We're now ready to start building the UI itself. The most important part of a scrolling chat view is the scroll view, so let's create that!
Open the GameObject menu, and choose UI -> Scroll View.
Select the newly created Scroll View in the Hierarchy.
In the Inspector, find the Transform component.
Set the Pos X and Pos Y values to 0,0.
Our scroll view won't need any scroll-bars, so we'll get rid of them now. We'll also set up the scroll view so that it only scrolls vertically.
Select the Scroll View object in the Hierarchy.
In the Inspector, find the Scroll Rect component.
Select the Horizontal Scrollbar field, and press Backspace to clear it. Do the same thing for the Vertical Scrollbar field.
Turn off the Horizontal checkbox, so that it doesn't scroll horizontally.
Next, we'll set up the scroll view to use our background image.
In the Inspector, find the Image component.
Change the Color property to fully opaque white.
Change the Source Image property to the BG-Lightmode sprite.
Because we removed the scroll bars, we'll need to make the viewport of the scroll view fill the available area:
Select the Viewport object in the Hierarchy.
In the Transform component, set the Left, Right, Top and Bottom values all to 0.
We're almost done setting up the scroll view. The next thing to do is to make the content in the scroll view appear at the bottom of the view. We'll do this by setting up the Anchors of the Content view so that it's pinned to the bottom of its parent. This means that as bubbles get added to the Content object, the content object will grow upwards (rather than downwards).
Select the Scroll View -> Viewport -> Content object in the Hierarchy.
Find the Transform component in the Inspector.
Set the Anchors -> Min to X: 0, Y: 0.
Set the Anchors -> Max to X: 1, Y: 0.
We'll now make all of the content in the Scroll View automatically laid out in a vertical list.
With the Content object still selected, click Add Component at the bottom of the Inspector.
Search for "Vertical Layout Group", and add one to the object.
Set the new Vertical Layout Group's Spacing to -10.
Set its Child Alignment to Lower Center.
This will cause the items in the list to fill the entire width of the list. The height of each of the elements will be determined by the items themselves, using a component we'll add later.
We'll now also make the content update its own total size based on its contents.
Click the Add Component button at the bottom of the Inspector.
Search for "Content Size Fitter" and add one to the object.
Set the Horizontal Fit of the new Content Size Fitter to Unconstrained.
Set the Vertical Fit to Preferred Size.
Next, we'll create the bubble itself.
The goals for the bubble are that the text fits in the available width, and it takes up as much height as it needs given that width. We also want to show a speech bubble image behind the text, and we want that image to only be as wide as the text - it shouldn't take up the full width of the chat view.
This mimics the way that conversations work in popular chat apps: the speech bubble graphic doesn't take up the full width of the row that it exists in.
Select the Content object in the Hierarchy.
Open the GameObject menu, and choose Create Empty Child.
Name the newly created object "Message Bubble".
Because it's a child of the Vertical Layout Group we added earlier, it will automatically take up the full width of the list. We'll also want the bubble to have the correct height, based on the amount of text it has, but because we haven't set up the text component yet, that isn't available. In the meantime, we'll manually set up the bubble to have a fixed height, and we'll replace this later.
Select the Message Bubble object.
Add a new Layout Element component to the object.
Check the Min Height box, and set its value to 200.
We'll now add an object that manages the width of the bubble graphic:
Create a new empty child game object of the Message Bubble.
Name the new object "Message Content".
Add a Layout Element component to it, and set its Min Width to 300.
Add a Content Size Fitter component to it, and set its Horizontal Fit to Min Size.
Next, we'll make the bubble be right-aligned inside its content, and fill the entire height of the row:
With the Message Content object selected, go to the Transform component.
Set the Anchors -> Min to X: 1, Y: 0.
Set the Anchors -> Max to X: 1, Y: 1.
Set the Pivot to X: 1, Y: 0.5.
Now we'll add the bubble background image itself. We'll add a new object, set up its anchors so that they fill the parent object, and then add the image:
Add a new empty child object of Message Content.
Name the new object "Background".
In the Transform, set the Anchors -> Min to X: 0, Y:0.
Set the Anchors -> Max to X: 1, Y:1.
Your view should now look like this:
Now we'll add the text component, which will show the text of the image. The text object's anchors will be set up so that they're pinned to all for edges of the container, making it fill up the parent object.
Add a new empty child object of Message Content. Name it "Text".
Set its Anchors -> Min to X:0, Y:0.
Set its Anchors -> Max to X:1, Y:1.
Set its Left, Top, Right, Bottom all to 0.
The bubble should now look like this:
The bubble now has the right graphics, but its size could be improved. We'll now add some code that makes the various parts of the bubble correctly sized. To get the best result, we need to think about the overall layout here:
The Vertical Layout group is controlling the width of the Message Bubble element, and we want to make each Message Bubble control its own height.
Inside the Message Bubble, we also want the visible bubble to be as wide as it needs to be to show the text, but no wider than that (that is, there should be a gap on the side).
To make this work, we'll create a component that calculates the required size of the text box, and use it in two places:
The Message Bubble will use it to calculate its height (with its width being driven by the Vertical Layout Group on Content).
The Message Content will use it to calculate its width (with its height inherited from its parent).
Open the Assets menu, and choose Create -> MonoBehaviour Script.
Name the new script "UseSizeOfText".
Double-click the new script to open it in your text editor.
Delete the contents of the file and replace it with this:
This code implements a basic layout element, which is something that the layout system can ask questions of about what size it wants to be. - we'll now add code to it to make it derive its size from the text element.
Add the following method to the UseSizeOfText class:
This method takes information describing the text in a TextMeshPro element, calculates the smallest size it needs to be given its constraints, and asks the layout system to rebuild the layout.
Lastly, we need the layout to update whenever the text changes. To do this, we'll make use of the event, which is called immediately before the text in a TextMeshPro component is rendered as a mesh. When this happens, it's likely that our required size has changed, so we tell the layout to update.
Add the following methods to UseSizeOfText:
Now that we've built a component that notifies the layout system of the necessary size of the text, we'll use it in our setup.
Select the Message Content object in the Hierarchy.
Remove the Layout Element component from it.
Add a new Use Size Of Text component.
The bubble will resize to fit the width of the text. We now need to make the row take the correct height.
Select the Message Bubble object in the Hierarchy.
Remove the Layout Element component from it.
Add a new Use Size Of Text component.
Your bubble should now look like this:
When you're using a chat app, you see a typing indicator while the person on the other end is typing. Let's set up our chat bubbles so that they can show an animation for a short duration before revealing the text, as though the messages was being "typed out".
The way that the typing animation will look will be three dots, which bounce up and down in sequence.
Our first step will be to set up an object inside the Message Bubble prefab to act as our typing indicator. This object will act as a container for the three dots, so it needs to fill the entire parent object.
Create a new empty game object as a child of Message Content. Name it "Typing Indicator".
In the Inspector for the Transform component, set its Anchor Min to X:0, Y:0.
Set its Anchor Max to X:1, Y:1.
Set its Top, Left, Right, and Bottom all to 0.
Let's now add the dots themselves!
Add three new empty game objects as children of Typing Indicator. Name them Dot 1, Dot 2, Dot 3.
Select all three of the Dot objects you just created.
Add an Image component to all of them.
Now that we've set up all of their common properties, we'll lay each of them out.
Select Dot 1, and set its Pos X to -25.
Select Dot 2, and set its Pos X to 0.
Select Dot 3, and set its Pos X to 25.
Finally, we'll set them up so that they bounce up and down while visible.
Open the Assets menu, and choose Create -> Animation -> Animation Clip.
Name the new animation clip "Typing".
Select the new animation clip, and in the Inspector, turn on Loop Time.
Drag and drop this new clip onto the Typing Indicator object.
This will create a new Animator Controller that uses this clip, and then add an Animation component to the Loading Indicator that uses the controller.
Next, we'll set up the animation itself.
Open the Window menu, and choose Animation -> Animation. (You may want to dock the tab in your Unity editor window, if it isn't already.)
Select the Loading Indicator object.
The Animator window should be showing the Typing animation. You can check by looking at the dropdown menu at the top-left of the pane.
In the Animator window, Click Add Property.
Choose Dot 1 -> Rect Transform -> Anchored Position.
Repeat this process for Dot 2 and Dot 3.
By default, the newly created channels will have a keyframe at frame 0, and another at frame 60.
Select the keyframe at frame 60 and press Ctrl-C.
Move the playhead to frame 30 and press Ctrl-V, to make a copy of it.
We'll now create a new keyframe that represents the high point of each dot's bounce.
Move the playhead to frame 15.
Click the Record button at the top left of the Animation pane.
In the Scene view, drag all three Dot objects up so that their Pos Y is 25.
Click the Record button again to leave recording mode.
Now that the dots are moving, we'll adjust the timing so that they bounce up in sequence - first one, then two, then three.
Select all of the keyframes for Dot 2 and shift them 30 frames to the right.
Select all of the keyframes for Dot 3 and shift them 60 frames to the right.
Click the Preview Play button again and watch as they bounce up in sequence.
Looking good!
We're done with our basic bubble. Let's now create prefabs for it.
Drag the Message Bubble object from the Hierarchy into the Project tab, creating a Prefab.
This will be our base prefab. We'll create two variants of it: one for each of our two characters.
Delete the Message Bubble object from the scene.
Select the Message Bubble prefab you just created.
Open the Assets menu, and choose Create -> Prefab Variant.
Name the new prefab Message Bubble A.
In our phone chat system, we want Message Bubble A to use a green graphic and be right-aligned, and Message Bubble B to use a blue graphic and be left-aligned. Message Bubble A is already what it needs to be, so we just need to make some changes to Message Bubble B. The only things we need to change are the anchors - Bubble A is pinned to the right side, and we'll need to adjust it so that it's pinned to the left.
Select the Message Content object in Bubble B.
Change its Anchors Min to X: 0, Y:0.
Change its Anchors Max to X: 0, Y:1.
Change its Pivot to X: 0, Y:0.5.
The bubble will now be aligned to the left of the box, but its background image is pointing the wrong way.
Select the Background object in Message Bubble B.
In the Transform component, set its Scale X to -1.
The bubble will now be facing the right way. Lastly, we'll make the bubble use a different colour image.
With the Background object still selected, change the Source Image in its Image component to Bubble Blue.
The two bubbles should now look like this:
We're all done with setting up these bubbles for displaying text, though we'll return to them later at the very end of this tutorial. Let's apply these changes to the prefab overrides.
Select the Message Bubble B object
In the Inspector, click Overrides, and then click Apply All.
This will apply these changes to Bubble B prefab variant (without modifying the A variant or the base prefab).
Delete the Message Bubble A and Message Bubble B objects from the scene.
Now that we've set up the prefabs for the line views, let's set up the UI for options. When the Dialogue Presenter needs to show options, we'll create a list of buttons that contain the text of each option.
We'll start by creating the object that will contain all of the buttons.
Create a new empty object under Content, and call it Options Container.
Add a Vertical Layout Component to it.
Set its Padding Top to 8, and Padding Bottom to 16.
Set its Spacing to 16.
This will create a new item in the scrolling list that is itself a vertical list. Now that we've built the layout for the options, let's create the button itself.
Create a new empty object under Options Container, and call it Option Button.
In the Transform component in its Inspector, set its Height to 60.
Now we'll add the image for the button. The button image itself will need to fill the entire parent, so we'll set up the anchors to do that.
Add a new empty object under Option Button, and call it Background.
Set its Anchor Min to X: 0, Y: 0
Set its Anchor Max to X: 1, Y: 1.
Set its Top and Bottom to 0, and its Left and Right to 20.
Next, the component that shows the text of the option. Again, this will fill the parent.
Add a new empty object under Option Button, and call it Text.
Set its Anchor Min to X: 0, Y: 0
Set its Anchor Max to X: 1, Y: 1
Set its Top, Bottom, Left and Right all to 0.
We'll now configure the Text component to try to fit more text on a single line, where needed.
Set Text Wrapping Mode to No Wrap, and set Overflow to Ellipsis.
Turn on Auto Size, and set its Min Size to 24, its Max to 36, and its WD% to 10.
This will cause it to condense the font slightly if it needs the space, and to shrink the font size if it still needs more space. If it still can't fit the text, it will truncate it with an ellipsis (...) rather than overflow onto a new line. With these settings, an option's text can be about 40 characters long before it truncates.
Finally, we'll add a Button component, which will do two things: it'll make the button change its sprite as the user interacts with it, and it will also (eventually) tell the Dialogue Presenter that the user has chosen this option when clicked or tapped. We'll set up the Button component to change the sprite drawn in the Background object based on the current interaction state of the button.
With the Option Button selected, Add a Button component.
Set the Transition to Sprite Swap.
Drag and drop the Background object into the Target Graphic field.
Set Highlighted Sprite to Option-Selected.
Your button should now look like this:
We're all done setting up the visuals of the button. The last step is to turn it into a prefab so that we can instantiate copies of it later!
Drag and drop the Option Button into the Project pane to create a prefab.
Delete the original Option Button object from the scene.
We're all set to start wiring this UI up to a Dialogue Presenter!
We'll start from the bottom up - we'll create scripts that manage the contents of the bubbles and the option buttons, and then create the Dialogue Presenter to work with these scripts.
Let's start with the code for the bubbles. This will be a simple script that can be told to show some text, or show a typing indicator.
Open the Assets menu, and choose Create -> MonoBehaviour Script.
Name the new script "ChatDialoguePresenterBubble".
Double-click the new script to open it, and replace its contents with the following code.
Now we just need to add this script to our bubbles. Because our Message Bubble A and Message Bubble B prefabs are variants, all we need to do is just add it to the base prefab, and it will appear on all of them.
Select the Message Bubble prefab in the Project pane.
Click Open at the top right of the Inspector to open the prefab for editing.
Add a ChatDialoguePresenterBubble component to the Message Bubble object.
Drag and drop the Typing Indicator into the Typing Indicator field.
Next, we'll create and set up the code for the Option Button. This script needs to both show text to the user, and also notify the rest of the system that the button has been pressed.
Open the Assets menu, and choose Create -> MonoBehaviour Script.
Name the new script "ChatDialoguePresenterOptionsButton".
Double-click the new script to open it, and replace its contents with the following code.
Now that the code's been written, we'll add it to the Option Button prefab.
Select the Option Button prefab in the Project pane.
Click Open at the top right of the Inspector to open the prefab for editing.
Add a Chat Dialogue Presenter Options Button component to the Option Button object.
In the Button component, add a new entry to the On Click event.
With this, our bubble and button prefabs are ready for use with the Dialogue Presenter!
We're now finally ready to create the Dialogue Presenter itself!
Open the Create menu, and choose Yarn Spinner -> Dialogue Presenter Script.
Name the new script "ChatDialoguePresenter".
Double click the new script to open it in your text editor.
The default Dialogue Presenter template script contains an empty implementation of all of the necessary methods needed to be a Dialogue Presenter. We'll start adding functionality to it, piece by piece.
Add the following variables to the ChatDialoguePresenter class.
Our code doesn't need to take any special action when dialogue starts and ends, so we'll replace them with simple methods that just return immediately.
Replace the OnDialogueStartedAsync and OnDialogueCompleteAsync methods with the following code:
Now it's time to start building the actual behaviour. We'll implement the RunLineAsync method, which is called when the Dialogue Runner needs to show a line of dialogue to the user. This method will look at the line, figure out which bubble prefab needs to be used, and then adds a copy of that prefab to the list and makes the bubble show the right content.
Replace the RunLineAsync method with the following code.
With this done, our Dialogue Presenter is able to show lines of dialogue in the bubble!
We'll now create the Dialogue Runner. Yarn Spinner comes with a simple menu item that creates a basic set of Dialogue Presenter, but because we've already built our own custom UI already, we don't need to use it. Instead, we'll create one from scratch.
Create a new empty object called "Dialogue Runner".
Add a Dialogue Runner component to the object.
Add a new empty game object as a child, and call it "Phone Chat Presenter".
Add a ChatDialoguePresenter component to Phone Chat Presenter.
While we're here, we'll also set up the prefabs that will be used for options.
Drag the Option Button prefab into the Options Button Prefab field.
Drag the Options Container object into the Options Container field.
We're ready to test! Let's create some dialogue and hook it up.
We'll start by creating a Yarn Project, and a single Yarn Script containing some sample dialogue.
Open the Assets menu, and choose Create -> Yarn Spinner -> Yarn Project.
Name the new file "Phone Chat Project".
Open the Assets menu, and choose Create -> Yarn Spinner -> Yarn Script.
Name the new file "Phone Chat".
Next, let's write some dialogue.
Open the Phone Chat script and replace its contents with the following:
Finally, we'll connect this project up to the Dialogue Runner we made in the previous section.
Select the Dialogue Runner object in the Hierarchy.
Drag and drop the Phone Chat Project that you just created into the Yarn Project field.
Turn on Start Automatically. (Leave the Start Node as 'Start'.)
The very last step is to add our Dialogue Presenter to the Dialogue Runner, so that it receives content and shows it to the user.
Add a new entry in the Dialogue Presenters list.
Drag in the Phone Chat Presenter object into the new field.
We're finally ready to see it all in action!
Click the Play button at the top of the Unity window. The conversation will play out!
This looks good, but there's some room for polish. The first thing to note is that all of the lines use the same bubble, and there's no way to tell who's saying what. Remember that we created two prefabs - the green one that we called 'A', and the blue one we called 'B'. Let's set it up so that lines spoken by character 'A' use our 'A' prefab, and likewise for 'B' and the 'B' prefab.
Select the Phone Chat Presenter in the Hierarchy.
In the Inspector, click the + button next to the Characters field to add an entry to the Characters dictionary.
Set the key of the new entry to "A", and drag the Message Bubble A prefab into the Object slot.
Repeat this process, creating a new entry "B" that uses the Message Bubble B prefab.
Lines from "B" will use a different-looking bubble!
We've got lines working just fine, but we don't currently have support for options. Let's fix that! We'll start by adding some actual options to our sample dialogue, and then we'll modify our Dialogue Presenter so that it knows what to do when options arrive.
Open the Phone Chat.yarn, and replace its contents with the following:
Open the ChatDialoguePresenter.cs file, and replace the RunOptionsAsync method with the following code:
One more thing before we test it out: when the user taps on an option to select it, we'll run the contents of that option as though it had been a line. (Without this, there's no record of what the player selected, which feels strange.)
Select the Dialogue Runner in the Hierarchy.
In the Dialogue Runner component's Inspector, turn on Run Selected Option As Line.
We're all set to see it in action.
Play the game. After the lines appear, option buttons will run!
We're all done with our Phone Chat Dialogue Presenter! Try writing some more complex conversations, swap out the sprites to create a new theme, create new bubbles, or use this code to create a scrolling log of messages that isn't phone themed!
Set the Width to 600, and the Height to 900.
Delete the Scrollbar Horizontal and Scrollbar Vertical objects from the scene.
Set the Pivot to X: 0.5, Y: 0.
Check the Control Child Size -> Width and Height boxes.
Check the Child Force Expand -> Width box, and uncheck the Height box.
Set Pos X, Top and Bottom all to 0.
Set the Left, Top, Right and Bottom values all to 0.
Add a new Image component.
Set the Source Image of the new component to Bubble Green.
Add a TextMeshPro - Text (UI) component to the object.
Set the Font to Montserrat-Regular. (This is one of the assets that you downloaded when you began this tutorial.)
Set the Text to something temporary, like "This is a test".
Scroll down to Extra Settings, and click it to expand it.
Set the Margins to:
Left: 55
Top: 30
Right: 55
Bottom: 35
Drag and drop the Dot sprite from the downloaded assets into the Source Image slot.
Click on the Color value, and change the Alpha value to 128, to make it semi-transparent.
Set the Pos Y for all of them to 0.
Set the Width and Height for all of them to 20.
Click the Preview Play button in the Animation window and watch as they all bounce up together.
Repeat the above two steps, creating a new prefab variant called Message Bubble B.
Drag Message Bubble A into the Content object in the hierarchy, creating an instance of it in the scene.
Drag Message Bubble B into the Content object as well.
Turn on Control Child Size Width, and Child Force Expand Width.
Add an Image component.
Set the Images component's Source Image to Option-NotSelected.
Add a TextMeshPro - Text (UI) component to the object.
Set its Vertex Color to black.
Set its Alignment to Centered Middle.
Set its font to Montserrat-Regular.
Click Extra Settings to expand it.
Set its Left and Right margins to 30.
Set Pressed Sprite to Option-Pressed.
Set Selected Sprite to Option-Selected.
Close the prefab and go back to the scene.
Drag and drop the Option Button object from the Hierarchy into the new event's object field.
Change the method from 'No Function' to 'ChatDialoguePresenterOptionsButton -> OnClicked()'.
Close the prefab and go back to the scene.
Drag the Message Bubble prefab into the Default Bubble Prefab field. This will be the prefab that's used whenever there's no specific prefab to use for a character.
Drag the Content object into the Bubble Container field.
Play the game again.
using UnityEngine;
using UnityEngine.UI;
using TMPro;
[ExecuteAlways]
[RequireComponent(typeof(RectTransform))]
public class UseSizeOfText : MonoBehaviour, ILayoutElement
{
private TMP_Text Text => GetComponentInChildren<TMP_Text>();
private RectTransform RectTransform => GetComponent<RectTransform>();
float ILayoutElement.preferredHeight => minHeight;
float ILayoutElement.preferredWidth => minWidth;
float ILayoutElement.flexibleWidth => 0f;
float ILayoutElement.flexibleHeight => 0f;
public float minWidth { get; private set; } = 0f;
public float minHeight { get; private set; } = 0f;
int ILayoutElement.layoutPriority => 0;
[SerializeField] float minimumWidth = 250f;
[SerializeField] float minimumHeight = 30f;
void ILayoutElement.CalculateLayoutInputHorizontal() { }
void ILayoutElement.CalculateLayoutInputVertical() { }
}private void UpdateLayout(TMP_TextInfo info)
{
if (info == null || info.textComponent == null || string.IsNullOrEmpty(info.textComponent.text))
{
minHeight = minimumHeight;
minWidth = minimumWidth;
return;
}
// Calculate the maximum width available to us by getting our
// parent's width
var parentWidth = RectTransform.parent.GetComponent<RectTransform>().rect.width;
// Get the left and right margins of the text component
var xMargin = info.textComponent.margin.x + info.textComponent.margin.z;
// Get the total width available for drawing text
var insetSize = parentWidth - xMargin;
// Compute the rectangle we'd need to draw the text in, given our
// available width and an (effectively) unlimited amount of vertical
// space
var size = info.textComponent.GetPreferredValues(info.textComponent.text, insetSize, float.MaxValue);
// Our minimum width and height are now based on this (we add a
// slight padding to the width)
minHeight = Mathf.Max(minimumHeight, size.y);
minWidth = Mathf.Max(minimumWidth, size.x + 5);
// Now that we know our minimum width and height, ask the layout
// system to rebuild our layout
LayoutRebuilder.MarkLayoutForRebuild(RectTransform);
}protected void OnValidate() => UpdateLayout(null);
protected void OnEnable()
{
if (Text != null)
{
Text.OnPreRenderText += UpdateLayout;
UpdateLayout(Text.textInfo);
}
}
protected void OnDisable()
{
if (Text != null)
{
Text.OnPreRenderText -= UpdateLayout;
}
}using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class ChatDialoguePresenterBubble : MonoBehaviour
{
[SerializeField] GameObject typingIndicator;
private TMP_Text TextView => GetComponentInChildren<TMP_Text>();
public bool HasIndicator => typingIndicator != null;
public void ShowTyping()
{
if (typingIndicator != null)
{
typingIndicator.SetActive(true);
}
if (TextView != null)
{
TextView.text = string.Empty;
}
}
public void ShowText(string text)
{
if (typingIndicator != null)
{
typingIndicator.SetActive(false);
}
if (TextView != null)
{
TextView.text = text;
}
}
}using System;
using UnityEngine;
using TMPro;
public class ChatDialoguePresenterOptionsButton : MonoBehaviour
{
private TMP_Text TextView => GetComponentInChildren<TMP_Text>();
public string Text
{
get => (TextView != null) ? TextView.text : string.Empty;
set { if (TextView != null) { TextView.text = value; } }
}
public Action OnClick { get; internal set; }
public void OnClicked()
{
OnClick?.Invoke();
}
}
[Header("Prefabs")]
[SerializeField] SerializableDictionary<string, ChatDialoguePresenterBubble> characters = new();
[Space, SerializeField] ChatDialoguePresenterBubble defaultBubblePrefab = null;
[SerializeField] ChatDialoguePresenterOptionsButton optionsButtonPrefab;
[Header("Containers")]
[SerializeField] RectTransform bubbleContainer;
[SerializeField] RectTransform optionsContainer;
[Header("Timing")]
[SerializeField] float delayAfterLine = 1f;
[SerializeField] float minimumTypingDelay = 0.5f;
[SerializeField] float maximumTypingDelay = 3f;
[SerializeField] float typingDelayPerCharacter = 0.05f;
[SerializeField] bool showTypingIndicators = true;public override YarnTask OnDialogueStartedAsync() => YarnTask.CompletedTask;
public override YarnTask OnDialogueCompleteAsync() => YarnTask.CompletedTask;public override async YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token)
{
// Early out if we don't have anywhere to put our bubble
if (bubbleContainer == null)
{
Debug.LogWarning($"Can't show line '{line.Text.Text}': no bubble container");
return;
}
// Next, we figure out what prefab to use.
// We'll start with our default bubble. If we know about a specific
// bubble that the character speaking the line should use, we'll use
// that instead.
var prefab = defaultBubblePrefab;
if (line.CharacterName != null)
{
characters.TryGetValue(line.CharacterName, out prefab);
}
// If we don't have a bubble prefab at this point, we didn't have a
// default prefab, and we didn't find a prefab for the specific
// character. We can't show the line.
if (prefab == null)
{
Debug.LogWarning($"Can't show line '{line.Text.Text}': no default bubble was set");
return;
}
// Next, we need to show the bubble. If the options container is
// present, insert it immediately before the container (so that the
// options are always at the bottom of the list.) If we don't have
// an options container, just insert it at the bottom of the list.
int index;
if (optionsContainer != null)
{
index = optionsContainer.GetSiblingIndex();
}
else
{
index = bubbleContainer.childCount - 1;
}
// If we're configured to show a typing indicator in the bubbles,
// and the bubble prefab we have actually HAS a typing indicator,
// we'll create a bubble for showing it, and wait for the
// appropriate time before replacing it with the text.
if (showTypingIndicators && prefab.HasIndicator)
{
// We create a bubble and then destroy and replace it (rather
// than changing its size) to avoid a layout pop
var typingBubble = Instantiate(prefab, bubbleContainer);
typingBubble.transform.SetSiblingIndex(index);
typingBubble.ShowTyping();
// Calculate how long the typing indicator should appear for
var typingDelay = Mathf.Clamp(
line.TextWithoutCharacterName.Text.Length * typingDelayPerCharacter,
minimumTypingDelay,
maximumTypingDelay);
// Wait for the required time. If our token gets cancelled in
// the meantime, stop waiting.
await YarnTask.Delay(System.TimeSpan.FromSeconds(typingDelay), token.HurryUpToken).SuppressCancellationThrow();
// Remove the typing bubble. We'll replace it with the text
// bubble in a moment.
Destroy(typingBubble.gameObject);
}
// Create the bubble containing the text.
var bubble = Instantiate(prefab, bubbleContainer);
bubble.transform.SetSiblingIndex(index);
bubble.ShowText(line.TextWithoutCharacterName.Text);
// Now that the line is on screen, wait for the appropriate delay,
// and then return. We'll leave the speech bubble we added, so that
// it stay on screen.
await YarnTask.Delay(System.TimeSpan.FromSeconds(delayAfterLine), token.HurryUpToken).SuppressCancellationThrow();
}title: Start
---
<<wait 0.5>>
A: hey i made this chat demo
A: it's pretty cool
B: lmao nice
===title: Start
---
<<wait 0.5>>
A: hey i made this chat demo
A: it's pretty cool
B: lmao nice
B: does it support options
A: lemme see
-> A: yep
-> A: uh huh
-> A: think so
B: nice, i bet it also supports wrapping text over multiple lines
===public override async YarnTask<DialogueOption> RunOptionsAsync(DialogueOption[] dialogueOptions, CancellationToken cancellationToken)
{
// First things first: check to see if we have everything we need to show options.
if (optionsContainer == null)
{
Debug.LogWarning($"Can't show options: no bubble container");
return null;
}
if (optionsButtonPrefab == null)
{
Debug.LogWarning($"Can't show options: no bubble prefab");
return null;
}
// Clear any previous options that might still be present.
for (int i = 0; i < optionsContainer.childCount; i++)
{
Destroy(optionsContainer.GetChild(i).gameObject);
}
// Create a completion source, which allows the buttons to indicate
// that an option has been selected.
var completionSource = new YarnTaskCompletionSource<DialogueOption>();
// Show a button for each of the options.
foreach (var option in dialogueOptions)
{
// Create the button, and show the text.
var button = Instantiate(optionsButtonPrefab, optionsContainer);
button.Text = option.Line.TextWithoutCharacterName.Text;
// When the button is clicked, complete the task with the
// appropriate option.
button.OnClick = () => completionSource.TrySetResult(option);
}
// Wait until an option has been selected.
var selectedOption = await completionSource.Task;
// Clean up by destroying all of the buttons.
for (int i = 0; i < optionsContainer.childCount; i++)
{
Destroy(optionsContainer.GetChild(i).gameObject);
}
// Return the selected option.
return selectedOption;
}





