All RPGs need good dialogue to support a good story. Dialogue, of course, has a few shapes. The player must be able to interact with NPCs, the NPCs should be able to interact with each other, and there should be a healthy amount of background chatter. This article describes how The Botanist’s player/NPC interaction works.

The Botanist uses three types of dialogue:

  • Interruptive Character/NPC Dialogue (we call this “conversation”)
  • Non-interruptive NPC chatter (called “incidental speech”)
  • NPC-to-NPC chatter

This article is concerned with the first, but stay tuned for articles on incidental chatter and NPC-to-NPC dialogue.

Just use the DOM!

Initially, I started building The Botanist’s UI pieces in native canvas, which was really far too tedious. Eventually I realized it was silly to force myself to use canvas for everything, so I ended up building static and slow-paced UI elements in HTML and CSS (things like dialogue, inventory, skill trees, etc). Because, y’know, that’s what HTML and CSS were built for.

It’s trivial to do rounded corners and drop shadows and positioning in CSS, and the only thing we have to worry about is making sure speech bubbles and UI elements show up in the right place and can communicate with the game instance. I’m OK with closely coupling UI to the game, because I’m not writing a game library, I’m just writing a game. Don’t judge! Always consider your context when making these decisions. Sometimes closely coupling is bad, sometimes it saves you a hell of a lot of effort. This is one case where I’m ok with making a “poor” architectural decision in exchange for building out this game that much more quickly.

When I write about incidental speech in a week or two I’ll show you how to couple sprite position updates with DOM position updates. It works quite smoothly.

Data Format Requirements

The biggest challenges in designing this dialogue system were figuring out the requirements and making sure I was using a data structure that would be easy to use and flexible enough to support future growth. We’re naturally using JSON, and here’s an example of what a “speech pattern” looks like:

{
	"name":"Unfriendly Jan Pre-Quest",
	"key":"unfriendly-jan-pre-quest",
	"start":"1424791485315",
	"elements":{
		"1424791485315":{
			"npc":"What do you want?",
			"character":{
				"1424791491948":{
					"text":"Have you noticed anything strange going on?",
					"followup":"1424791562420"
				},
				"1424791514604":{
					"text":"I'm in need of a few things, can you point me to the shops?",
					"followup":"1424791601252"
				}
			}
		},
		"1424791562420":{
			"npc":"What am I, the town crier? Get lost."
		},
		"1424791601252":{
			"npc":"You've got eyes. Find them yourself.",
			"character": {
				"120945329342": {
					"text":"Let's start over.",
					"followup":"1424791485315"
				}
			}
		}
	}
}

This is understandably confusing at first glance. The dialogue has a number of “elements”, which are individual call/response patterns. An element starts with the NPC saying something, and then the player has a number of options to choose from in their response.

Elements are not nested. Instead, they have unique keys, and the followup property of a character response refers to an element’s key. When that response is chosen, the dialogue advances to the new element. The reason elements are not nested is so they can be referred to by multiple responses, if necessary.

The start parameter above lets the speech system know where the conversation should begin.

Let’s make it easier!

It would be ridiculous to have to write these conversations in JSON by hand. So obviously, we should whip up a quick PHP/JS tool to do it. Here’s what that looks like:

Speech Editing Tool

I’m not going to go into the building of the tool here. It’s poorly-written procedural PHP and JS that uses a JSON datafile. I didn’t feel the need to build a whole Laravel app around this. After all, this is precisely what PHP’s initial purpose was: whipping together quick scripts for the web. And it works swimmingly and makes the job of creating dialogue so much easier.

The unique keys aren’t guaranteed unique, but we’re using new Date().getTime() (the current timestamp in milliseconds) for each key, which is really unlikely to collide. Quick-n-dirty.

I can’t stress how important it is to have good tools if you want to build a big, ambitious game. The two hours it takes to whip a tool together will save you dozens or more in the long run, and help you better visualize what you’re building.

Implementation

Once the data structure and conversation builder tool is figured out, the rest is easy. It’s trivial to load JSON files into Phaser’s cache and grab the data later.

In a preload:

this.game.load.json('speech', 'assets/speech.json');

And then later on, when you need the data:

var speech = this.game.cache.getJSON('speech');

The Phaser implementation is easy too, all you need are a few functions in your character or player class.

Character.prototype.startConversation = function(convoKey) {
    var speech = this.game.cache.getJSON('speech');
    var convo = speech['conversations'][convoKey];
    this.game.paused = true;
    this.activeConversation = convo;
    this.updateConversationState(this.activeConversation.start);
};

Character.prototype.stopConversation = function() {
    this.activeConversation = null;
    this.activeConversationState = null;
    this.game.paused = false;
};

Character.prototype.updateConversationState = function(stateId) {
    this.activeConversationState = stateId;
    // $showConversationState is a jQuery function that manages the DOM
    $showConversationState(this.activeConversation, stateId);
};

Our speech, which is interruptive, pauses the game so that nobody kills you while you’re in the middle of a conversation :). Starting the conversation loads the active conversation and pauses the game. Stopping a conversation is easy too, just un-do what you did to start it (plus make sure you get rid of the dialogue box on the UI-side). And updating the conversation state just needs a stateId to go off of and sends that to the UI.

A future feature of this dialogue system is attaching triggerable events to a conversation element – eg, starting a quest, being gifted an item, giving money, etc. But since we’ve designed our data structure flexibly, we can easily add an action property to a conversation element, and then updateConversationState method can take care of the logic to trigger the action. I’ll probably write a separate article on that when I get to that piece of functionality.

I’ll let you try and imagine what the $showConversationState function looks like without actually showing it to you – it’s a jQuery function that is responsible for rendering a dialogue box with the NPC’s speech and the list of possible character responses. And don’t forget that you need event handlers on the character responses to trigger updateConversationState, and of course an exit button that hides the UI and calls stopConversation.

Demo Time

Enough blabbering! Here’s this week’s snapshot. Walk up to the NPC and click him to launch a dialogue. You have to be pretty close to the NPC, so if nothing happens when you click him, get closer. I’ll tune the distance later!