Character and NPC Dialogue in PhaserJS
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:
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!