specialization
Context-sensitive dialogue system
As part of our second year at The Game Assembly (TGA) students plan and deliver a portfolio project. For programming students like myself, this takes the form of a specialization project. This is an opportunity to dive deeper into a preferred subject within game programming, and involves research, project planning, and documenting the process.
For my specialization, I created a dialogue system that weaves gameplay information into speech bubbles. I wanted to investigate whether otherwise passive UI elements, such as status effect icons and quest markers, could be baked into a more active system.
1. Define the scope
What belongs in a dialogue system? What do I need in order to display a conversation? How much is needed for a proof of concept? I narrowed it down to these three things:
- dialogue trees and a tool for building them
- speech bubbles and a dialogue options interface
- 2 to 3 player states that affect the conversation
The primary goal of this project was to bridge the gap between text-based narrative systems and the readily available visual gameplay cues, such as animation. If your character is wounded, drunk or freezing, should that not affect what they say? I took inspiration from comic book typography, where emotional information is often conveyed via the shape, color and outline of a speech bubble. As for dialogue trees – they are a tried-and-true way for visualization and structuring. Why reinvent the wheel?
I further planned for additional features, in case I had time to up-scope: shrinking speech bubbles and blurring the text based on the speaker’s distance to the player.
2. Scavenge and modify
Are there tools are already available? Which systems can be reworked to serve a new purpose?
- the Visual Script Editor
- health bars from Spite: Bleeding Skies
- 2D UI system from our custom engine
My secondary goal was to better learn how to make use of already existing game systems to create a new feature. If all went well, my team and I could then use that feature for our coming game projects. To that end, I decided to build my project in bean, the custom engine my team had built for our second year at TGA. From there I could pilfer two vital building blocks: the UI infrastructure, and a health bar system.
I had created both for Spite: Bleeding Skies, the first game we built in the engine. I stripped down the health bars to a bare minimum, gave them a white box sprite for a background, and added a text widget. Since Spite:BS used text widgets only for tooltips and debugging, the text rendering system was never made to be particularly robust or visually appearling. But for this proof of concept, it ought to prove adequate. I also made use of ImGui for quick prototyping, and to avoid some of the more annoying quirks of our text widgets.
For our current game project, Finders Keepers, I had been working on fleshing out and improving a basic Visual Script Editor originally provided for our course in visual scripting. It initially seemed suitable for building dialogue trees. However, adapting the work flow to its already idiosyncratic structure proved a challenge…
3. Stumbling blocks
What happens when small tasks become big problems? How late is too late for scrapping and redesigning? And can the robust substrate of an earlier system prove a weakness when applied to a new task?
- workarounds for dialogue trees
- separating blueprint from execution
- storing data for evaluation of conditions
The original plan had been for the Visual Script Editor to provide a 1-to-1 representation of dialogue trees. I had also thought it could be used for the execution of the dialogue tree itself. However, halfway through the project it became apparent that the editor would need major system changes for this to be possible. Updating the dialogue system would otherwise require the entire graph to be executed each frame, simply because of how it handles execution flow.
Instead, I had to pivot (or bring out the duct tape, but pivot sounds nicer). The Editor would be used only to build the dialogue trees. Each graph would represent one distinct conversation. Once executed, an external structure (a Tree Builder) parses the graph nodes (s-nodes, for “script”), and populates a Dialogue Tree with its own type of nodes (t-nodes, for “tree”). The Editor’s graph thus becomes a blueprint for the tree, while the tree itself is an in-engine class, built in runtime. It was not optimal, but it worked.
The first iteration of this system had a fatal flaw: Because two t-nodes had to exist in the Dialogue Tree before a connection could be made between them, the visual script graph required it as well. This resulted in the sequence of s-node A, then s-node B, then an s-node that establishes a connection from A to B (right side of image 1, below). If that sounds confusing, it’s because it is. And if it looked confusing to me, what hope would the end user have?
A far better design was to have each s-node, upon successful execution, find the s-node which the flow had passed through just prior. In so doing, a connection from the previous to the current node could be automatically created once two viable, adjacent t-nodes had been added to the tree. This functionality could easily be included in the base class for node building (image 2, below). Once an s-node successfully executes, it stores the ID of the t-node that was just created.
This solution meant, however, that conditions became limited. Because the graph cannot be compiled (unlike e.g. Unreal Blueprints), function pointers, lambdas and the like could not be used for condition nodes. The alternative I landed on was to create a blackboard structure for storing character stats and other data. That way, once the dialogue tree hit a condition node during a dialogue, the relevant data could be gathered from the blackboard with a key, and compared to e.g. a threshold value set in the script graph (and thus transferred to the dialogue tree).
Each condition should apply to a specific A-to-B connection, rather than all connections into node B. This required the ability to store condition info in the connections (edges), rather than the wider t-node structure. Back to the original problem: setting that info required an edge, and the edge by definition needed to know its destination node. I thus had to allow the Tree Builder to store a temporary (“pending”) edge, to be assigned to the dialogue tree itself once a destination node had been constructed. This was done in the same intermediary s-node that allows the user to customize the connection with a condition and/or the text which represents that dialogue option in the game UI (see image 3, below).
4. Conclusions and future improvements
What have I learned from this process? Did I fall prey to sunk cost fallacy? And what would I have added, given more time?
- unnecessary generalization and complexity
- a more focused Visual Dialogue Editor
- intergration with audio
In hindsight, early iterations of the dialogue tree/script editor interface were a muddled mess. Not only was it visually confusing, but there was little gain overall. The script graphs, once saved, had to be executed in game runtime to create a dialogue tree in a builder class, which was then copied into a dialogue manager; a three-layered and poorly optimized approach.
By contrast, the end product is used to create a representative graph, build the tree at the push of a button, and export it as a json file. This can then be used like any asset in bean. As a bonus, the json provides a clear, human-readable overview of a dialogue tree (which makes for easier debugging).
Considering the scope of this project, the blackboard system and the complex condition creation and evaluation were likely unnecessary for a mere proof of concept. Worse still was that the blackboard had become dependent on game-specific data, eventually causing dependency issues. I was forced to redesign large swathes of code before I could relocate the entire dialogue system into our Engine project – as opposed to having it under our Application/Game domain.
Player status effects could just as well have been part of already viable Player-adjacent components. This method would have been more in line with my stated goal of reworking existing systems to serve a new function. However, the now generic blackboard is both useful on its own, and makes the dialogue system more modular for future game projects.
The most apparent deviation from my original plan is the shift in focus. My original intension to keep the same emphasis on in-game visual aspects (speech bubbles and dialogue interface) as I did on the editor, instead ended up being close to a 20-80 distribution in favor of the latter. (Perhaps this is for the best – my limited artistic skills would not have done the concept justice.) Future developments would certainly require a decent-looking text rendering system, since bean doesn’t have one fit for purpose. This would have allow for testing a variety of fonts and typographic techniques. I did, however, manage to add the shrinking effect to distant speech bubbles (see below).
More visual effects would also be a good addition. Two ideas I had to scrap were A) that a wounded player’s speech bubble should drip blood, and B) making dialogue options blurry and floating around when the player is drunk. The challenge then would be how to handle the visual effects of different states – do they blend, or is there a hierarchy where one might be prioritized over others?
Last, but perhaps most importantly, I intend to branch off the Visual Script Editor into a dedicated Dialogue Tree Editor. Splitting the Dialogue Editor into a different, dedicated tool will decrease the risk of mixing domains and bloating an already extensive application. Hopefully, it can turn out to be a useful tool during my last game project at TGA.