All pages
Powered by GitBook
1 of 4

Loading...

Loading...

Loading...

Loading...

Storylets and Saliency

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.

Custom Saliency Strategies
Advanced Saliency

Basic Storylets and Saliency

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.

For more on this topic we have Storylets and Saliency Primer, which explores the concept in the abstract, and a sample and guide on Custom Saliency Strategies and Advanced Saliency.

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.

What we'll be covering

  • Writing node group storylets

  • Writing line group storylets

  • Calling salient content

  • Interacting with generated variable storage systems

The Sample

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

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.

You can learn more about , if you're curious.

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

The NPCs

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?

Writing Storylets in Yarn Spinner

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 and Line Groups

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, Node Groups, and When Always

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 and Conditionals

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 and Multiple Whens

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

Nodes Groups and Titles

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:

  1. Each node in the group must share the same title

  2. Every node must have at least one when header to be recognized as part of a group

  3. If you don't have specific conditions, use when: always as a fallback

Learn more about .

The Saliency Strategy

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:

  1. Removes any storylets with failing conditions

  2. Counts the conditions on each viable storylet to determine its "complexity"

  3. Selects the storylet with the highest complexity (most conditions)

  4. 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.

Conclusion

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

Variable Storage
Line Groups
Node Groups
Node Groups
The Basic Saliency Sample
Generated variable storage section of the project inspector
The Dialogue Interactible inspector for Alice

You can learn how to add the Yarn Spinner for Samples to your project over at Samples.

Custom Saliency Strategies

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.

You can learn how to add the Yarn Spinner for Samples to your project over at .

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.

This sample focuses on creating custom saliency strategies, not on saliency itself. For more information about the concept of saliency, see Storylets and Saliency Primer, and Saliency.

What we'll be covering

  • Creating new saliency strategies through the IContentSaliencyStrategy interface

  • Reading line metadata

  • Reading node headers

IContentSaliencyStrategy

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.

Weighted Saliency

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.

You'll notice several return null statements in this method, which is entirely appropriate. It's normal and expected that sometimes there won't be any valid content to run. Returning null is how you inform Yarn Spinner that no valid content is available.

Samples
=> I am line A #weight:2
=> I am line B #weight:1
weightString = 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;
    }
}

Advanced Saliency

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.

You can learn how to add the Yarn Spinner for Samples to your project over at .

What we'll be covering

  • Dynamically building up Yarn content

  • Dynamic selection of Yarn content

  • Writing highly dynamic storylets

  • Changing a scene on the fly in response to Yarn value changes

  • Challenges in writing content for these types of games

The Room

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:

  1. 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.

  2. 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 Sample

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.

The buttons

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.

The Void

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

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

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.

The Yarn

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:

Character Interpolation

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.

The Whens

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.

Sample Limitations

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.

Samples
The advanced saliency sample showing off an office based scenario
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 == .NotStarted
title: 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 == false
A: date time!
Player: Date Time!
B: DATE TIME!
A: why are you here?!