Designing a Dialog Tree
Designing a Dialog Tree
Dialog trees are a standard feature in many game genres, particularly in RPGs. The Roblox Dialog system works for small and straightforward trees, but quite often dialog trees need to loop or have conditional branches. In such cases, a custom system built off of a graph data structure works well.
In this tutorial, we will cover how to make a dialog system implementing the following dialog tree:
Instead of storing the contents of our dialog tree in the game’s hierarchy, we will need a data structure in code to keep track of all of the text and the connections between nodes. In this case, we will use a structure called a Graph. A Graph is simply a set of nodes that can be connected together. Graphs are often used for dialog trees and pathfinding.
Instead of making a Graph from scratch, we can use this public Module. Insert it into your game, then enter the following code in a
LocalScript within the module:
local Graph = require(script.Parent) local DialogTree = Graph.new(Graph.GraphType.OneWay)
We can require a
ModuleScript to get the value it returns - the Graph module. In this module, we use the new function: it takes a GraphType as an argument, which defines what kind of connections our graph expects and can be either OneWay or TwoWay. Since we don’t want our dialog to always be able to travel backwards in the graph, we select OneWay as the type.
Before we start implementing our dialog tree, let’s first learn how to use the Graph we just setup. A Graph comes with many functions, but we will only use 4 of them: AddVertex, Connect, Disconnect, and Neighbors.
- AddVertex adds a new node to the graph. This function only takes one argument: the value we want to put in the node. This value can be anything we want. In this case, we will store a custom table.
- Connect allows you to specify two nodes to connect together. Note that since we configured our Graph to be OneWay, this connection has direction. To make a connection between two nodes, we pass in the node where the connection starts and the node where the connection ends. For example, to make a connection between nodeA and nodeB, we just have to call:
- Disconnect allows you to remove a connection between two nodes. Like Connect, Disconnect does have a direction; If you want to remove a connection that starts at nodeA and ends at nodeB you have to pass those nodes in the correct order:
- Neighbors takes a node as an argument and returns all of the nodes that are connected to it. Note that this list is not sorted in any way so you will have to use the table.sort function if you need to arrange the neighbors in a particular order.
local theNeighbors = myGraph:Neighbors(nodeA)
Creating Dialog Nodes
Now that we can create graphs, let’s build a structure to hold the actual dialog in the tree. There are several ways we can approach this problem. In this case, every node will store a dialog choice, the NPC’s response to that choice, the place this choice should take in the list of choices (if there are more than one), and lastly a function to call when the choice is selected in case we want to do anything special.
We make a helper function called createDialogNode to help us create new nodes. This function takes four parameters, but the function has defaults for each if they are not provided. The function also automatically adds our new node to the Graph.
With our new data structure let’s take a look at how we will organize our dialog tree:
Now we can use our function createDialogNode to create all of the above nodes. We can also connect them with the Graph’s Connect function.
The nodes off of Investigate are worth looking into a little bit further as they aren’t as straightforward as the others. We only want the player to be able to ask “What is your favorite color?” and “How long have you been here?” once. When the player selects either of these nodes, we disconnect the node from Investigate. We then connect the node to the remaining neighbors of Investigate.
The nevermind node likewise has some special code when it is selected. If the investigate node only has one neighbor, that means the player has already selected both color and long. In this case, it doesn’t make any sense to allow the player to navigate back to investigate, so we remove the connection between nevermind and investigate.
Lastly, we have a placeholder function for exitDialog which we will complete in the next step.
We have the structure for our dialog tree, now we need to display it to the player. In StarterPlayer we can use a TextLabel for the NPC responses and three TextButtons for the player choices. We can also put our LocalScript with all of our dialog code into the ScreenGui that contains all of our other elements.
Now we can insert code to tie our tree to our graphical elements.
We’ve defined a couple of more functions. The first, resetGUI, hides all of the buttons. It also cycles through the dialogButtonConnections and disconnects the connections stored within. Note that these connections are the connections created when binding the button click events, not the connections between Graph nodes.
The selectNode function transitions the GUI to a new node and populates the elements accordingly. First, it calls ResetGUI to clear the buttons. Then, if the NPC has dialog in the new node it fills in the TextLabel, otherwise the label won’t update and will show the previous message.
The function then gets the neighbors of the current node. It sorts these neighbors by priority so we can control the order the neighbors appear. We then cycle through the sorted neighbors and updates the corresponding TextButton. In the button’s MouseButton1Click event, we first call the OnSelected function of the node and then transitions to the next node.
Now we have a dialog tree with various choices the player can navigate through! If we want to add more dialog, we now only have to create more nodes with createDialogNode and then connect them to the other nodes with Connect.