XML DIALOGUE TREES IN UNITY C#
I wanted to take a break from my development of System Dot to share my method of programming dialogue trees in Unity. Currently, there is not one designated way to put dialogue trees into Unity (i.e. no dialogue tree data structure or object) so I needed to build a custom one. There are a variety of methods for coding a dialogue system online, but none of them curtailed to my specific needs. System Dot uses dialogue to not only push the player to the end of the level, but to subtly teach them important programming concepts during key moments. For instance, when the player first opens the terminal window, which is a console-like area to input code I customly defined, the dialogue directs the player through how to type code and what certain buttons mean. I did not necessarily need to build a “tree” per se since there are no branching paths in the dialogue, but instead an event-driven system that would react to player’s actions and output a response accordingly. In order to facilitate “dialogue trees”, I store the dialogue nodes into an XML file and then parse the file into a dictionary of keys and values. The idea to store dialogue into an XML file was taken from https://forum.unity.com/threads/need-to-create-conversation-trees.41056/. The way I format my XML file, output dialogue, and handle events is built by me. See the full extent of my Dialogue class here.
Being in favor of having general game systems and tools to help reduce code duplication and development time, I thought about what general attributes a dialogue system had. I needed a way to output the dialogue on screen, the speed at which the text appears on screen, a way to parse the XML file to read the dialogue, and a method to move dialogue from one node to another depending on the player’s actions. The Dialogue class is a superclass responsible for accommodating all those features. Then, any Unity scene or object that takes advantage of this structure will be a child of the Dialogue class. For instance, IntelliSense, the player’s guide in the game, is the object that initiates the dialogue. In addition to outputting the dialogue, IntelliSense zooms out of and into the player when it starts or finishes the dialogue, respectively, and hovers up and down as it speaks. Since there are also multiple instances of IntelliSense in various scenes, I then make specific level child classes (ex. “IntelliSenseLevel1”) that are in charge of the specific events of that level and dialogue. In those classes, there are methods that handle what the player clicks on and how to respond accordingly.
I kept that class structure in mind when designing the dialogue node in the XML file. Here is an example of a dialogue node:
We can see that I’ve wrapped each line of dialogue within a “say” tag. Similar to games like Pokemon, an NPC may not say everything in one simple instance. The separation of lines is a quick way to facilitate drama in different sentences and a separation of long speeches. In the future, though, I could modify my class to intelligently detect when to break off into another line without having individual “say” tags.
A dialogue node has several key attributes within the opening and closing “message” tags:
- id: the name of the key when storing the node into a dictionary. The id can also denote which path the user has taken when undergoing specific actions or replies. For instance, two dialogue nodes could be titled “NameInputted-Y” and “NameInputted-N” to denote whether the name a player puts in is valid or not and the corresponding dialogue for that decision.
- event: the name of the event that will be called when the dialogue node is being set.
- char: who is speaking so we can differentiate between different NPCs in a dialogue instance.
With the dialogue node defined, here is the snippet of code that parses the file:
Through the System.Linq and System.Xml libraries, I use the XDocument object for storing data from the XML file into a traversable tree (unlike XmlReader which discards data after one pass). In order to store the contents of each message block, I used a dictionary of sayList lists where each list will contain the lines of dialogue that an NPC needs to say and the name of the NPC who says it. For each message tag, I store the id attribute as the key value for the dictionary and then add the values of both the char attribute and of each “say” element into the sayList list. After exhausting that “message” element, I add the id name and the corresponding list of dialogue statements into the dialogue dictionary. Likewise, events are stored in the same manner in the events dictionary.
Once we have these two data structures set, we can use them to set the piece of dialogue that will be displayed to the player. That code is in the overridable SetDialogue function shown here:
By making this function virtual, I am able to override the method in child classes in cases where the events are not used in certain scenarios. In essence, all I need to do to initiate a piece of dialogue is to call SetDialogue with the corresponding dialogue instance id. And viola! Once I know which entry in the dictionary to read from, the Dialogue class’s Update method takes the whatToSay list and starts printing the text on screen. If that entry has an event associated with it, it automatically calls the initialEvent method at the start and the performEvent method at the end. The only strain on the system is the initial storage of the dialogue into the dictionary. Otherwise, setting which piece of dialogue to output is as simple as retrieving data from a dictionary, which is in O(1) time according to how dictionaries are implemented in C#. Of course, the Dialogue class is not completely finalized and the way I read in XML data may improve in the future (since it does take O(n2) time), but stay tuned on my GitHub for any updates.