In this post I go behind the scene on the conversation engine I built for Mars Crisis. Mars Crisis is a one man project developed with Harlowe and Twine. Play the latest version of the game here.
Mars Crisis is a game about political dilemmas. Getting into a complex situation and then deciding what to do is basically what you do in the game. For Kelly to find out what the situation is and what the possible solutions are, she will talk with stakeholders. Political opponents as well as allies and friends and relatives. Hence, conversations is a key component of Mars Crisis and I wanted to build a conversation engine to provide a satisfying user experience.
What is a good conversation?
A good conversation is an exchange of information between two people. Both can ask questions and provide answers and the conversation continues as long as there is mutual interest and something to talk about. Conversations generally start on superficial topics (How is business? How is the family?) and then dives into deeper topics as mutual trust and shared interest emerges (I love you! Marry me!).
So let’s break down conversations between Kelly and other characters in the colony into conversation snippets. Each snippet is a conversation about a given topic. A snippet can reveal new topics to explore so the list of topics needs to be dynamic.
For a satisfying player experience, the snippets need to be digestible and come at a good pace, so let’s have the player pull the conversation snippets from a short list of topics, as Kelly asks the person about a topic.
At any time, the player can end the conversation and go back into explore mode (moving about between locations in the colony) or situation mode (having followed a story hook and been pulled into a specific situation like Strike at Mars Drilling).
A collection of conversation snippets each covering a topic sounds like a data map (or dictionary). However, I wanted to be able to write the prose of the conversations in plain Twine text, not embedded in quotes and with every quote escaped (I will spare you from an example of how that would look).
So I went for storylets. In Harlowe, you can mark up a passage as a storylet with a lambda expression controlling the condition for when it is available (open-storylets:). Furthermore, by also tagging up the conversation snippets with a ‘conversation’ tag, you get a nice way to add snippets without having to maintain a repository of conversations explicitly.
Storylets can also have an urgency specified. We can use this to rank topics such that superficial topics appear at the top and then when exhausted, new topics appear (or not, depending on how the conversation goes).
Next, conversation snippets need also to be mapped to persons and topics.
While Harlowe allows adding meta data to passages, it feels a little clumsy. As I would anyhow end up naming passages something like “Mayor Lang on Immigration”, I decided to use structured names for passages that are conversation snippets and then derive the topic and the person from the name of the passage. I added tests to verify this as errors here can otherwise be very time consuming to find. A typo in a tag name or passage name could cause content to disappear from the game completely.
That leaves writing the basic conversation loop to present a list of topics and to reveal the snippet when select. Here is how it looks:
And here is how the code is called:
Here is how it looks in the game:
Here is how a conversation snippet looks:
I decided to remove the menu bar for conversations to avoid issues where the conversations start over after the player has consulted Kelly’s notes, messages or options. Keep it simple to avoid complex implementation tasks.
With conversation snippets written in passages, it is very easy to mark them up to be further interactive.
The empty conversation looks like this.
(display:) versus (macro:)
I’ve used (display:) to create the Talk to Someone code for the conversation engine.
The (display: “Passage name”) command includes the source of another passage in a passage. Hook names and variables exist in the context of the passage the template page is included in, completely as if the passage source was copy-pasted into the destination page. This is super nice to provide structure and simplify the game. However, there is the risk of name clashes with hooks and temp variables. The Talk with Someone passage requires the calling page to set specific local variable prior to calling, a code pattern close to relying on global variables that you would usually avoid.
However, custom macros only run once. So to implement Talk with Someone as a custom macro, you would need to output generated code which would be possible but pretty close to write-only-code. If the input parameters to a passage included with (display:) starts to get complex, it's possible to create a data type representing the input and provide validation and default values.
Next is to add further content. With a solid foundation it’s easy to do without breaking what is already there. Conversation snippets are first class citizens of Mars Crisis so there is plenty of room for creativity.