Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Welcome to Yarn Spinner! On this page you'll learn how to get started.
Get started with Yarn Spinner Scripting by working through the fundamentals in detail.
Learn about Yarn Spinner's built-in functions.
visited(string node_name)visited_count(string node_name)format_invariant(number n)random()random_range(number a, number b)dice(number sides)min(number a, number b)max(number a, number b)round(number n)round_places(number n, number places)floor(number n)ceil(number n)inc(number n)dec(number n)decimal(number n)int(number n)Learn about using line groups, which allow Yarn Spinner to choose which content to run, depending on conditions.
title: Start
---
Captain: Navigator, fire the glitter torpedoes! That'll confuse the enemy ships!
=> Navigator: *sighs deeply* Sir, we don't have 'glitter torpedoes.' Those were in your dream last night.
=> Navigator: *eyes roll skyward* Captain, weaponizing craft supplies is not part of standard space combat protocol.
=> Navigator: *Slumps shoulders in defeat* I'll... make a note in the log that you suggested tactical glitter, sir.
===Dive into the Advanced features of Yarn Spinner Scripting.
Learn about tags and metadata, for adding additional context to lines in Yarn Spinner Scripts
Markup lets you add attributes to text that is delivered in lines.
[ and ] charactersnomarkup Attributecharacter Attributeselectplural and ordinalplural and ordinalLearn about reusing the same line in multiple places, using shadow lines.
title: Tavern
---
Ava: Hello, barkeep!
Guy: Hi there, how can I help?
Ava: I should go. #line:departure
===
title: Kitchen
---
Ava: Greetings, chef!
Guy: What are you doing back here?
Ava: I should go. #shadow:departure
===Use Yarn Spinner Scripts in Unity games, using Yarn Spinner for Unity.
Learn about the Welcome Sample, a launching point to explore some basics of Yarn Spinner for Unity.

There's a lot of new features for storylets and saliency in Yarn Spinner. Learn about them!
Learn about our premium add-ons for Yarn Spinner for Unity, giving you ready-made Dialogue Wheels and Speech Bubbles for Yarn Spinner, as well as integrations with popular third-party assets.



Learn about the Unity components that you use when working with Yarn Spinner for Unity.
Learn how to use Yarn Spinner for Godot (GDScript). This Quickstart is here to help you get up and running during the Alpha Period for Yarn Spinner for Godot (GDScript).
Learn about Dialogue Presenters, which present dialogue content to the user in Yarn Spinner for Unity.
Guard: Who goes there?
// If the player is a thief, a royal visitor, or a merchant, then
// go run the appropriate conversation for that. The player might be
// some combination of the three, so let them choose.
-> A thief! <<if $player_is_thief>>
<<jump Guard_Thief_Conversation>>
-> A royal visitor! <<if $player_is_royal_visitor>>
<<jump Guard_RoyalVisitor_Conversation>>
-> A merchant! <<if $player_is_merchant>>
<<jump Guard_Merchant_Conversation>>
// But if the player is NONE of those, then none of the options would have
// been available. We'll fall through to here.
Player: I'm nobody!
<<jump Guard_Nobody_Conversation>>{
"projectFileVersion": 4,
"sourceFiles": ["**/*.yarn"],
},
}"excludeFiles": [
"**/build/**",
"**/backup/**",
"**/Library/**"
]<<set $gold_amount to 5>>
Player: I'd like to buy a pie!
<<if $gold_amount < 10>>
Baker: Well, you can't afford one!
<<endif>>
<<set $gold_amount to 5>>
Player: I'd like to buy a pie!
<<if $gold_amount < 10>>
Baker: Well, you can't afford one!
<<elseif $gold_amount < 15>>
Baker: You can almost afford one!
<<else>>
Baker: You can afford a pie!
<<endif>><<set $gold_amount to 5>>
<<set $reputation to 10>>
<<if $gold_amount >= 5 and $reputation >= 8>>
// The player is both rich and popular
Merchant: You're rich enough and popular enough for me to serve you!
<<elseif $gold_amount >= 10 or $reputation >= 10>>
// The player is either rich enough or popular enough to serve
Merchant: I wouldn't normally, but I'll serve you!
<<else>>
// The player is dirt
Merchant: You're neither rich enough nor important enough for me to serve!
<<endif>>Guard: You're not allowed in!
-> Sure I am! The boss knows me! <<if $reputation > 10>>
-> Please?
-> I'll come back later.Guard: Who goes there?
// If the player is a thief, a royal visitor, or a merchant, then
// go run the appropriate conversation for that. The player might be
// some combination of the three, so let them choose.
-> A thief! <<if $player_is_thief>>
<<jump Guard_Thief_Conversation>>
-> A royal visitor! <<if $player_is_royal_visitor>>
<<jump Guard_RoyalVisitor_Conversation>>
-> A merchant! <<if $player_is_merchant>>
<<jump Guard_Merchant_Conversation>>
// But if the player is NONE of those, then none of the options would have
// been available. We'll fall through to here.
Player: I'm nobody!
<<jump Guard_Nobody_Conversation>><<once>>
// The guard will introduce herself to the player only once.
Guard: Hail, traveller! Well met.
Guard: I am Alys, the guard!
<<endonce>><<once>>
Guard: Hail, traveller! Well met.
<<else>>
Guard: Welcome back.
<<endonce>><<once if $player_is_adventurer>>
// The guard knows the player is an adventurer, so say this line,
// but only ever once!
Guard: I used to be an adventurer like you, but then I took an arrow in the knee.
<<else>>
// Either the player is not an adventurer, or we already saw the
// 'arrow in the knee' line.
Guard: Greetings.
<<endonce>>Guard: Who are you? <<once>> // Show this line only one time
Guard: Go on, get lost!-> What's going on? <<once>>
Guard: The kingdom is under seige!
-> Where can I park my horse? <<once if $has_horse>>
Guard: Over by the tavern.
-> Lovely day today!
Guard: Uh huh.
-> I should go.
Guard: Please do.<<once>>
// Show long, character-establishing lines the first time
Guard: There's nothing new to report!
Guard: I've been at this post for hours, and I'm so bored.
Guard: I can't wait for the end of my watch.
<<else>>
// Show a more condensed version all other times
Guard: Nothing to report!
<<endonce>><<declare $player_is_friends_with_sam = $sam_relationship_score > 50>>
<<if $player_is_friends_with_sam>>
Sam: Hey buddy! Good to see you again.
<<else>>
Sam: Oh, it's you. What do you want?
<<endif>><<declare $has_enough_materials = $wood >= 5 && $nails >= 10>>
<<declare $has_required_skill = $carpentry_level >= 3>>
<<declare $can_build_chair = $has_enough_materials && $has_required_skill>>
<<if $can_build_chair>>
Craftsman: You've got everything you need to build that chair now.
<<else>>
<<if !$has_enough_materials>>
Craftsman: You'll need more materials first.
<<else>>
Craftsman: Your carpentry skills aren't quite there yet.
<<endif>>
<<endif>><<declare $is_evening = $game_hour >= 18 && $game_hour < 22>>
<<declare $town_shops_open = !$is_holiday && $game_hour >= 9 && $game_hour < 18>>
<<if $is_evening && !$town_shops_open>>
Innkeeper: Most shops are closed now, but you're welcome to stay here for the night.
<<endif>><<wait 2>>
<<setsprite ShipName happy>>
<<fade_out 1.5>>// Wait for 2 seconds
<<wait 2>>
// Wait for half a second
<<wait 0.5>>// Leave the dialogue now
<<stop>>
// Leave the dialogue if we don't have enough money
<<if $money < 50>>
Shopkeeper: You can't afford my pies!
<<stop>>
<<endif>>// Inside an if statement:
<<if dice(6) == 6>>
You rolled a six!
<<endif>>
// Inside a line:
Gambler: My lucky number is {random_range(1,10)}!=> Guard: Greetings, citizen.
=> Guard: Hello, traveller.
Guard: Stay vigilant. // runs after 'Hello, traveller.'
=> Guard: Hail, adventurer! <<if $player_is_adventurer>>
=> Guard: I used to be an adventurer like you, but then I took an arrow in the knee. <<once if $player_is_adventurer>>Homer: Hi, I'd like to order a tire balancing. #tone:sarcastic #duplicateHello there.
-> Hi!
-> What's up?Hello there. #lastline
-> Hi!
-> What's up?Mechanic: You're in orbit of Jupiter, at a rest station along the main tourism lines. There's a meteorite headed towards here that'll completely destroy this station in three days. #line:4c49c5
Mechanic: And you're a wayfinding robot bolted to the floor of said Jupiter Tourist Station. #line:5b6256
-> Bolted to the floor?! #line:f65d07
Mechanic: Yeah, like all tourist helper bots. #line:1b159b
-> Three days?! #line:40eaf7
Mechanic: More or less. I wouldn't make any long-term plans. #line:3a6c94title: Train_Dialogue
tags: #camera2 background:conductor_cabin
---
Why did you stop the train?
Now we won't arrive in time at the next stop!
===title: Node_Name
tracking: always
---
I know how many times you've been here.
===Oh, [wave]hello[/wave] there!Oh, hello there!Oh, [wave]hello [bounce]there![/bounce][/wave]Oh, [wave]hello [bounce]there![/wave][/bounce][wave/][wave][bounce]Hello![/][wave size=2]Wavy![/wave][wave=2]Wavy![/wave][wave wave=2]Wavy![/wave][mood=angry]Grr![/mood]
[mood="angry"]Grr![/mood]A [wave/] BA [wave trimwhitespace=false/] B
// (produces "A B")Here's some square brackets, just for you: \[ \]Here's some square brackets, just for you: [ ]Here's a backslash! \\Here's a backslash! \[nomarkup]Here's a big ol' [ bunch of ] characters, filled [[]] with square [[] brackets![/nomarkup]Here's a big ol' [ bunch of ] characters, filled [[]] with square [[] brackets!CharacterA: Hello!
CharacterB: Oh, hi![character name="CharacterA"]CharacterA: [/character]Hello!
[character name="CharacterB"]CharacterB: [/character]Oh hi!// In this example, the $gender variable is a string that
// contains either "m", "f", or "nb".
I think [select value={$gender} m="he" f="she" nb="they" /] will be there!
// Depending on the value of $gender, this line can appear
// as one of these possible options:
I think he will be there!
// or:
I think she will be there!
// or:
I think they will be there!<<if $apple_count == 1>>
You have one apple!
<<else>>
You have {$apple_count} apples!
<<endif>>PieMaker: Hey, look! [plural value={$pie_count} one="A pie" other="Some pies" /]!
// This will appear as either:
PieMaker: Hey, look! A pie!
// or:
PieMaker: Hey, look! Some pies!PieMaker: I just baked [plural value={$pie_count} one="a pie" other="% pies" /]!
// This will appear as, for example:
PieMaker: I just baked a pie!"
// or:
PieMaker: I just baked 4 pies!"Runner: The race is over! I came in [ordinal value={$race_position} one="%st" two="%nd" few="%rd" other="%th" /] place!
// This will appear as, for example:
Runner: The race is over! I came in 1st place!
// or:
Runner: The race is over! I came in 23rd place!title: Start
---
Wow!
My first ever Yarn script in Unity!
-> Gosh!
-> Incredible!
-> I'm amazed!
Anyway, time to get writing!
===IEnumerator MoveCube(float duration, Vector3 goal)
{
float accumulator = 0;
Vector3 start = this.transform.position;
while (accumulator < duration)
{
this.transform.position = Vector3.Lerp(start, goal, accumulator / duration);
yield return null;
accumulator += Time.deltaTime;
}
}void Start()
{
StartCoroutine(MoveCube(1, new Vector3(10, 0, 0)));
}async YarnTask MoveCube(float duration, Vector3 goal)
{
float accumulator = 0;
Vector3 start = this.transform.position;
while (accumulator < duration)
{
this.transform.position = Vector3.Lerp(start, goal, accumulator / duration);
await YarnTask.Yield();
accumulator += Time.deltaTime;
}
}async void Start()
{
await MoveCube(1, new Vector3(10, 0, 0));
}async YarnTask MoveCube(float duration, Vector3 goal, CancellationToken token = default)
{
float accumulator = 0;
Vector3 start = this.transform.position;
while (accumulator < duration && !token.IsCancellationRequested)
{
this.transform.position = Vector3.Lerp(start, goal, accumulator / duration);
await YarnTask.Yield();
accumulator += Time.deltaTime;
}
this.transform.position = goal;
}async void Start()
{
await MoveCube(5, new Vector3(10, 0, 0), this.destroyCancellationToken);
}CancellationTokenSource cancellationTokenSource;
async void Start()
{
cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(this.destroyCancellationToken);
await MoveCube(5, new Vector3(10, 0, 0), cancellationTokenSource.Token);
}async void Start()
{
cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(this.destroyCancellationToken);
try
{
await MoveCube(5, new Vector3(10, 0, 0), cancellationTokenSource.Token);
}
catch (System.Exception ex)
{
Debug.LogException(ex);
}
}void Update()
{
// if you release tab the cube movement is cancelled
if (Input.GetKeyUp(KeyCode.Tab))
{
cancellationTokenSource.Cancel();
}
}async YarnTask<int> RollDice(int totalNumberOfOptions, CancellationToken cancellationToken)
{
// animate in the dice
await FadeUpDiceAnimation(token);
// generate a random number
int number = UnityEngine.Random(0, count);
// animate the dice rolling to that number
await AnimateRoll(number, token);
// animate away the dice
await FadeDownDiceAnimation(token);
// returning the value
return number;
}public override async YarnTask<DialogueOption> RunOptionsAsync(DialogueOption[] dialogueOptions, CancellationToken cancellationToken)
{
int roll = await RollDice(dialogueOptions.Count, cancellationToken);
return dialogueOptions[roll];
}public class ButtonWaiter : MonoBehaviour
{
public YarnTaskCompletionSource buttonCompletion;
public Button button;
async void Start()
{
button.onClick.AddListener(() =>
{
buttonCompletion.TrySetResult();
});
}
}public override async YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token)
{
// configuring
var completionSource = new YarnTaskCompletionSource();
buttonWaiter.buttonCompletion = completionSource;
// fade in the UI, including our button
await FadeInUI();
// start waiting on the button
await buttonWaiter.Task;
// fade down the UI
await FadeOutUI();
}OnOptionSelected.TrySetResult(this.Option);// Wait for a selection to be made, or for the task to be completed.
var completedTask = await selectedOptionCompletionSource.Task;
// finally we return the selected option
return completedTask;ysc compile your_project.yarnprojectGame (Node2D)
├── YarnDialogueRunner (Node)
├── CanvasLayer
│ ├── LinePresenter (PanelContainer)
│ │ └── VBoxContainer
│ │ ├── CharacterLabel (Label)
│ │ ├── TextLabel (RichTextLabel)
│ │ └── ContinueIndicator (Label)
│ └── OptionsPresenter (PanelContainer)
│ └── OptionsContainer (VBoxContainer)extends Node2D
@onready var dialogue_runner := $YarnDialogueRunner
@onready var line_presenter := $CanvasLayer/LinePresenter
@onready var options_presenter := $CanvasLayer/OptionsPresenter
func _ready():
dialogue_runner.add_presenter(line_presenter)
dialogue_runner.add_presenter(options_presenter)
dialogue_runner.start_dialogue("Start")# On any node in your scene tree:
func _yarn_command_shake(intensity: float) -> void:
# <<shake 2.5>>
# Runs instantly, dialogue continues immediately
camera.shake(intensity)
func _yarn_command_fade(duration: float) -> Signal:
# <<fade 0.5>>
# Returns a Signal -- dialogue waits for it to complete
var tween = create_tween()
tween.tween_property($Overlay, "modulate:a", 1.0, duration)
return tween.finished
func _yarn_command_wait(seconds: float) -> Signal:
# <<wait 1.5>>
return get_tree().create_timer(seconds).timeoutclass_name Character
extends Node2D
func _yarn_command_move(destination: String) -> Signal:
var target := get_node("/root/Game/Waypoints/" + destination)
var tween := create_tween()
tween.tween_property(self, "position", target.position, 1.0)
return tween.finishedfunc _ready():
var library := dialogue_runner.get_library()
library.register_instance_command("move", Character)
library.set_target_root(self)func _ready():
dialogue_runner.add_function("player_health", _get_health, 0)
dialogue_runner.add_function("has_item", _check_item, 1)
func _get_health() -> float:
return player.health
func _check_item(item_name: String) -> bool:
return inventory.has(item_name)<<if player_health() < 50>>
You're not looking so good.
<<endif>>
<<if has_item("key")>>
The door opens.
<<endif>>
You have {format("{0}", player_health())} health remaining.<<declare $coins = 0>>
<<declare $player_name = "Adventurer">>
<<declare $has_sword = false>>
<<set $coins = $coins + 50>>
Welcome, {$player_name}! You have {$coins} coins.var coins = dialogue_runner.variable_storage.get_value("$coins")
dialogue_runner.variable_storage.set_value("$player_name", "Hero")dialogue_runner.dialogue_started.connect(func(): show_dialogue_ui())
dialogue_runner.dialogue_completed.connect(func(): hide_dialogue_ui())
dialogue_runner.node_started.connect(func(name): print("Entered: ", name))
dialogue_runner.node_completed.connect(func(name): print("Left: ", name))
dialogue_runner.unhandled_command.connect(func(text): print("Unknown: ", text))extends YarnDialoguePresenter
func run_line(line: YarnLine) -> Variant:
$Label.text = line.get_plain_text()
visible = true
# Wait for player to click
await $Button.pressed
visible = false
return null
func run_options(options: Array[YarnOption]) -> int:
# Build your own UI, return the index of the selected option
for i in options.size():
var btn = Button.new()
btn.text = options[i].get_plain_text()
btn.pressed.connect(func(): selected = i)
$Container.add_child(btn)
await $Container.get_child(0).pressed # Simplified
return selecteddialogue_runner.add_presenter(my_custom_presenter)dialogue_runner.saliency_strategy = YarnRandomBestLeastRecentlyViewedSaliencyStrategy.new()Learn how to use the Yarn Spinner brand in your game, and how to acknowledge your use of Yarn Spinner.
Learn how to use Yarn Spinner for Visual Studio Code as your Yarn editor.
/// How many times the player has visited the shop
<<declare $shop_visits = 0>>title: Note_Reminder
style: note
color: red
position: 100,200
---
TODO: Add more dialogue options for the shopkeeper.
===title: EvilPath
color: purple
---title: Volcanos
cluster: MainTopics
---title: ForestScene
image: forest.png
---Learn how to use the Live Share Extension with Yarn Spinner for Visual Studio Code.

.yarn files using the Live Share extension and VIsual Studio Code.yarn files.yarn files together.Learn about nodes and lines in Yarn Spinner scripts.
title: Start
---
Narrator: Hi, I'm the narrator for the documentation!
===Mae: Well, this is great.
Mae: I mean I didn't expect a party or anything
Mae: but I figured *someone* would be here.
Mae: ...
Mae: Welcome home, Mae.This is a line of dialogue, without a character name.
Speaker: This is another line of dialogue said by a character called "Speaker".title: Start
---
Navigator: The quantum fluctuations are intensifying. We need to jump now.
Captain: But the calculations aren't complete. We could end up anywhere.
Navigator: The wormhole is collapsing. It's now or never.
Captain: Fine. Initiate jump sequence.
Navigator: Something's wrong. We're being pulled backward...
Captain: That's impossible. Unless...
Navigator: We're arriving before we left. We've become our own rescue mission.
===Learn to use options, which allow your players to choose lines of dialogue.
title: Start
---
Navigator: The quantum fluctuations are intensifying. We need to jump now.
Captain: But the calculations aren't complete. We could end up anywhere.
Navigator: The wormhole is collapsing. It's now or never.
Captain: Fine. Initiate jump sequence.
Navigator: Something's wrong. We're being pulled backward...
Captain: That's impossible. Unless...
Navigator: We're arriving before we left. We've become our own rescue mission.
-> Captain: Let's alter our trajectory and break this temporal loop!
-> Captain: We must complete the cycle. Our past selves depend on it.
===title: Start
---
Navigator: The quantum fluctuations are intensifying. We need to jump now.
Captain: But the calculations aren't complete. We could end up anywhere.
Navigator: The wormhole is collapsing. It's now or never.
Captain: Fine. Initiate jump sequence.
Navigator: Something's wrong. We're being pulled backward...
Captain: That's impossible. Unless...
Navigator: We're arriving before we left. We've become our own rescue mission.
-> Captain: Let's alter our trajectory and break this temporal loop!
-> Captain: We must complete the cycle. Our past selves depend on it.
Navigator: Ayee! We're all going to die!
-> Captain: Nonsense! Keep yourself together!
-> Captain: AHHHH! We're all going to die!
===-> Captain: Let's alter our trajectory and break this temporal loop!
-> Captain: We must complete the cycle. Our past selves depend on it.-> Captain: Nonsense! Keep yourself together!
-> Captain: AHHHH! We're all going to die!title: Start
---
Navigator: The quantum fluctuations are intensifying. We need to jump now.
Captain: But the calculations aren't complete. We could end up anywhere.
Navigator: The wormhole is collapsing. It's now or never.
Captain: Fine. Initiate jump sequence.
Navigator: Something's wrong. We're being pulled backward...
Captain: That's impossible. Unless...
Navigator: We're arriving before we left. We've become our own rescue mission.
-> Captain: Let's alter our trajectory and break this temporal loop!
Navigator: Risky, Captain. We'd be writing ourselves out of existence.
-> Captain: We must complete the cycle. Our past selves depend on it.
Navigator: Then we're doomed to repeat this moment... forever.
===title: Start
---
Navigator: The quantum fluctuations are intensifying. We need to jump now.
Captain: But the calculations aren't complete. We could end up anywhere.
Navigator: The wormhole is collapsing. It's now or never.
Captain: Fine. Initiate jump sequence.
Navigator: Something's wrong. We're being pulled backward...
Captain: That's impossible. Unless...
Navigator: We're arriving before we left. We've become our own rescue mission.
-> Captain: Let's alter our trajectory and break this temporal loop!
Navigator: Risky, Captain. We'd be writing ourselves out of existence.
-> Captain: Damnit, Navigator! Nothing can stop me existing!
Navigator: *sigh* Very well, Captain.
-> Captain: By gods! You're right!
Navigator: But it's only solution, I fear.
-> Captain: We must complete the cycle. Our past selves depend on it.
Navigator: Then we're doomed to repeat this moment... forever.
-> Captain: If we're doomed, at least we'll be remembered as heroes.
Navigator: .. if anyone remembers us at all
-> Captain: Forever... forever... forever...
Navigator: Sir?
-> Captain: We must do it!
Navigator: As always, sir, you're right.
===-> Captain: Let's alter our trajectory and break this temporal loop!
-> Captain: We must complete the cycle. Our past selves depend on it.-> Captain: Damnit, Navigator! Nothing can stop me existing!
-> Captain: By gods! You're right!-> Captain: If we're doomed, at least we'll be remembered as heroes.
-> Captain: Forever... forever... forever...
-> Captain: We must do it!Learn to use the jump command to move the narrative between nodes.
title: Start
---
Navigator: Where to, Captain?
-> Captain: I want to go back to earth!
Navigator: Earth it is sir.
Navigator: This jump will take us 10 hours.
Navigator: Permission to jump, sir?
-> Captain: Granted, let's go!
Navigator: On it, sir.
-> Captain: Not yet. Just wait a moment.
Navigator: Standing by, sir.
-> Captain: Second star to the left!
Navigator: Can you be more specific?
title: Start
---
Navigator: Where to, Captain?
-> Captain: I want to go back to earth!
<<jump Earth>>
-> Captain: Second star to the left!
<<jump SecondStar>>
Navigator: Being a Navigator sure is hard work!
===
title: Earth
---
Navigator: Earth it is sir.
Navigator: This jump will take us 10 hours.
Navigator: Permission to jump, sir?
-> Captain: Granted, let's go!
Navigator: On it, sir.
-> Captain: Not yet. Just wait a moment.
Navigator: Standing by, sir.
<<jump Done>>
===
title: SecondStar
---
Navigator: Can you be more specific?
-> Captain: I cannot, no.
Navigator: Right away, sir.
-> Captain: ... that one *gestures*
Navigator: Very good, sir.
<<jump Done>>
===
title: Done
---
Navigator: Being a Navigator sure is hard work!
===<<jump>> command being visualised in the Graph View.Learn about detour and return, which let you temporarily move to another node, then return.
title: Guard
---
Guard: Have I told you my backstory?
-> Yes.
Guard: Oh. Well, then.
-> No?
<<detour Guard_Backstory>>
Guard: Anyway, you can't come in.
===
title: Guard_Backstory
---
Guard: It all started when I was a mere recruit.
// (five minutes of exposition omitted)
===title: Guard
---
Guard: Have I told you my backstory?
-> Yes.
Guard: Oh. Well, then.
-> No?
<<detour Guard_Backstory>>
Guard: Anyway, you can't come in.
===
title: Guard_Backstory
---
Guard: Do you want the detailed version or the short version?
-> Detailed.
<<detour Guard_Detailed_Backstory>>
Guard: I hope you enjoyed learning all that.. Anyway...
-> Short.
Guard: Right, well, I was a recruit, then I wasn't.
===
title: Guard_Detailed_Backstory
---
Guard: It all started when I was a mere recruit.
// (five minutes of exposition omitted)
// (other stuff happens)
Guard: Want to hear more?
-> Yes.
-> No.
<<return>>
Guard: (speaks more garbage)
===<<detour>> command being visualised in the Graph View.Learn about storing data using variables in Yarn Spinner Scripts.
/// The name of the player.
<<declare $playerName = "Reginald the Wizard">>
/// The number of gold pieces that the player has.
<<declare $gold = 42>>
/// Is the door to the dungeon unlocked?
<<declare $doorUnlocked = false>><<set $greeting to "Hello, Yarn!">>// Set some initial values in some variables
<<declare $myCoolNumber = 7>>
<<declare $myFantasticString = "wow, text!">>
// Now change them!
<<set $myCoolNumber to 8>>
<<set $myFantasticString to "incredible!">>// Set some initial values in some variables
<<declare $myCoolNumber = 7>>
<<declare $myFantasticString = "wow, text!">>
// This will NOT work, because you can't change types!
<<set $myCoolNumber to "eight">>
<<set $myFantasticString to 42>>// Stores 3 inside $numberOfSidesInATriangle
<<set $numberOfSidesInATriangle = 2 + 1>>
// Store 4 inside $numberOfSidesInASquare
<<set $numberOfSidesInASquare = $numberOfSidesInATriangle + 1>>// This will NOT work, because you can't add a string and a number:
<<set $broken = "hello" + 1>><<declare $aNumber = 42>>
<<declare $aString = "This is my string.">>
<<set $aString = string($aNumber)>><<set $variableName to "a string value">>
The value of variableName is {$variableName}.The value of variableName is a string value.Learn the principles behind our storylets and saliency features.
Barry: Oh is that so?
=> Alice: Yep.
=> Alice: Of course it is!
=> Alice: Why would you think otherwise?Barry: Oh is that so?
Alice: Of course it is!Barry: Oh is that so?
=> Alice: Yep.
=> Alice: Of course it is! <<if $barry_suspicion > 3>>
=> Alice: Why would you think otherwise? <<if $barry_suspicion > 5>>
=> Alice: I am not talking without my lawyer <<if $barry_suspicion > 5 && $knows_barry_is_cop>>Barry: Oh is that so?
Alice: I am not talking without my lawyerBarry: Oh is that so?
=> Alice: Yep.
Barry: lol lmao, thanks nerd.
=> Alice: Of course it is! <<if $barry_suspicion > 3>>
=> Alice: Why would you think otherwise? <<if $barry_suspicion > 5>>
=> Alice: I am not talking without my lawyer <<if $barry_suspicion > 5 && $knows_barry_is_cop>>
Barry: Why would you need a lawyer?
Alice: *cool silence*title: Barry
---
Barry: Oh is that so?
<<jump Alice>>
===
title: Alice
when: always
---
Alice: Yep.
Barry: lol lmao, thanks nerd.
===
title: Alice
when: $barry_suspicion > 3
---
Alice: Of course it is!
===
title: Alice
when: $barry_suspicion > 5
---
Alice: Why would you think otherwise?
===
title: Alice
when: $barry_suspicion > 5
when: $knows_barry_is_cop
---
Alice: I am not talking without my lawyer
Barry: Why would you need a lawyer?
Alice: *cool silence*
===Learn about saliency and saliency strategies, which let you control how line groups and node groups select which content to run.
<<set_saliency first>>
<<set_saliency random>>
<<set_saliency best>>
<<set_saliency best_least_recent>>
<<set_saliency random_best_least_recent>>Learn about the terminology and assets used to work with Yarn Spinner Scripts in Unity.
This example project demonstrates making a simple dialogue-based game when beginning with only an empty Unity scene.
title: Start
---
This is an example of a Yarn Spinner script. Write your dialogue here!
===title: Start
---
/// Whether Capsley like you or not. This starts true, but may change.
<<declare $capsley_likes_you = true as bool>>
/// The player's name. The player chooses this. It starts empty.
<<declare $player_name = "" as string>>
Capsley: Hello, I am Mr Capsley.
Capsley: Who are you then?
-> I'm Capsule, but my friends call me "Tic Tac". No idea why...
<<set $player_name to "Tic Tac">>
-> The name's Triquandle.
<<set $player_name to "Triquandle">>
-> Pyramid. Why - who wants to know?
<<set $player_name to "Pyramid">>
<<set $capsley_likes_you to false>>
<<if $capsley_likes_you>>
Capsley: Nice to meet you {$player_name}!
<<else>>
Capsley: No need to be so rude...
Capsley: Maybe you should be called Grumpy {$player_name}.
<<endif>>
===YarnCommand attributepublic class CharacterMovement : MonoBehaviour {
[YarnCommand("leap")]
public void Leap() {
Debug.Log($"{name} is leaping!");
}
}<<leap MyCharacter>>
// will print "MyCharacter is leaping!" in the console// Note that we aren't subclassing MonoBehaviour here;
// static commands can be on any class.
public class FadeCamera {
[YarnCommand("fade_camera")]
public static void FadeCamera() {
Debug.Log("Fading the camera!");
}
}<<fade_camera>>
// will print "Fading the camera!" in the console[YarnCommand("walk")]
public void Walk(GameObject destination, bool dancing = false) {
var position = destination.transform.position;
// If the second parameter is used in the command,
// and it's "true" or "dancing", use a dance
// animation
if (dancing) {
// set animation to a dance
} else {
// set animation to a regular walk
}
// walk the character to 'position'
}<<walk MyCharacter StageLeft>> // walk to the position of the object named 'StageLeft'
<<walk MyOtherCharacter StageRight dancing>> // walk to StageRight, while dancingpublic class CustomCommands : MonoBehaviour {
// Drag and drop your Dialogue Runner into this variable.
public DialogueRunner dialogueRunner;
public void Awake() {
// Create a new command called 'camera_look', which looks at a target.
// Note how we're listing 'GameObject' as the parameter type.
dialogueRunner.AddCommandHandler<GameObject>(
"camera_look", // the name of the command
CameraLookAtTarget // the method to run
);
}
// The method that gets called when '<<camera_look>>' is run.
private void CameraLookAtTarget(GameObject target) {
if (target == null) {
debug.Log("Can't find the target!");
}
// Make the main camera look at this target
Camera.main.transform.LookAt(target.transform);
}
}<<camera_look LeftMarker>> // make the camera look at an object named LeftMarkerpublic class CustomWaitCommand : MonoBehaviour {
[YarnCommand("custom_wait")]
static IEnumerator CustomWait() {
// Wait for 1 second
yield return new WaitForSeconds(1.0);
// Because this method returns IEnumerator, it's a coroutine.
// Yarn Spinner will wait until onComplete is called.
}
}<<custom_wait>> // Waits for one second, then continues runningusing Yarn.Unity;
public class CustomWaitCommand : MonoBehaviour {
[YarnCommand("custom_wait")]
static async YarnTask CustomWait() {
// Wait for 1 second
await YarnTask.Delay(1000);
// Yarn Spinner will wait until this method
// returns, before continuing the dialogue.
}
}public class AdderFunction {
[YarnFunction("add_numbers")]
public static int AddNumbers(int first, int second)
{
return first + second;
}
}<<if add_numbers(1,1) == 2>>
One plus one is {add_numbers(1, 1)}
<<endif>>[YarnCommand("give_item")]
public void GiveItem(string itemName, int quantity = 1)
{
// Your code to give the player an item
Debug.Log($"Giving {quantity} {itemName}(s) to the player");
}NPC: Here, take this potion.
<<give_item "health_potion">>public class CustomDialoguePresenter : DialoguePresenterBase
{
public override void RunLine(LocalizedLine dialogueLine, Action onDialogueLineFinished)
{
// Your custom line presentation code
Debug.Log($"Presenting line: {dialogueLine.TextWithoutCharacterName}");
// Call this when the line presentation is complete
onDialogueLineFinished();
}
}
Learn about the samples we provide for Yarn Spinner for Unity.
Learn how to visually theme one of our provided Dialogue Presenters.

title: Alice
---
Player: Hello
Alice: Hello, this is a sample showing off retheming the base dialogue presenters
-> Neat
Alice: right?
-> Dull
Alice: rude!
===Learn how to make a Phone Chat view and explore our Phone Chat Sample.
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;
}Learn how to make options that timeout after a period of inactivity from the user.
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 to add voice and localisation to your Yarn Spinner-powered projects.
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.

Learn how to use markup in your Yarn Spinner Scripts, and respond to it in Unity by styling your narrative's text.
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.
Learn how to build and use an advanced saliency system when you use Yarn Spinner for Unity.
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 about Text Animator for Yarn Spinner.
Text Animator for Yarn Spinner has got me feeling [shake]goosebumps[/shake]!// Connect this to your Text Animator Yarn Typewriter component in the scene
public TextAnimatorYarnTypewriter Typewriter;
public override async YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token)
{
// Tell the typewriter that content is going to appear.
Typewriter.PrepareForContent(line.Text);
// (At this point, you could fade up your dialogue
// UI or perform any other animation needed enter
// 'dialogue mode' in your game. In this example,
// we'll just proceed right into showing the text.)
// Show the text. Text Animator for Yarn Spinner
// will handle all of the appearance and effects
// for you!
await Typewriter.RunTypewriter(text, token.HurryUpToken).SuppressCancellationThrow();
// Wait until the line has been ended.
await YarnTask.WaitUntilCanceled(token.NextContentToken).SuppressCancellationThrow();
// Signal that the content is going away.
Typewriter.ContentWillDismiss();
}This line has a <rainb>Text Animator effect</rainb> on it.This line has a [rainb]Text Animator effect[/rainb] on it.This line has a <rainb>Text Animator effect</rainb>[pause /] on it.This line has [custom type="appearance"]tags[/custom] of different [custom type="behaviour"]types[/custom]This line has {custom type="appearance"}tags{/custom} of different <custom type="behaviour">types</custom>✅ = supported and recommended
❓ = supported but not recommended
❌ = not supportedThe guide and documentation for the paid Yarn Spinner for Unity add-on, Speech Bubbles for Yarn Spinner.

Learn how to use the Speech Bubbles, from the Speech Bubbles for Yarn Spinner Add-On Package.
The guide and documentation for the paid add-on, Dialogue Wheel for Yarn Spinner.

Learn how to install the Dialogue Wheel for Yarn Spinner Package.
Learn about the Dialogue Runner, which runs the contents of your Yarn Scripts and delivers lines, options and commands to your game.
Learn about Dialogue Presenters, which present dialogue content to the user in Yarn Spinner for Unity.
Learn about the Line Advancer, a component that can signal to a Dialogue Presenter that the user wants to proceed to the next piece of content.
This guide teaches you how to use the Variable Storage system.
// getting the dialogue runner
var runner = FindObjectOfType<Yarn.Unity.DialogueRunner>();
if (runner == null)
{
Debug.LogWarning("Was unable to find a dialogue runner");
return;
}
// attempting to find a float called $gold
if (runner.VariableStorage.TryGetValue<float>("$gold", out var gold))
{
// we found the variable
// it's value has been stored into the gold parameter
// we can now use the gold variable
if (gold > 100)
{
Debug.Log("they are rich, unlock the Player Is Rich cheevo!");
}
}
else
{
// we failed to find $gold
Debug.LogWarning("Was unable to find a number value for $gold");
}// getting the dialogue runner
var runner = FindObjectOfType<Yarn.Unity.DialogueRunner>();
if (runner == null)
{
Debug.LogWarning("Was unable to find a dialogue runner");
return;
}
// modifying the value of the players gold, they now have 25 gold
runner.VariableStorage.SetValue("$gold", 25);/// the number of gold coins the player has
<<declare $gold = 0>>/// <summary>
/// the number of gold coins the player has
/// </summary>
public float Gold
{
get
{
if (this.TryGetValue<float>("$gold", out var gold))
{
return gold;
}
return 0;
}
set => this.SetValue<float>("$gold", value);
}<<enum TimeOfDay>>
<<case Morning>>
<<case Evening>>
<<endenum>>public enum TimeOfDay
{
/// <summary>
/// Morning
/// </summary>
Morning = 0,
/// <summary>
/// Evening
/// </summary>
Evening = 1,
}public class GameStateManager : Monobehaviour
{
private List<IConvertible>[] gameState;
private MinPerfectHash hashFunction;
public void Initialise(string[] keys) { ... }
public bool TryGetValues<T>(string variableName, out T[] result) { ... }
public T[] ValuesAt<T>(int index) { ... }
public void AddValue(string variableName, IConvertible value) {}
public void AddValueAt(int index, IConvertible value) {}
public void Rollback(string variableName) { ... }
public void RollbackAt(int index) { ... }
public void Clear() { ... }
}public void Initialise(string[] keys)
{
// generate a new hash function for the specific list of keys
var keyHashGenerator = new VariableHashKeySource(keys);
hashFunction = MinPerfectHash.Create(keyHashGenerator, 1);
// now we make the values array of the size of the hash function
gameState = new List<IConvertible>[hashFunction.N];
}public void AddValue(string variableName, IConvertible value)
{
// if we don't have a hash function we can't find the index for where to add the new value
if (hashFunction == null)
{
throw new InvalidOperationException();
}
var index = hashFunction.IndexOf(variableName);
var values = gameState[index];
// if we have no list at this index that means that the key is invalid
if (values == null)
{
throw new ArgumentException();
}
// finally we can now add the new value to the list
values.Add(value);
gameState[index] = values;
}public bool TryGetValues<T>(string variableName, out T[] result)
{
if (hashFunction == null)
{
result = default;
return false;
}
var index = hashFunction.IndexOf(variableName);
if (gameState[index] == null)
{
result = default;
return false;
}
// adding all the elements to an array, letting you see the variable history
var extant = gameState[index];
T[] values = new T[extant.Count];
for (int i = 0; i < extant.Count; i++)
{
values[i] = (T)extant[i];
}
result = values;
return true;
}public class GameStateManager : Yarn.Unity.VariableStorageBehaviour
{
private List<IConvertible>[] gameState;
private MinPerfectHash hashFunction;
public Yarn.Unity.YarnProject project;
private List<IConvertible>[] gameState;
private MinPerfectHash hashFunction;
public void Initialise(string[] keys) { ... }
public bool TryGetValues<T>(string variableName, out T[] result) { ... }
public T[] ValuesAt<T>(int index) { ... }
public void AddValue(string variableName, IConvertible value) {}
public void AddValueAt(int index, IConvertible value) {}
public void Rollback(string variableName) { ... }
public void RollbackAt(int index) { ... }
public override void Clear() { ... }
public override void SetValue(string variableName, string stringValue) { ... }
public override void SetValue(string variableName, float floatValue) { ... }
public override void SetValue(string variableName, bool boolValue) { ... }
public override bool TryGetValue<T>(string variableName, out T result) { ... }
public override bool Contains(string variableName) { ... }
public override void SetAllVariables(Dictionary<string, float> floats, Dictionary<string, string> strings, Dictionary<string, bool> bools, bool clear = true) { ... }
public override (Dictionary<string, float> FloatVariables, Dictionary<string, string> StringVariables, Dictionary<string, bool> BoolVariables) GetAllVariables() { ... }
}public void Initialise(string[] keys)
{
// if we don't have a project we will have to abort initialisation
if (project == null)
{
Debug.LogError("Unable to initialise variable storage as there is no Yarn Project set");
return;
}
// getting the initial values from the project
// and merging that with the rest of the game keys
var initialValues = project.InitialValues;
List<string> yarnKeys = new();
yarnKeys.AddRange(keys);
yarnKeys.AddRange(initialValues.Keys);
// generate a new hash function for the specific list of keys
var keyHashGenerator = new VariableHashKeySource(yarnKeys.ToArray());
hashFunction = MinPerfectHash.Create(keyHashGenerator, 1);
// now we make the values array of the size of the hash function
gameState = new List<IConvertible>[hashFunction.N];
// now we can add into the array the default yarn values
foreach (var pair in initialValues)
{
uint index = hashFunction.IndexOf(pair.Key);
gameState[index] = new List<IConvertible>()
{
pair.Value,
};
}
}public override void SetValue(string variableName, string stringValue)
{
AddValue(variableName, stringValue);
}
public override void SetValue(string variableName, float floatValue)
{
AddValue(variableName, floatValue);
}
public override void SetValue(string variableName, bool boolValue)
{
AddValue(variableName, boolValue);
}public override bool TryGetValue<T>(string variableName, out T result)
{
// if we don't have a hash function we can't find the index
if (hashFunction == null)
{
result = default;
return false;
}
// getting the index of the variable name
var index = hashFunction.IndexOf(variableName);
var values = gameState[index];
// if we have no value at that index we also can't return it
if (values == null)
{
result = default;
return false;
}
// grabbing the last element
var value = values[^1];
// checking it is actually of type T
if (!typeof(T).IsAssignableFrom(value.GetType()))
{
result = default;
return false;
}
// returning it
result = (T)value;
return true;
}public override bool Contains(string variableName)
{
// if we don't have a hash function we can't see if we have a value for that key
if (hashFunction == null)
{
throw new InvalidOperationException();
}
var index = hashFunction.IndexOf(variableName);
var values = gameState[index];
return values == null;
}public override void SetAllVariables(Dictionary<string, float> floats, Dictionary<string, string> strings, Dictionary<string, bool> bools, bool clear = true)
{
if (hashFunction == null)
{
throw new InvalidOperationException();
}
foreach (var pair in floats)
{
AddValue(pair.Key, pair.Value);
}
foreach (var pair in bools)
{
AddValue(pair.Key, pair.Value);
}
foreach (var pair in strings)
{
AddValue(pair.Key, pair.Value);
}
}public override (Dictionary<string, float> FloatVariables, Dictionary<string, string> StringVariables, Dictionary<string, bool> BoolVariables) GetAllVariables()
{
if (hashFunction == null)
{
throw new InvalidOperationException();
}
if (project == null)
{
throw new InvalidOperationException();
}
Dictionary<string, float> allFloats = new();
Dictionary<string, string> allStrings = new();
Dictionary<string, bool> allBools = new();
foreach (var key in project.InitialValues.Keys)
{
var index = hashFunction.IndexOf(key);
var values = gameState[index];
// if we have no list at this index that means that the key is invalid
if (values == null)
{
continue;
}
var value = values[^1];
if (value is bool v)
{
allBools[key] = v;
continue;
}
if (value is float f)
{
allFloats[key] = f;
continue;
}
if (value is string s)
{
allStrings[key] = s;
continue;
}
}
return (allFloats, allStrings, allBools);
}Learn about updating a Unity project to use Yarn Spinner 3 when it's already using Yarn Spinner 2.
This page shows you how to install Yarn Spinner for Godot, the Godot integration for running Yarn and Yarn Spinner scripts in your Godot-based games.

addons directory in a local copy of Yarn Spinner for Godot.addons directory in. <Import Project="addons\YarnSpinner-Godot\YarnSpinner-Godot.props" />.csproj for your project.Learn about Yarn Projects, which group your scripts together for use in a Dialogue Runner.























































































































































































--- and the body of the node follows. The body is where the lines are kept.
// Yarn script example of custom "wavy text" markup.
Oh, [wave]hello[/wave] there!
// After compiling, text will look like: "Oh, hello there!"
// And then the resulting markup data will look like:
// - name: "wave"
// - position: 4
// - length: 5title: Start
---
Narrator: Hi, I'm the narrator for the documentation!
===title: Adventure
---
Narrator: We're going to go on an adventure!
===
title: Cave
---
Narrator: Let's look inside the spooky cave...
===title: Start
---
Narrator: Hi, I'm the narrator for the documentation!
<<jump Adventure>>
===title: Adventure
---
Narrator: We're going to go on an adventure!
-> OK! Let's go!
<<jump Cave>>
-> I don't want to go on an adventure...
Narrator: Oh, OK then.
===var stringListener = VarStorage.AddChangeListener("$stringVar", (string value) =>
{
Debug.Log($"$stringVar changed to " + value);
});stringListener.Dispose();public override void SetValue(string variableName, float floatValue)
{
// (code for updating the variable omitted)
// Notify the change listeners that this variable changed
NotifyVariableChanged(variableName, floatValue);
}variableStorage = GameObject.FindObjectOfType<InMemoryVariableStorage>();
float testVariable;
variableStorage.TryGetValue("$testVariable", out testVariable);
variableStorage.SetValue("$testVariable", testVariable + 1);static int myNumber = 10;
// note: all Yarn Functions must be static
[YarnFunction("getMyNumber")]
public static int GetMyNumber() {
return myNumber;
}
// Yarb Commands can be static or non-static
[YarnCommand("setMyNumber")]
public static void SetMyNumber(int newNumber) {
myNumber = newNumber;
}My number is { getMyNumber() }!
<<setMyNumber 999>>
But now it's { getMyNumber() }!Player: Sure would be nice if I could take a breather [pause /] right now.if (player presses SPACE)
then find the nearest NPC
get that NPC's dialogue node name
call DialogueRunner.StartDialogue() with the NPC's dialogue node
disable player movement


















Learn about creating enums, which allow you to create variables that are constrained to a specific set of values.
<<enum Food>>
<<case Apple>>
<<case Orange>>
<<case Pear>>
<<endenum>>// Declare a new variable with the default value Food.Apple
<<declare $favouriteFood = Food.Apple>>
// You can set $favouriteFood to the 'apple', 'orange' or 'pear'
// cases, but nothing else!
<<set $favouriteFood to Food.Orange>>
// You can use enums in if statements, like any other type of value:
<<if $favouriteFood == Food.Apple>>
I love apples!
<<endif>>
// You can even skip the name of the enum if Yarn Spinner can
// figure it out from context!
<<set $favouriteFood = .Pear>>public bool TryGetValue<T>(string variableName, out T result);
public void SetValue(string variableName, string stringValue);
public void SetValue(string variableName, float floatValue);
public void SetValue(string variableName, bool boolValue);
public void Clear();
public bool Contains(string variableName);Learn about using node groups, which allow Yarn Spinner to choose which content to run, depending on conditions.
Learn how to install the Speech Bubbles for Yarn Spinner Package.
title: Guard
when: once
---
Guard: You there, traveller!
Player: Who, me?
Guard: Yes! Stay off the roads after dark!
===
title: Guard
when: always
---
Guard: I hear the king has a new advisor.
===
title: Guard
when: $has_sword
---
Guard: No weapons allowed in the city!
===
.unitypackage

=> 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;
}
}Quick Start Guide










title: Start
---
Narrator: What brings to the pool?
-> Cleaning
I have come to clean the pool.
Narrator: Ah, just as I thought.
-> I'm a pool cleaner
Narrator: I know.
<<jump End>>
-> I was actually lying.
Narrator: Oh, I see.
<<jump End>>
-> Treasure
I am looking for the lost treasure of... the pool.
Narrator: There is no treasure in the pool.
-> WHAT!?
WHAT?! WHY NOT? I WAS TOLD THERE WAS TREASURE HERE!
Narrator: Nope.
<<jump End>>
-> Oh, okay.
Oh, well, I guess I'll go.
Narrator: OK, bye!
<<jump End>>
-> I know.
I know, I just wanted a swim.
Narrator: In you get, then!
<<jump End>>
-> No reason
I have a fetish for pool cleaning equipment.
Narrator: Whatever floats your boat...
-> Thanks.
Narrator: Uh uh.
<<jump End>>
-> Commerce
I'd like to buy a pool.
Narrator: Well, it's not for sale.
Narrator: Go away.
<<jump End>>
-> Swimming
I'm here to go for a swim.
Narrator: Well, you can't.
<<jump End>>
===
title: End
---
Narrator: Anyway...
Narrator: Have a nice day!
<<stop>>
===title: Start
---
Narrator: Show me the positions on the Six-Segment Wheel?
-> Left Top #lt
-> Left Middle #lm
-> Left Bottom #lb
-> Right Top #rt
-> Right Middle #rm
-> Right Bottom #rb
===title: Start
---
Narrator: Show some options?
<<set-opt 1 3>>
-> I'm an option!
-> I'm another option!
-> I'm yet another option!
-> Me too!
===DefaultDialogueSystem instantiated into your scene.DialogueRunner.DialogueRunner to start automatically.

DefaultDialogueSystem.tscn.Learn how to use the Automatic-Layout Dialogue Wheel, from the Dialogue Wheel for Yarn Spinner Add-On Package.








Introduction.yarnStart.



title: Start
---
Player: Hello there!
NPC: Oh, hello! How can I help you today?
-> I need information.
NPC: What would you like to know?
-> I'm just browsing.
NPC: Feel free to look around!
-> Actually, I should go.
NPC: No problem. Come back anytime!
<<jump End>>
===
title: End
---
Player: Thanks for your help!
NPC: You're welcome! Have a nice day!
===


























