Storylets for ink + Unity

[This is a preview of content found in the upcoming book, Dynamic Story Scripting with the ink Scripting Language, featured in Chapter 12 on the topic of procedural storytelling with ink + Unity.]

Review of Storylets

Storylets are an attractive narrative pattern for generating or managing dynamic narrative experience in interactive projects because they provide a way to pick the next available content to play next. Within such a system, there are three core aspects (Short, 2019):

  • Content.
  • Prerequisites.
  • Effect on state.

Each storylet contains its content, what the prerequisites are for its availability, and how it effects the collection of values (state). Written out, this might appear as the following example:

  • Content: The princess escapes the tower.
  • Prerequisites: tower is in story; princess is in story; princess is in the tower
  • Effects: Tower is no longer in the story; princess is no longer in the tower

In the above example, the storylet would become available to be used next in a story if its three prerequisites are met: the tower is in the story, the princess is in the story, and the princess is in the tower.

Translated into pseudo-code, this same example might appear as the following (assuming story is an object containing properties tower, princess, and princess_in_tower):

  • Content: The princess escapes the tower
  • Prerequisites: story.tower == true && story.princess == true && story.princess_in_tower == true
  • Effects: story.tower = false; story.princess_in_tower = false

Each ink story is a storylet

ink does not provide a way to create complex data structures. That’s not its purpose. It is designed, and excels at, scripting narrative experiences. It is designed to provide tools to authors for creating branches of stories and managing how content is expressed to a user. However, when ink is used with Unity via the ink-Unity Integration plugin, this allows Unity to handle complex data in C# and allows ink to succeed at what it does best: scripting narratives.

In ink, variables (created using the keyword VAR) and functions are global. This means they can be accessed by any connected code and, with variables, their values used in different places. With the application programming interface (API) provided by the ink-Unity Integration plugin, this means C# code can potentially access any variables in an ink story or call its functions to process a change or receive back some output. This effectively creates a bridge (via the API) where the ink story handles its internals, but exposes data to Unity.

In storylet terms, this encapsulation of variables and functions within each ink story allows each be its own storylet. It has its own content and potentially changes values internally. However, as it comes to the last aspects of storylets, the effect on a state across multiple storylets, this poses a problem. How does an individual ink story let a data structure in C# know its values have changed? And, as a result of those changes, should the availability of an ink storylet also change?

To start to address these issues requires understanding the ink-Unity Integration plugin provides an event-based interface for changes to variables following an observer model per ink story. Variables can be observed and, should their values change, a function will be notified. Using the API provided, each ink story can report its changes to a C# data structure in Unity:

Variable change events reporting to Unity

This begins to solve the first issue. As variables are changed, those changes are being saved to Unity, which now contains the state for the entire system. Immediately, this poses a new problem: how does an ink story know a global state value was changed? This requires knowing variables are global in ink and can be accessed by Unity via the API provided by the plugin. For each variable update reported by another story, all the other stories perform two checks and then potentially update their own value:

  1. Do I have a variable by this name?
  2. If so, is this new value reported by Unity different than its existing value?
  3. If so, update the value reported from Unity internally!

The first step in this process is important for a reason that may not be obvious. ink is a scripting language and, when run, variables either exist or they do not. Attempting to change the value of a variable that does not exist will cause an error in Unity! This means the first step is to check if the ink story contains the variable by its name. If it does, move to the next step. If no, move on to the next story.

The second step in the above process flow prevents an endless loop from occurring. If it was not included, any variable update would trigger an endless chain of updates as individual update would trigger new updates across all stories until the project crashed. We need to make sure we are only ever overwriting a new value when it is different than the old one.

In the new process, when a variable updates in one ink story, this triggers a check across all ink stories currently part of a collection. Each is then asked if it contains the variable and if its current value is different. If so, it updates:

First ink story reports an update, which triggers the other stories to check

In the new process, Unity acts as an information exchange system between the various ink stories as part of a collection. When it detects a change in one story (via the event-based API), it relays the change to the other stories, potentially prompting them to update their values.

Updates and Availability

In the new system of waiting for events and relaying changes between stories, both the more global Unity state and individual ink stories are constantly synchronized with each other. Any one update to an observed variable changes all others using the same name in all other stories. However, there is one piece missing for the storylet set of aspects. How does Unity know if an individual ink story is available? How are its prerequisites expressed?

Each storylet need only care about its own prerequisites. Translated into this system, each ink story would contain a function whose purpose is to report back a Boolean value. If the conditional expression is true, yes, the ink story is available. If not, no.

In ink, this might look like the following example:

VAR rose_relationship = 0
VAR geoff_relationship = 0

// Content here!

== function available()
~ return (rose_relationship > 50 && geoff_relationship < 40)

In the above code, the function available() compares the variables rose_relationship and geoff_relationship against its own prerequisites. If the conditional expression is true, based on the internal values as updated by Unity’s information exchange, the ink story is available for usage.

Creating this simple function allows each ink story handle its own variables, story branching, and content. It need only contain a function for returning if it is available. All other work happens at the Unity level as it detects changes in variable values and updates other stories.