As the game grew in complexity over the last 6 months, it reached a saturation point where it became impossible for me to be aware of all the ramifications that the player actions can create when interacting with the NPCs (Non-Player-Characters). I knew it would be impossible to test every single possibility, but I need to be sure that whatever they decide to do, is a defined reaction and makes sense within the current context. This post is going to cover our dialogue system and how that allowed me to regain control over the NPC behavior.
After the writers joined the team, we had a meeting where I showed them the pipeline for dialogue work.
All conversations are written in a flowchart program (yEd), separate from the game engine. Seeing conversations this way, allows me to quickly navigate the hierarchy, see the branching possibilities and have an overall picture of the complexity. As an example, here is a screenshot of a small section that represents dialogues the player can initiate with the wife:
And if we zoom into one of the blocks (what I call a Script), it looks like this:
The first line is the title of the dialogue, then right below is the text line that you choose in order to initiate the conversation, and finally the actual exchange between the actors. Each color represents a different actor (In this case Yellow = Player, Purple = Wife).
Once I’m happy with a conversation, I open a Script file (that is compiled by the engine) and re-write it in the format that the game understands. So the same flowchart we just saw, would read something like this:
This means that each time I want to make a change, I need to re-edit the flowchart, update the script with the changes, compile the game and finally check the result in-game. When I was working alone, this was painful but bearable, but once I started to explain this to the writers, and they kept having more and more questions, I realized how unnecessarily complicated, and error-prone the whole thing was.
I want them to focus on the conversation flow, the story beats and emotional development of each character, not to be jumping from a flowchart to a script file, without even being able to test any of the changes unless they have the editor open and compile the scripts!
I knew this wasn’t an ideal process, but I’ve been told repeatedly, by experienced programmers, that one of the worst mistakes you can do, is to try and optimize a system without knowing what you want it to do, since you will get lost in the process, and write code that is too specific that you will end up throwing away.But since I waited so long, it was kind of obvious what needed to be done.
I had to remove the script part completely and have the engine read the flowcharts directly, also allowing for the game executable to re-load them on command, without any need to have the editor open. This way, the writers can just make changes in the flowchart, press a button in the game and play to see the changes.
I knew this was a big improvement but I didn’t realize how much of a change it would create.
I was able for the first time to visually be aware of the complexity of ALL the conversations in the game. How many options you have for any single topic, how they branch, where they end and connect, etc etc. I can also quickly have a feeling of how long an exchange between actors will take, if it should be more detailed, faster, etc. It also meant that trying ideas was just a matter of writing new blocks and connect them.
The initial problem
Going back to the original problem, the NPC behavior organization, I realized how similar it was to the dialogue system. To give you an idea, here is a breakdown of how it works.
The smallest element you can have are ACTIONS, these are simple interactions like:
- Open [Door],
- Pick Up [Object]
- Sit [Chair]
These are dynamic and can queue other Actions based on the situation. For example, if I want to drop a book on a table, I would queue a GoTo[Table] and if there is a door on the way to the table, also queue a Open[Door]. When I arrive at the table, if there is no more space, do a Wait[seconds] or some other reaction. Actions are also deleted if no longer needed (e.g. another actor opens the door for you).
These ACTIONS are all organized inside what I call TASKS. For example, PrepareDinner would have several nested Actions from picking plates from the table, filling glasses with water and even call additional Tasks if the process is messed with (e.g. take a glass from the table before the NPC picks it up to fill it with water).
All these tasks were written in script files just like the dialogues, and as the game grew in complexity more unique cases had to be taken into account and I would end up forgetting these exceptions between Tasks, spending hours looking through the code trying to understand why an NPC did a certain thing, a certain way at a certain moment. Here is a made up example:
Imagine a non-player character (NPC), whose Tasks are to PrepareDinner and then ReadBook.
While he is doing the PrepareDinner, he decides to interrupt the first task and GoToToilet but the player decides to grab the book from the table and lock himself in the bathroom, so the NPC can’t use the toilet and can’t ReadBook either. So he decides to WaitForToilet but also reacts to the fact that he can’t read the book (exception inside WaitForToilet).
While waiting, someone rings at the front door and so the NPC decides to AnswerDoor.
At the same moment, the player opens the bathroom door and blocks the front door, without opening it or closing it. So now the NPC is not really doing any Task but just waiting for you to give him space to open the door, at the same time surprised by your illogical behavior and he still wants to GoToToilet…
The current system was not scalable, impossible to visualize and worst of all, it was hard for me to remember the edge cases that needed custom Tasks and how they would work.
The search for a solution
Luckily for me, I ran into this problem around February, just before GDC (Game Developer Conference), where I knew a lot of really good AI programmers would be all gathered in one place. So I used this opportunity to present them with my problem. They agreed it’s not an easy problem to solve and pointed me to a lot of solid out-of-the-box solutions for NPC behavior, so I decided to test some of them.
What I realized, is that they are focused on short-term AI situations, like squad based decisions or patrol routes. None of them took into account long-term behavior that is more abstract and layered. And every time I tried to use these systems for my case, I ended up with the same original problem, a big spaghetti code mix that was impossible to work with.
But playing with these solutions I realized how they are extremely organized and consistent in their ‘rules. Sometimes solving a unique case meant going the long way around BUT keeping the system consistent and thus easy to debug. In order to see how this could apply to my case, I decided to write every single Task into a flowchart, and connect all the branching options (below is a slice of the Wife Tasks).
As expected, this was unreadable just like when it was in code, I had to remember all these tasks in my brain, and understand the relationship between them in order to do a change that won’t break the whole thing. But now, for the first time, I had a visual perspective on the problem, just like with the dialogs, and the more I looked at it, the more I started to see patterns and obvious simplifications.
For example, Tasks can be often grouped in a bigger category, and thus become contained (I call them Stories). Also, a lot of Tasks are actually reactions to certain situations (e.g. AnswerDoor is a reaction to someone ringing the doorbell and not part of the original goal of PrepareDinner or ReadBook).
This means I can have chronologically ordered Stories that once complete are invalid, while others are temporary shortcuts, before resuming the original goals. And so, slowly I was able to re-organize all Tasks into something readable and above all scalable.
Here is how a section of the wife flowchart currently looks like:
Each ‘pink heading’ represents a goal, inside this goal you have the Stories (in green) that contain the Tasks (yellow) and in red are the Triggers that would initiate the Story.
Everything inside a Story is isolated and cannot be affected by any event around it unless it’s a Reaction (that is also subcategorized in different types). This allows me to go deep in complexity while keeping everything encapsulated and controlled. Here is a closer made-up example where the Wife would give a present to the player and waiting for him to react:
While she is doing the GivePresentToPlayer Task, the moment the PlayerOpenedPresent she will start the ReactToPresent. If you take too long, then she calls a new Story TiredOfWaitingForPresent with its own set of Tasks and Triggers. Of course, I’m oversimplifying with these examples, but I just want to get the idea across. And just like the dialog system, the game reads these flowcharts directly, no more need to re-write it all in the script.
I can’t stress how much of a difference being able to visually read all this information it makes and in this situation, it was immediately quantifiable. Over the last months, I was able to re-design all actor behavior to be a lot more precise and controllable, and being able to ‘visualize’ abstract concepts, makes me able to much more quickly understand the complexity and what might be missing.