I’ve always loved classic adventure games! There are some great GDC videos on narrative in video games by Jon Ingold from inkle. The studio has developed a scripting language for branching narrative, called “ink”. The system is completely text-based, with a very lean C# API for Unity integration.
In this project, I explored ways to map the dialog / choice structure of an ink story file to a more interactive setting, with locations, actions, and objects. You can play the result, a miniature escape room style adventure, in any desktop browser by clicking on the image below:
There isn’t that much gameplay, the software architecture implications are very interesting, though:
- The game can be played entirely in text form – greatly simplifying automated tests.
- Story and game logic are strictly separated from the visuals, akin to an MVC architecture:
- The ink description is the model.
- The view (game graphics) is updated from the model’s state.
- Player actions are sent back to the model, which in turn updates to advance the story.
- Completely text-based story content would allow for procedural generation, too.
The experiment had a big influence on designing the narrative backend(s) for my later projects Amy Shifts Gears and Amy’s World.
If you’re into it, here is a more detailed breakdown of the approach:
- In ink, a game is constructed around pieces of content (text) presented to the player, and sets of choices to direct flow to other pieces of content.
- Programming logic is provided to allow state tracking and flow control.
- There are no concepts for “rooms”, “objects”, “containers”, or “actors”.
In “Escape”, I made extensive use of custom markup in the story file, for instance:
- The choice
[!push!button]
will be presented as an action menu on the “button” object in the scene. - Location changes are mapped to choices: a choice with the
!trigger!
markup will be executed when the player enters the respective area in the scene. - The
#player
tag sends text to a thought balloon. #cut
sends text to a cut scene screen (at the beginning and end of the game)
Here is the complete ink file driving the game:
// items that can be picked up
LIST items = nothing, soap
VAR inventory = ()
// states
LIST playerState = walking, (inBed), soaked, soaped
LIST bedState = (down), up
LIST manholeState = (closed), open, examined
LIST showerState = (off), activated
// locations
LIST locations = (none), BED, CELL, SHOWER, SHAFT
VAR playerLocation = BED
VAR prevPlayerLocation = none
VAR bedItems = (BED)
VAR cellItems = (CELL)
VAR showerItems = (SHOWER, soap)
VAR shaftItems = (SHAFT)
-> hub
// functions for moving around
// did we enter location x for the first time?
=== function first(x) ===
~ return x == 1
// did we just enter the current location from somewhere else?
=== function enter ===
~ return prevPlayerLocation != playerLocation
=== function moveTo(x) ===
~ playerLocation = x
=== function removeItem(ref itemList, item) ===
~ itemList -= item
// pick up an item from current player location
=== function getItem(item) ===
{ playerLocation:
- BED: ~removeItem(bedItems, item)
- CELL: ~removeItem(cellItems, item)
- SHOWER: ~removeItem(showerItems, item)
- SHAFT: ~removeItem(shaftItems, item)
}
~ inventory += item
// main story hub, switching flow to the available locations
=== hub ===
{ playerLocation:
- BED: <- bed(->hub)
- CELL: <- cell(->hub)
- SHOWER: <- shower(->hub)
- SHAFT: <- shaft(->hub)
}
<- inventory_actions(->hub)
~ prevPlayerLocation = playerLocation
-> DONE
=== bed(->back) ===
#cut
E S C A P E
The same dream, night after night.
A prison cell.
No way out.
One day after the other, your life passes by.
"Hope" is just a faint shadow of a word without meaning.
%%
You awake from restless slumber...
%%
%%
(any key to start)
%PAUSE%
#player
Zzzzz.... hrm?
Where am I?
Oh.
\*Sigh*
How do I get out of here?
%WAIT%
* [!anyaction!Get up] Ouch, my back! This "bed" is killing me. #player
~ moveTo(CELL)
- -> back
=== cell(->back) ===
+ [!trigger!shower]
~moveTo(SHOWER)
* [!pull!bed]
~bedState = up
+ {bedState ? up && manholeState ? (closed,examined)} [!pull!grating]
Uaarrggh! Heavy! #player
~manholeState = open
-> examine_manhole
* (examine_manhole) {bedState ? up} [!examine!grating]
{examine_manhole == 1 : {manholeState ? closed : The gutter, covered with a grating.|This shaft looks REALLY tight!}} #player
~manholeState += examined
+ {manholeState ? open} [!use!manhole]
{playerState ? soaped:{Are you serious?? It stinks!|It still smells!|Ewww!}|It's too tight, I'll get stuck!}#player
+ + {playerState ? soaped} [!use!manhole]
Aah, all right.
Down the rabbithole! #player
%WAIT%
~moveTo(SHAFT)
+ + [!noaction!manhole] -> back
- -
-
-> back
=== shaft(->back) ===
#cut
Carefully, you lower your soaked, soaped, slippery body down the hole, head first.
One last push, and off you go!
Holding your breath, you're hoping for the best.
%%
Freedom, at last!
%%
%%
(any key to restart - but why would you?)
%END%
->END
=== shower(->back) ===
{enter():{Take a shower, maybe?|}} #player
* [!examine!button] A big red button. #player
* [!push!button]
~ showerState = activated
Yikes! #player
Why didn't I undress first?
~ playerState = soaked
* [!examine!soap] Disgusting.#player
How can SOAP be so dirty?!
* [!get!soap] This slimy thing gives me the creeps. #player
~ getItem(soap)
+ [!trigger!leaveshower]
~moveTo(CELL)
-
-> back
=== inventory_actions(->back)
* {inventory ? soap && !(playerState ? soaked)} [!use!soap] It's too dry! #player
+ {inventory ? soap && playerState ? soaked} [!use!soap] Yay, foam bubbles! #player
Hmmm - lavender?
~inventory -= soap
~playerState = soaped
- ->back