Ink: Dialogue Trees

Last month, Inkle shared a post on their Patreon with an example of some code from their recent game Overboard! (2021). Within the code was a function called oneChoice() that limits the number of choices within a set. While seemingly simple, it solves a particularly interesting problem in Ink: dialogue trees!

Often, when working in Ink to create a narrative-based game, a set of dialogue responses might be needed. These may come from how the scene is created or a need to have a certain number of options for players. (Overboard! prefers to always have three options, for example!)

At first glance, the multiple-line alternative stopping may seem a good choice. Combined with threads, you might think two alternatives could be used in the following way:

-> loop
== loop
<- first_stopping
<- second_stopping
-> DONE

== first_stopping
{stopping:
    - * A
    - * B
}

== second_stopping
{stopping:
    - * A
    - * B
}

However, when using choices within a sequence, an explicit divert must always be used. The previous code is not valid and will not run. When diverts are added to the code, it becomes the following:

-> loop
== loop
<- first_stopping
<- second_stopping
-> DONE

== first_stopping
{stopping:
    - * A
        -> loop
    - * B
        -> loop
}

== second_stopping
{stopping:
    - * A
        -> loop
    - * B
        -> loop
}

The new code will progress both “trees” (a choice from the knot first_stopping and one from the knot second_stopping). However, because the result of a choice is the use of divert, each alternative will progress simultaneously. The first output will be both of the first options. Clicking either will progress both. Once one of them has been exhausted, the other will remain, but they cannot be separated:

Animation of stopping alternative running together

The use of the oneChoice() function fixes this issue, but it must be used within a structure supporting its usage. Choices are part of a knot or stitch. Thus, to know the current count of choices, they have to be separated. Each choice also has to be preceded by the use of the function to limit its usage and slowly move through the tree of options. The updated code including the function would appear as the following:

-> loop
== loop
<- first
<- second
-> DONE

== first
~ temp count = CHOICE_COUNT()
<- first_options

- (first_options)
* {oneChoice(count)} A
* {oneChoice(count)} B
- -> loop

== second
~ temp count = CHOICE_COUNT()
<- second_options

- (second_options)
* {oneChoice(count)} C
* {oneChoice(count)} D
- -> loop

=== function oneChoice(choiceCount) 
~ return choiceCount == CHOICE_COUNT()

In the new form, each use of a thread creates its own separate structure. Using the oneChoice() function before each option also limits their usage per structure in the order they appear. This has the added bonus of creating different buckets per thread, too. New options can be added to each separately without regard to the other. They operate on their own:

Animation of oneChoice() dialogue trees