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:

DialogTreeExample.png

Graph Module[edit]

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. To set up the module, enter the following code in a LocalScript:

local GraphModule = game:GetService('InsertService'):LoadAsset(303855726):GetChildren()[1]
local Graph = require(GraphModule)
local DialogTree = Graph.new(Graph.GraphType.OneWay)

The function LoadAsset will insert the ModuleScript into our game. We use GetChildren()[1] because InsertService automatically puts the ModuleScript into a Model that we don’t need. We can then require the module and use the new function that is defined in the module. This function 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.
myGraph:AddVertex(myTable)
  • 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:
myGraph:Connect(nodeA, nodeB)
Connect takes a third argument which represents the cost of the connection. Specifying a cost is very useful when using a graph for pathfinding or other AI algorithms. In this case a cost would not be of much use to us, so we do not have to specify one.
  • 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:
myGraph:Disconnect(nodeA, nodeB)
  • 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[edit]

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.

local GraphModule = game:GetService('InsertService'):LoadAsset(303855726):GetChildren()[1]
local Graph = require(GraphModule)
local DialogTree = Graph.new(Graph.GraphType.OneWay)
 
local function createDialogNode(choice, response, priority, onSelected)	local newNode = {} 	if not choice then		choice = ""	end	newNode.Choice = choice 	if not response then		response = ""	end		newNode.Response = response 	if not priority then		priority = math.huge	end	newNode.Priority = priority 	if not onSelected then		onSelected = function() end	end	newNode.OnSelected = onSelected 	DialogTree:AddVertex(newNode)	 	return newNodeend

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:

DialogTreeStructure.png

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.

local GraphModule = game:GetService('InsertService'):LoadAsset(303855726):GetChildren()[1]
local Graph = require(GraphModule)
local DialogTree = Graph.new(Graph.GraphType.OneWay)
 
local function exitDialog() end 
local function createDialogNode(choice, response, priority, onSelected)
	local newNode = {}
 
	…
 
…
 
	DialogTree:AddVertex(newNode)	
 
	return newNode
end
 
local start = createDialogNode("", "Hello there! How are you?") local goodFeel = createDialogNode("I'm well, thank you.", "That's great! Glad to hear it!", 1)local mehFeel = createDialogNode("Meh, been better.", "I'm sorry to hear that.", 2)local badFeel = createDialogNode("I'm grumpy.", "I'm sorry to hear that.", 3) DialogTree:Connect(start, goodFeel)DialogTree:Connect(start, mehFeel)DialogTree:Connect(start, badFeel) local who = createDialogNode("So who are you anyway?", "I'm the dialog guy of course!") DialogTree:Connect(goodFeel, who)DialogTree:Connect(mehFeel, who)DialogTree:Connect(badFeel, who) local investigate = createDialogNode("Investigate", "", 1)local goodbye = createDialogNode("Goodbye!", "", 2, exitDialog) DialogTree:Connect(who, investigate)DialogTree:Connect(who, goodbye) local colorcolor = createDialogNode("What's your favorite color?", tostring(BrickColor.Random()), 1, function()	DialogTree:Disconnect(investigate, color)	local neighbors = DialogTree:Neighbors(investigate)	for _, neighbor in pairs(neighbors) do		DialogTree:Connect(color, neighbor)	endend) local longlong = createDialogNode("How long have you been here?", "A while.", 2, function()	DialogTree:Disconnect(investigate, long)	local neighbors = DialogTree:Neighbors(investigate)	for _, neighbor in pairs(neighbors) do		DialogTree:Connect(long, neighbor)	endend) DialogTree:Connect(investigate, color)DialogTree:Connect(investigate, long) local nevermindnevermind = createDialogNode("Nevermind.", "Is there anything else I can do for you?", 3, function()	local investigateNeighbors = DialogTree:Neighbors(investigate)	if #investigateNeighbors <= 1 then		DialogTree:Disconnect(nevermind, investigate)	endend) DialogTree:Connect(investigate, nevermind)DialogTree:Connect(nevermind, investigate)DialogTree:Connect(nevermind, goodbye)

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.

GUI Elements[edit]

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.

DialogTreeGui.png

DialogTreeGuiStarterGui.png

Now we can insert code to tie our tree to our graphical elements.

local GraphModule = game:GetService('InsertService'):LoadAsset(303855726):GetChildren()[1]
local Graph = require(GraphModule)
local DialogGui = script.Parent
local dialogButtonConnections = {}local DialogTree = Graph.new(Graph.GraphType.OneWay) 
local function resetGUI()
	DialogGui.Choice1.Visible = false	DialogGui.Choice2.Visible = false	DialogGui.Choice3.Visible = false	for _, connection in pairs(dialogButtonConnections) do		connection:disconnect()		connection = nil	endend local function exitDialog()
	resetGUI()
	DialogGui.NPCDialog.Visible = falseend 
local function createDialogNode(choice, response, priority, onSelected)
	local newNode = {}
 
…
 
…
 
 
DialogTree:Connect(investigate, back)
DialogTree:Connect(back, investigate)
DialogTree:Connect(back, goodbye)
 
local function selectNode(node)
	resetGUI()	if node.Response ~= "" then		DialogGui.NPCDialog.Text = node.Response	end	local neighbors = DialogTree:Neighbors(node)	if neighbors then		table.sort(neighbors, function(a,b)			return a.Priority <= b.Priority		end)		for index = 1, #neighbors do			local nextNode = neighbors[index]			local choiceButton = DialogGui:FindFirstChild("Choice"..index)			choiceButton.Visible = true			choiceButton.Text = nextNode.Choice			dialogButtonConnections[index] = choiceButton.MouseButton1Click:connect(function()				nextNode.OnSelected()							selectNode(nextNode)			end)		end	endend selectNode(start)

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.