ink + Unity: Working with common utilities and story templates

[This is a continuation of content found in the book, Dynamic Story Scripting with the ink Scripting Language, featured in Chapter 11 and Chapter 12]

When working with ink, it can be tempting to make an entire project one long file. This can often lead to thousands of lines of ink code and complications with debugging. Trying to find the exact place where a problem is happening can be incredibly frustrating.

In large projects, especially those with multiple areas, chapters, or story sections, it can often make much more sense to divide up the projects into different project files. These can then be dynamically loaded in Unity as needed. Not only will this decrease the overall memory usage, but it can make testing and playing through ink story content much as easier.

Breaking up common utilities

The first step of breaking up a large project file into smaller ones is identifying what the common utilities are across the project. For example, I often implement functions in ink to increase or decrease values like currency, health, or other values. That way, I can call the function and it will handle any additional checks and value updates:

Health example

VAR health = 0

// Story content

== function update_health(amount)
~ health += amount

In the above example, the function update_health() acts as a utility for the project. Instead of handling the changing of the variable health in different places, the function solves this problem for me. All I need to do is call the function and pass it a positive number to increase the health or a negative one to decrease it.

Once a utility has been isolated, it should be moved to a different file. A useful naming scheme might be something like utilities.ink that contains all the utility functions:

chapter1.ink

INCLUDE utilities.ink
VAR health = 0

// Story content

utilities.ink

// Updates the health variable
== function update_health(amount)
// Both positive and negative values can be used
~ health += amount

In the above, updated project, there are now two files. One contains the story content for Chapter 1 and the other contains the utility functions for Chapter 1.

Utilities per section

With functions broken out into utilities.ink, it can be tempting to use one central file for the whole project. For small projects, this might work well. For large projects, those with thousands of lines or more, this is the same problem as before in a new form. Instead, utilities can be broken out per section.

Something to always keep in mind about how ink works with Unity is that each source file in ink becomes a compiled JSON file. In other words, multiple ink files become one JSON. This also means utilities.ink will be bundled with each separate JSON file. To keep the overall size smaller, a different utilities.ink file can be created per section of the story. That way, the different utility functions for that particular story section can be managed on their own:

Diagram of separate utility files per section of a larger project

Mapping Unity objects to ink utility functions

Utility functions can do more than be accessed by ink. Because functions are global in ink, they can also be accessed by the Unity API directly. This allows for creating objects and defining methods in Unity to match common or repeating utility functions in ink.

In the example of having a variable named health and utility function update_health() as something appearing across multiple sections of a story, I might decide to define multiple functions for working with the variable:

utilities.ink

// Updates the health variable
== function update_health(amount)
// Both positive and negative values can be used
~ health += amount

// Sets the health variable
== function set_health(amount)
~ health = amount

In the above, updated code, there are now two functions. The first, update_health(), updates the value and the second, set_health(), allows for setting a new health value. In looking at them, it may not seem immediately obvious why both are needed. After all, it would seem like update_health() could happen the same task.

A context where the second function is helpful in in the setup of a new story section. For example, consider a scenario where health is changed in one section. When the next starts, because it is a different Story object in Unity, its value would be reset to 0. With the function set_health(), however, it could be set to the updated value from the previous section, carrying the value forward across sections of the story:

StorySection.cs

using Ink.Runtime;

public class StorySection
{
    private Story story;

    StorySection(string s, int previousHealth)
    {
        story = new Story(s);
        setHealth(previousHealth);
    }

    void setHealth(int i)
    {
        story.EvaluateFunction("set_health", i);
    }
}

In the above code, a health value can be passed to the new StorySection object as a second argument to the constructor. Internally, this will pass the value to the C# method setHealth(), which in turn calls the ink function set_health().

Using story templates

Having a common approach to organizing sections of a larger project, such as using utility files, also opens the door to a sister approach: story templates.

Games like Where the Water Tastes Like Wine (2018) and Wildermyth (2019), where the structure is the same across sections but the content is different, are great examples where templates can be quite powerful. The gameplay of multiple parts of those games are more or less the same. However, the content can be very different depending on choices made.

Along with utility functions, common variables can be isolated as well. For example, if the values health and money are shared across multiple sections, they could also be broken into their own separate files:

variables.ink

VAR health = 0
VAR money = 0

utilities.ink

// Updates the health variable
== function update_health(amount)
~ health += amount

// Sets the health variable
== function set_health(amount)
~ health = amount

// Updates the money variable
== function update_money(amount)
~ money += amount

// Sets the money variable
== function set_money(amount)
~ money = amount

chapter1.ink

INCLUDE utilities.ink
INCLUDE variables.ink

// Story content

In the new code, there are three files. The first contains the common variables. The second the common functions. The third is where the story content would be written. A new writer for the project could be given this template of three files. They could then reference the common utility functions and use the appropriate variables without needing to know them:

Example of story template files

2 thoughts on “ink + Unity: Working with common utilities and story templates

  1. Maybe I am missing the context from the rest of the book here, but I am missing the “why” of breaking up the common utilities. It’s already mentioned that the different files will be compiled into one json for the runtime to interact with, so why would this be any different? I suspect the Unity side connects these sections together, by having multiple instances of a Story, but I can’t make that up from this text.

    This question of “why” extends to the example of using a function for health. If it only changes a variable the example is completely arbitrary. A death check would really help to make that example actually serve its purpose.

    1. Dan Cox

      The purpose of breaking up the ink files is for easier debugging and testing. Testing a 10,000-line ink file is difficult. Testing ten 1000-line files designed for different part of a larger game is much easier. The more modularity you can introduce, the easier the overall maintenance.

      Although not made explicitly, the example of a function that adjusts a value illustrates how to separate functionality from their values. In a future revision, the name of the variable might change. There could also, as you mention, be an additional check involved when the value is updated. The name of the function would remain the same even if these additional changes are added.

Comments are closed.