Tiny game #3: Escape

It’s NaNoWriMo, and even though I’m not writing a novel, it’s the perfect month to work on a game with narrative!

There are some great GDC videos on narrative in video games by Jon Ingold from inkle. Over the years, they have developed a scripting language for games based on branching narrative called “ink”, which has recently been open-sourced. The system is completely text-based. They do provide a very lean C# API, too, so it’s almost a no-brainer to use ink for authoring the story backbone of a graphical adventure-type game!

In ink, a “game” is essentially 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”, “actors”, though. What about action adventure / rogue-like gameplay, where you wander around a dungeon, encounter NPCs, collect and use items?

So that was my challenge, and here’s the result:

click on the image to open the game in a new window – webgl-enabled browser required!

This is very much a proof of concept rather than a full game, but I am quite happy how it all worked out! Gameplay is completely driven by the following .ink text file:

// 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

(Disclaimer: There may well be better ways to do this – it’s my first time using ink!)

On the Unity side, incoming text is processed by a StoryAdapter wrapper around the ink API. Player choices in the text are mapped to actions in the game environment by custom markup, which I made up in a very ad hoc fashion. E.g.:

  • the choice [!push!button] will be presented as a menu on the “button” object in the scene, when the player approaches it.
  • Location changes are choices, too: a choice with the !trigger! markup will be executed when the player crosses a special invisible object in the scene.
  • The #player tag sends text to a balloon displayed as the player’s thoughts.
  • #cut sends text to a cut scene screen (at the beginning and end of the game)
  • … and so on.

What you end up with, is a sort of MVC architecture of a game: the ink description is the model, being visualized by the game graphics. The player actions in the scene are sent back to the model, which then updates itself to advance the story.

Now, this is really exciting! It means:

  • the game can be played entirely in its text form! You can paste the above text into the Inky editor and play through. It’s not going to be very pretty, due to all the markup, but…
  • … authoring story and game logic is a breeze after the foundation is laid, or at least a lot easier;
  • … the game can be tested without any UI, for instance using automated randomized picking of choices;
  • … games could also be completely or in part procedurally generated.

The one thing I didn’t get around to is dialogue, which is a real shame. But something has to be left for another game, right?