Branching narrative is difficult to write. The trick is to balance player freedom against the combinatorial explosion that would result if we attempted to model all possible options.
Emily Short’s storylets are an excellent way to think about modeling branching narrative in a game. They’re simple, and flexible, and I’d like to provide a set of example for making them a bit more concrete by using them alongside YarnSpinner, a real-world storytelling engine that can be used in a variety of game engines.
YarnSpinner bills itself as a dialogue engine, but that’s only one of its possible uses. By default, it can only deliver lines of text to a game one line at a time, but that’s really all that you need. In games where you would like to show the player multiple paragraphs before presenting them with a decision, you can just buffer lines until hitting the next decision point. My reason for using YarnSpinner is twofold. First, I’m basing my next project on it, so I’m going to have to explain it to my writers, but also that it’s simple, easy to use, and powerful enough to be used to script an entire game. Also, you can try it out for yourself in the browser. I encourage you to copy-and-paste the examples in this article into the YarnSpinner browser tool and give each of them a try for yourself.
Storylets
Emily Short provides a very general definition of a storylet, which I’ve paraphrased below. A storylet is:
- a piece of content,
- with prerequisites that must be met before it can be presented to the player
- which may act on the game world after its conclusion.
I like this definition. It’s flexible and general enough to encompass a lot of story engines, including anything you can dream up using YarnSpinner. Immediately after the blog post, Emily gives a bunch of examples of structures that can be captured by storylets. Her examples are provided via a bunch of diagrams which I have a hard time following. I’d like to lay them out concretely in YarnSpinner to make things easier to follow.
The examples we’ll show start simple, then become more complex and use more YarnSpinner features.
The Gauntlet
As Emily Short defines it, a gauntlet is a linear story where the player is confronted with a series of choices where any wrong move spells “doom” for the player. It’s important to note that “doom” is relative, ranging from game-ending consequences (“You Died”), to failure. What seems to be important about the Gauntlet is that the player only gets one shot at success. If the player can try again, it’s seems like it’s really a Gauntlet, more like an “Uphill Climb”.
Here is the diagram Emily uses to explain this sort of branching narrative.
The player continues through a linear story, presented with wrong turns at most steps along the way. To render a gauntlet in YarnSpinner, you could use code like the following.
title: Start
---
The bomb's timer counts down menacingly before you.
You carefully unscrew the front panel to reveal a pile of wires.
-> Cut the red wire // DOOM!
BOOM! the bomb explodes
<<stop>>
-> Cut the white wire
You cut the white wire, but to your horror, the timer keeps ticking!
-> Shake it! Shake the bomb! // DOOM!
You pick up the bomb and shake it violently before it explodes.
The last thing you see is a red hot fireball blooming between your hands.
<<stop>>
-> Uh... are there any other wires?
Only the red wire. Do you cut it?
-> No! Try something else! // DOOM!
You've run out of time! The bomb explodes.
<<stop>>
-> Yes! Cut the red wire.
You cut the red wire! The timer stops abruptly at 0:01.
You did it!
===
Notice that each of the ‘DOOM’ options uses YarnSpinner’s <<stop>>
command to abruptly halt execution of the current
node. We would also set any variables needed to update the game state just before playing the next line. What’s
interesting about this code is that the ‘happy path’ isn’t indented all the way. We could have written the exact same
story with more indentation like below, but doing so makes a mess and gains us nothing.
title: Start
---
The bomb's timer counts down menacingly before you.
You carefully unscrew the front panel to reveal a pile of wires.
-> Cut the red wire // DOOM!
BOOM! the bomb explodes
<<stop>>
-> Cut the white wire
You cut the white wire, but to your horror, the timer keeps ticking!
-> Shake it! Shake the bomb! // DOOM!
You pick up the bomb and shake it violently before it explodes.
The last thing you see is a red hot fireball blooming between your hands.
<<stop>>
-> Uh... are there any other wires?
Only the red wire. Do you cut it?
-> No! Try something else! // DOOM!
You've run out of time! The bomb explodes.
<<stop>>
-> Yes! Cut the red wire.
You cut the red wire! The timer stops abruptly at 0:01.
You did it!
===
Branch & Bottleneck
A Branch and Bottleneck structure is a way to give the player freedom before something important and devastating happens. Graphically, this structure can be laid out like below:
In a branch and bottleneck structure, certain critical events will happen at predetermined points of the story. The points themselves might be random, but ordered to ensure the narrative flows logically. For instance, the game might have the main villain arrive in town at any point between Sept 1st and Sept 15th, then murder the mayor 3-5 days later and immediately try to make their escape. A one-node example of a branch and bottleneck structure could look like the below:
title: Start
---
<< set $player_health to 100 >>
Dr. Robotnik fires up his flamethrower hovercraft and chases you down!
-> Run across the bridge to escape!
You run across the bridge, leaving Robotnik in your dust!
But the bridge wasn't built for your speed and the ground falls out from under you!
-> Jump! Try to grab the edge!
You jump! Flying through the air, but it's too far!
You smack the ground and feel your bones crunch.
<< set $player_health to $player_health-10 >>
You pass out, and wake up moments later. Robotnik found you!
-> Tuck and roll to break your fall!
Good thinking! You tuck and roll to break your fall, but
you pass out anyway as you hit the ground.
You wake up and Dr. Robotnik is above you!
-> Run under the hovercraft and get him from behind!
Dr. Robotnik dips his hovercraft and it smacks you in the head.
You pass out, and wake up moments later.
-> Run behind a hill and escape!
Alright! You run behind the hill.
But you're going too fast and you fall down a pit trap!
You pass out, then wake up and find Dr. Robotnik flying above you!
Only one thing to do. Time to fight. // Bottleneck
===
This narrative structure uses YarnSpinner’s indentation feature to deeply nest extra options which only become available
after the player has made certain decisions. Each of these could be broken off into separate nodes using the <<jump>>
command, which may make organization of large stories easier. That would look like the below.
title: Start
---
<< set $player_health to 100 >>
Dr. Robotnik fires up his flamethrower hovercraft and chases you down!
-> Run across the bridge to escape!
<< jump Bridge>>
-> Run under the hovercraft and get him from behind!
Dr. Robotnik dips his hovercraft and it smacks you in the head.
You pass out, and wake up moments later.
-> Run behind a hill and escape!
Alright! You run behind the hill.
But you're going too fast and you fall down a pit trap!
You pass out, then wake up and find Dr. Robotnik flying above you!
<< jump Fight>>
===
title: Bridge
---
You run across the bridge, leaving Robotnik in your dust!
But the bridge wasn't built for your speed and the ground falls out from under you!
-> Jump! Try to grab the edge!
You jump! Flying through the air, but it's too far!
You smack the ground and feel your bones crunch.
<< set $player_health to $player_health-10 >>
You pass out, and wake up moments later. Robotnik found you!
<< jump Fight >>
-> Tuck and roll to break your fall!
Good thinking! You tuck and roll to break your fall, but
you pass out anyway as you hit the ground.
You wake up and Dr. Robotnik is above you!
<< jump Fight >>
==
title: Fight
---
Only one thing to do. Time to fight. // Bottleneck
===
This option has the exact same story structure, but uses more nodes. This is an advantage in large, sprawling stories,
where branching and deeply nesting can become unwieldy. It also helps cut down on redundancy in case there are multiple
ways that the player can find themselves on the bridge in this encounter. For this example, we might make falling in the
pit a random event and give the player the option to double-back to the bridge after running behind the hill, rewriting
the -> Run behind a hill and escape!
option in the Start
node to look like the below:
-> Run behind a hill and escape!
Alright! You run behind the hill.
<< if dice(3) == 3 >>
But you're going too fast and you fall down a pit trap!
You pass out, then wake up and find Dr. Robotnik flying above you!
<< else >>
There's nothing here, but Robotnik shouts "I'm gonna get you!"
-> Stay here and wait.
You wait. Dr. Robotnik flies over the hill, flamethrower ablaze!
<< jump Fight >>
-> Double-back to the bridge; he'll never see that coming!
You run a few laps around hill before speeding off towards the bridge!
<< jump Bridge >>
<< endif >>
Sorting Hat
The sorting hat structure is one of the simplest to model, used in narrative tales like Echo and other visual novels. The main idea is that at some point the player makes a crucial choice which changes their path forever. Visually, it looks like this.
In games that take this approach to story-telling, each separate branch is often called a ‘route’ through the story. Many games that use a route-based storytelling structure develop and may even release each route separately, as the story is written. One downside to using routes is that the player has to play the game multiple times in order to see all of the story. On the other hand, this approach increases replayability, as each route is essentially a separate game.
In YarnSpinner, the Choose
node is simply modeled as a single node which jumps to the start nodes of multiple other
routes based on the player’s choice. A small example is below, where the Start
node also acts as our choose node.
title: Start
---
Host: "Welcome to Lusitania's most eligible bachelor!" We're here with our contestant, Hiro Protagonist!
Host: Hiro! Which of our lovely ladies will you pick first?
-> Bachelorette 1
<< jump B1 >>
-> Bachelorette 2
<< jump B2 >>
-> Bachelorette 3
<< jump B3 >>
===
title: B1
---
Player: "Bachelorette 1, I often like long walks on the beach, what do you do for fun?"
Miyazaki-chan: I like to read~
// continue with Miyazaki-chan's route
===
title: B2
---
Player: "Bachelorette 2, do you like walking in the rain?"
Tonada-chan: When I have an umbrella, yes~
// continue with Tonada-chan's route
===
title: B3
---
Player: "Bachelorette 3, what's your favorite food?"
Akihiko-chan: Gyoza!~
// continue with Akihiko-chan's route
===
Loop & Grow
Loop and Grow is one of the most interesting and complicated story structures that Emily Short explains. Of course, YarnSpinner is more than capable of handling it. To make use of it, we have to carefully design our nodes so that they are able to tell a slightly different story depending on the game’s current state. The only tool available to do this is YarnSpinner’s variables.
For instance, let’s imagine telling a story around Adventure Time’s Dungeon Train – a magical train dungeon that circles around itself forever in a loop without stopping. Each time we visit a room, the creatures in the room should react to the player differently.
title: Start
---
<< set $player_health = 100 >>
You begin your ride along the Dungeon Train!
<< jump Car1 >>
===
title: Car1
---
The first car contains a magical unicorn.
<< if $player_unicorn_attacked >>
It remembers the treachery of your last visit!
The unicorn charges!
-> I jump to the left!
You juke left, but the unicorn anticipated your feint!
You are gored!
<< set $player_health to $player_health - 10 >>
You escape into the next car, bleeding!
-> I jump to the right!
You jump right, but the unicorn goes left!
He crashes into the wall of the train!
<< set $unicorn_crashed to true >>
You run into the next car!
<< jump Car2 >>
<<else>>
Do you fight it?
-> I slay the unicorn!
Unicorns are not so easily slain!
<< set $player_unicorn_attacked to true >>
He dodges your attack and you fly through to the next car.
<< jump Car2 >>
-> I bow, for I am pure of heart!
<< if $player_ct_unicorn_horn != 0 >>
The unicorn bows in return. You may continue!
<< else >>
The unicorn thanks you for sparing its life.
He grants you one of his horns.
<< set $player_ct_unicorn_horn to 1 >>
You continue to the next car
<< endif >>
<< jump Car2 >>
<<endif>>
===
title: Car2
---
The second car is filled with the rest of the dungeon...
Writing it is an exercise for the reader...
Let's assume it goes well and the player makes it back to the first car.
<< jump Car1 >>
===
This is a much more complicated narrative structure than the other nodes we’ve seen so far. If you play it, you’ll notice that all the player can do is go around and around. If this weren’t a small example, we would be doing even more work like checking to see if the player’s health has hit zero or evaluating a win condition.
Entry Nodes
Loop & Grow forms a basis for open-world narrative. Every time the player revisits an area, they have an opportunity to learn or find something new. To enable the player to find something new in the same node, an ’entry node’ can be used.
title: Start
---
In start!
<< set $next = "A" >>
<< jump EntryNode >>
===
title: EntryNode
---
At entry node! Jumping to {$next}
<< jump {$next} >>
===
title: A
---
You have entered node A
===
The above script sets the value of the $next
variable to the name of the node we want to jump to; node A
. Then, we
jump to the EntryNode, which executes its script before jumping to node A
. This allows us to re-use the EntryNode
in
multiple locations, cutting down on the amount of duplicate code we have to write. In large projects, the same $next
variable can also be re-used by multiple EntryNode
s.
We can use this technique to check for a random encounter each time the player visits a certain area, like in the below script.
title: Start
---
In start!
<< set $next = "Spooky_Forest" >>
<< jump EntryNode >>
===
title: EntryNode
---
<< if dice(6) == 6 >> // roll a six-sided die
A challenger approaches!
<< jump Challenger >>
<< endif >>
<< jump {$next} >>
===
title: Spooky_Forest
---
Welcome to the spooky forest.
===
title: Challenger
---
The challenger attacks! You died! :(
===
Time Cave
A time cave is a narrative structure where each choice the player makes forks them off into another universe. The problem with Time Caves is the amount you have to write grows exponentially with the amount of time you’d like a player to spend in your world.
Needless to say, this exponential growth is dangerous for large projects. But smaller portions of a larger project can be modeled this way, so it’s worth covering.
title: Start
---
You are in the kitchen
-> Do the dishes.
You start to do the dishes, a snake jumps from the sink.
-> Kill it.
You grab the snake by the throat and smack it around. It dies.
-> Put it in the fridge.
-> Throw it outside.
-> Don't.
The snake latches onto your arm.
-> Continue doing the dishes.
-> Pull it off!
-> Open the fridge.
You open the fridge. It's empty.
-> Go grocery shopping.
You don't have your keys.
-> Find your keys.
You start searching for your keys.
-> Just walk!
You walk to the store.
-> Step inside.
You step inside the fridge. It's cold in here.
-> Keep breathing.
You hyperventilate a bit, but it doesn't make you feel any better.
-> Hold your breath.
You hold your breath.
This works well when the text used to respond to each option is small, like in the example above, but it can easily become hard to navigate when nodes become larger. To make large Time Caves manageable, the nesting depth of each node needs to be controlled, by forking off new nodes and continue the story from there.
Gated Storylets
Emily Short mentions two types of storylets that only become available to the player based on their progress:
Resource-gated storylets, and skill-gated storylets. In YarnSpinner, both can be modeled by an if
statement. We can
choose whether to put the if
statement before using jump
, like so:
<< if $total_stone >= 100 >>
<< jump Rock_troll >>
<< endif >>
This has a flaw, though. It means that every point in the game at which it’s possible to meet the Rock troll has to have the same if statement. This can become hard to manage; suppose during playtesting we realize it’s way too easy to acquire 100 stone, and we need to adjust it to 1000; we’d have to update every location in our code.
A simpler approach uses an entry node for the Start node to check for this condition.
title: Start
---
<< set $total_stone = 0 >>
You are in the mines.
-> Mine some rock.
You mine a rock.
<< set $total_stone = $total_stone + 1 >>
<< jump EnterStart >>
===
title: EnterStart
---
<< if $total_stone > 100 >>
<< jump Rock_troll >>
<< endif >>
<< jump Start >>
===
title: Rock_troll
---
A rock troll bursts through the door!
"You're eating all my rock", he says.
He munches you. You die.
===
Now for every jump
, we have to remember to jump to EnterStart
instead of Start
, but the if
statement is in one
place.
Conclusion
Emily also mentions other structures on her blog, but they’re simpler than the ones we’ve covered here. Linear Storylets and Branching Storylets Between Gameplay Levels both involve writing code that lies outside of YarnSpinner, to trigger storylets at the appropriate time. Likewise, Narrative Deckbuilding involves altering random encounter tables based on the outcome of previous storylets. The rest of Emily’s blog contains a lot of analysis of branching narrative structures which any writer could find useful or inspiring.
Regarding YarnSpinner, the inclusion of branching and jumps in the language makes it Turing-complete of its own accord. All this means is that it can be used to perform arbitrary computations, but Turing-completeness has other implications as well. Since every Turing-complete language is as powerful as every other, YarnSpinner can be used to encode any Storylet structure that can be executed by a computer. This makes it a powerful tool for narrative story-telling, but also admits a lot of complexity, which can make editing and maintenance a pain. Taking the time to carefully organize your large YarnSpinner projects from the outset will pay dividends down the line.