Critical Play Project: Working with Ink + Unity

When researching different dialogue engines / narrative scripting languages for this project, I came across three: Fungus, Yarn and Ink. I had already used Ink before as my first step towards understanding scripting last Spring, so I decided to stick with it – learning a new language for the purposes of this project would have made for a good learning outcome, but I was conscious of time. Additionally, I wasn’t confident in how ‘deep’ Fungus and Yarn could go into under-the-hood mathematics, and I knew Ink could handle the kind of functional programming I was after. I didn’t expect the story to ‘go’ very far, but I did anticipate needing to access large lists and booleans across multiple scripts. Ink is flexible and hardy enough for that.

I set out an initial dialogue ‘flow’ (or ‘weave’) by writing ‘First line of dialogue’, ‘Second line of dialogue’ etc., with the player afforded three choices connected to ‘impel / repel / compel.’ 

I was also able to expose these different choices or reactions by prefixing each line with a command to show the current ‘intention’ values. In Ink this is achieved by writing the variable within curly brackets: ‘{inflection} First line of dialogue’ would produce a result of ‘3 First line of dialogue,’ if 3 were the value of the intention variable. This would prove invaluable in debugging the story’s sometimes finicky behaviour.

I also wrote a simple version of my weighting function, which would react to a player choice by calculating the ‘difference’ between the previous ‘partner intention’ and the new ‘player intention.’ Based on that difference, it would add or subtract ‘weight’ from three integer variables, one for each basic reaction. These weights would be totalled, then a random number generated between 0 and the total – if the number fell inside the range of one of the weighted ‘buckets’, then it would return that bucket’s reaction. Programming this variance into it meant that each runthrough would be different, and players, though able to influence the weighting significantly, would be unable to guarantee results.

Connecting Ink’s output to Unity was a cinch. The second scene (readthrough) was actually the easiest to create, as Ink’s out-of-the-box Unity plugin provided code for connecting story text to a text box and choices to instantiated buttons. It was easy to arrange the scene as I had imagined. 

Building the first scene (the script) was a little more difficult, as I actually had to instantiate buttons over where I imagined the chosen dialogue text would appear. The trick here was actually in the animation – by creating different prefabs for the button text and the dialogue text I could time the OnClick() disappearance of the former with the fade in of the latter. I also had to rewrite some of the cleanup code so that only the choices were removed on story refresh, rather than all the text.

Although the surface elements of connecting Ink to Unity were easy, I encountered many problems in connecting Ink’s functions to Unity. This was mostly a symptom of the difference in how Ink and C# run through their code. While C# runs quite predictably frame by frame, Ink (as I understand it) will only ‘output’ once it hits a ‘stop’ in the story flow (usually a player choice). What can happen as a result is that Ink can encounter a function call within its code, run that function, and print the next line of story text – but Unity might need to change something in the UI before the line is printed! I spent at least two days working out how to reliably get Unity to inject its changes into Ink before the story was loaded, and vice versa.

One of my solutions, which I regard as unsustainable, was to write the same ‘reaction’ function in both C# and in Ink. The version that previewed likely reactions was run on the Unity side, while the version that ‘committed’ choices to the story was run in the Ink story. This was tiresome to code (Ink, while similar in architecture to C#, replaces many of its symbols, so curly brackets become colons, if statements are dashes within curly brackets, etc. etc.), and were I not under deadline I would have liked to have spent a lot more time digging into a better approach. Something for next time.

Another difficulty was the semantic and functional difference between C# arrays and Ink’s lists. A C# array, while it can be coded to include multiple values, is generally referenced to check if it ‘includes’ a variable or object. If a variable or an object is in an array, then it ‘includes’ that variable or object. In Ink, every variable is both a value (an integer or a string, for example) and a boolean. This is useful for, say, organising a list of clues, then checking off whether a player has discovered them, or for progressing along a list of chronologically connected states (Monday, Tuesday, Wednesday…). What this means is that a list can ‘contain’ one hundred different variables, but they could all need to be declared ‘true’ before (for example) a random output function will recognise that they’re actually *in* the list. The solution in this case was to make a master list of verbs, then ‘add’ every verb to its relevant list at game start. It took an embarrassingly long time to figure this out, and in future games I will look for more elegant solutions. It might even be more useful to contain this sort of function in Unity entirely…

Once the UI feedback was in place, I realised that I didn’t need my earlier multiple choices; indeed, for the Ink script to work as I wanted it to, I would have to either present a single choice that output various values depending on previous choices and mouse position, or nine choices for each line of dialogue! Neither looked particularly easy to implement, but the nine choices seemed the least flexible and most time-consuming. I took to writing the code for the first option.

This took the form of extending the existing functions quite extensively, and I spent a lot of time doing tidying work, like converting variables from one format (-1, 0, 1 for altering the visuals of the game) to another (the list and array-friendly 0, 1, and 2). Having to update functions in two languages was extremely time-intensive, and I’ve definitely learned a lesson about not coding myself into a bind. In future I will plan out functions more completely, regardless of which language I’m using.

One final discovery (is discovery the right word for something you remember at the last possible minute?): since each ‘scene’ required Ink to function differently (early stories didn’t need access to the lists of verbs, for example), I had been creating new Ink stories for each scene. But, like coding a ‘class’ in C# to pass down functions to its children, Ink can ‘include’ different stories, removing the need to rewrite every function or preface each story with the same block of lists. Migrating my work into this format was time-consuming – were I to return to Ink as a language I would begin with a stricter project hierarchy, and use multiple ‘includes’ from the get-go.