Using Ink for Conversations
by Zach and Kevin on September 19, 2019
Signs of the Sojourner is a narrative deck-builder where your deck is your character, and each conversation is a card game (currently crowdfunding on Indiegogo!). To produce the majority of the text in Signs of the Sojourner, we're using Inkle's Ink scripting language. This has been beneficial in that it allows our writer, Kevin, to have enough control over the narrative state to work fairly independently.
Each conversation in the game is identified by one knot (this is Ink parlance that means a checkpoint in your script from which you can begin execution; for programmers, it's kind of like a goto label). When you start an event, the game will set the Ink runtime's "program counter" to this location.
Upon returning home for the first time, you'll encounter Elias. This conversation, which I'll use as an example, is identified by the knot:
=== event_elias_home
This knot is then further divided up into stitches (sub-knots), for which we have a strictly defined format. Because the game code directs the flow and routes to these as necessary, they need to exist in every event.
The first stitch we execute is the unimaginatively named init
stitch. For this event, it looks like:
= init
~ items += bartow_keyring
- -> DONE
You unconditionally get the keyring from Elias in this event, so the data updates right when the event starts. The UI that announces you received the item is shown later.
We then produce the title and intro text for the intro popup:
= title
Returning Home
- -> DONE
= intro
{
- surprise :
~ elias_negative++
{ch}Elias{_ch} comes up while you're unloading the truck. You didn't see him on the drive in, but now it seems like you avoided him. He's holding a box labeled {pl}BARTOW{_pl}.
- else :
~ elias_positive++
You find {ch}Elias{_ch} at home, sitting on the floor, surrounded by craft materials. Glue, markers, card stock, thin polystyrene sheets… He looks like he hasn't slept.
}
- -> DONE
Defining the title and intro blurb in Ink allows us to easily make them dynamic. In this example, the specific intro you see depends on if you choose to speak with Elias (surpsise == false
) or if you try to circumvent the conversation by packing up early (surprise == true
). Moreover, that choice affects your relationship with Elias (the elias_negative++
and elias_positive++
bits). The {ch}/{_ch}
and {pl}/{_pl}
tags are a convention we're using for text markup. Here ch
stands for "character", and pl
stand for "place". Content between these tags ultimately gets stylized in some way, whether it's a different color, bold, squiggly, etc. As of writing this post, both ch
and pl
cause the text to get rendered bold. We're still coming up with new tags to give the dialogue character.
The bulk of an event's text is in the strike and sequence text. You'll see sequence text both when a sequence begins and ends. Each sequence has its own stitch:
= sequence_0
{
- surprise :
Oh, there you are… I made these for the store while you were gone. {it}Bartow keyrings{_it}. Y'know, souvenirs. #exp neutral
- else :
While you were gone, I made something you can stock in the store: {it}Bartow keyrings{_it}! People <i>love</i> souvenirs! #exp happy
}
Just a memento to help travelers remember {pl}Bartow{_pl}. You don't owe me this time, ha ha… Unless they're really popular! #exp happy
- -> DONE
Each sequence stitch produces exactly two lines of text (one for the beginning and one for the end). We're using the surprise
flag again here to modify the tone of the dialogue. The #exp
tags trigger character sprite expression updates. The {it}
tag refers to "item", which also currently outputs bold text.
Each event has some number of sequence stitches defined, which determines how many sequences are required to complete the event. This event in particular has four in total.
Strikes, congruent to sequences, also have one stitch each. Strikes occur when you play a mismatched card with no shield or other effect to save it. Here's a strike stitch:
= strike_0(pc)
~ elias_negative++
{
- pc :
You must be tired. #exp confused
- else :
Sorry. I'm exhausted. #exp confused
}
- -> DONE
Currently each strike stitch produces one line of text. Here we're again updating your relationship status with Elias. Predictably, it deteriorates as you strike, but there are more interesting consequences later on. pc
refers to whether it's you, the player character, or not, who caused the strike.
When the event is over, you'll see an outro. There are two primary possibilities:
= concordance(symbol)
{
- symbol == circle :
You patiently field {ch}Elias'{_ch} questions about the road, even when he asks the same ones twice.
}
- -> DONE
= discordance(symbol)
{
- symbol == circle :
You answer question after question, but {ch}Elias{_ch} continues on, like he's searching for words he doesn't know.
}
- -> DONE
You'll see the discordance
outro if you "strike out." The outro text itself will also change based on the dominant symbol throughout the conversation. These symbols are passed from Unity, and narratively they're abstractions for certain conversation styles—empathetic, diplomatic, direct, creative, and so on.
Finally, the exit
knot:
= exit
- -> DONE
Which does nothing in this event. We're using it in other events do state mutations that are independent of the particular outro you received. For example, the first time you meet a character, Ink will update a list named npcs_met
in the exit
knot, preventing the character from introducing themselves again elsewhere.
If you're familiar with Ink, you might notice that we're using a limited subset of the syntax. For example, there are no diverts from one knot/stitch to another and there are no choices at all. This is a mostly a result of 1. directing basically all of the game flow through game code and 2. a card game driving the dialogue instead of literal choices. This simplifies a lot of the more dynamic game elements at the expense of forgoing any available tools such as Inky to test the narrative in isolation. The Ink scripts depend on the game existing and can't function correctly otherwise. That's not to say that we didn't try...
Though it's hard to tell from breaking down just one event instead of several, using Ink also allows Kevin to quickly check the narrative state and display certain dialogue in response to it. If a character has been met before, if the PC has experienced a related event or not, the player's past relationship with the character, and so on. The entire narrative state is handled in Ink logic.
The narrative design from Kevin is responsive; it unfolds across a series of cycles, reminiscent of The Last Express or Majora's Mask. Adopting Ink has enabled us to write a more dynamic, living narrative compared to using static data files.
Using Ink for Conversations
by Zach and Kevin on September 19, 2019
Signs of the Sojourner is a narrative deck-builder where your deck is your character, and each conversation is a card game (currently crowdfunding on Indiegogo!). To produce the majority of the text in Signs of the Sojourner, we're using Inkle's Ink scripting language. This has been beneficial in that it allows our writer, Kevin, to have enough control over the narrative state to work fairly independently.
Each conversation in the game is identified by one knot (this is Ink parlance that means a checkpoint in your script from which you can begin execution; for programmers, it's kind of like a goto label). When you start an event, the game will set the Ink runtime's "program counter" to this location.
Upon returning home for the first time, you'll encounter Elias. This conversation, which I'll use as an example, is identified by the knot:
=== event_elias_home
This knot is then further divided up into stitches (sub-knots), for which we have a strictly defined format. Because the game code directs the flow and routes to these as necessary, they need to exist in every event.
The first stitch we execute is the unimaginatively named init
stitch. For this event, it looks like:
= init
~ items += bartow_keyring
- -> DONE
You unconditionally get the keyring from Elias in this event, so the data updates right when the event starts. The UI that announces you received the item is shown later.
We then produce the title and intro text for the intro popup:
= title
Returning Home
- -> DONE
= intro
{
- surprise :
~ elias_negative++
{ch}Elias{_ch} comes up while you're unloading the truck. You didn't see him on the drive in, but now it seems like you avoided him. He's holding a box labeled {pl}BARTOW{_pl}.
- else :
~ elias_positive++
You find {ch}Elias{_ch} at home, sitting on the floor, surrounded by craft materials. Glue, markers, card stock, thin polystyrene sheets… He looks like he hasn't slept.
}
- -> DONE
Defining the title and intro blurb in Ink allows us to easily make them dynamic. In this example, the specific intro you see depends on if you choose to speak with Elias (surpsise == false
) or if you try to circumvent the conversation by packing up early (surprise == true
). Moreover, that choice affects your relationship with Elias (the elias_negative++
and elias_positive++
bits). The {ch}/{_ch}
and {pl}/{_pl}
tags are a convention we're using for text markup. Here ch
stands for "character", and pl
stand for "place". Content between these tags ultimately gets stylized in some way, whether it's a different color, bold, squiggly, etc. As of writing this post, both ch
and pl
cause the text to get rendered bold. We're still coming up with new tags to give the dialogue character.
The bulk of an event's text is in the strike and sequence text. You'll see sequence text both when a sequence begins and ends. Each sequence has its own stitch:
= sequence_0
{
- surprise :
Oh, there you are… I made these for the store while you were gone. {it}Bartow keyrings{_it}. Y'know, souvenirs. #exp neutral
- else :
While you were gone, I made something you can stock in the store: {it}Bartow keyrings{_it}! People <i>love</i> souvenirs! #exp happy
}
Just a memento to help travelers remember {pl}Bartow{_pl}. You don't owe me this time, ha ha… Unless they're really popular! #exp happy
- -> DONE
Each sequence stitch produces exactly two lines of text (one for the beginning and one for the end). We're using the surprise
flag again here to modify the tone of the dialogue. The #exp
tags trigger character sprite expression updates. The {it}
tag refers to "item", which also currently outputs bold text.
Each event has some number of sequence stitches defined, which determines how many sequences are required to complete the event. This event in particular has four in total.
Strikes, congruent to sequences, also have one stitch each. Strikes occur when you play a mismatched card with no shield or other effect to save it. Here's a strike stitch:
= strike_0(pc)
~ elias_negative++
{
- pc :
You must be tired. #exp confused
- else :
Sorry. I'm exhausted. #exp confused
}
- -> DONE
Currently each strike stitch produces one line of text. Here we're again updating your relationship status with Elias. Predictably, it deteriorates as you strike, but there are more interesting consequences later on. pc
refers to whether it's you, the player character, or not, who caused the strike.
When the event is over, you'll see an outro. There are two primary possibilities:
= concordance(symbol)
{
- symbol == circle :
You patiently field {ch}Elias'{_ch} questions about the road, even when he asks the same ones twice.
}
- -> DONE
= discordance(symbol)
{
- symbol == circle :
You answer question after question, but {ch}Elias{_ch} continues on, like he's searching for words he doesn't know.
}
- -> DONE
You'll see the discordance
outro if you "strike out." The outro text itself will also change based on the dominant symbol throughout the conversation. These symbols are passed from Unity, and narratively they're abstractions for certain conversation styles—empathetic, diplomatic, direct, creative, and so on.
Finally, the exit
knot:
= exit
- -> DONE
Which does nothing in this event. We're using it in other events do state mutations that are independent of the particular outro you received. For example, the first time you meet a character, Ink will update a list named npcs_met
in the exit
knot, preventing the character from introducing themselves again elsewhere.
If you're familiar with Ink, you might notice that we're using a limited subset of the syntax. For example, there are no diverts from one knot/stitch to another and there are no choices at all. This is a mostly a result of 1. directing basically all of the game flow through game code and 2. a card game driving the dialogue instead of literal choices. This simplifies a lot of the more dynamic game elements at the expense of forgoing any available tools such as Inky to test the narrative in isolation. The Ink scripts depend on the game existing and can't function correctly otherwise. That's not to say that we didn't try...
Though it's hard to tell from breaking down just one event instead of several, using Ink also allows Kevin to quickly check the narrative state and display certain dialogue in response to it. If a character has been met before, if the PC has experienced a related event or not, the player's past relationship with the character, and so on. The entire narrative state is handled in Ink logic.
The narrative design from Kevin is responsive; it unfolds across a series of cycles, reminiscent of The Last Express or Majora's Mask. Adopting Ink has enabled us to write a more dynamic, living narrative compared to using static data files.