PcoWSkbVqDnWTu_dm2ix
We use cookies on this site to enhance your user experience

Designing a Dialog Tree

Designing a Dialog Tree

Jul 03 2018, 11:31 AM PST 10 min

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:

DialogTreeDiagramExample

Graph Module

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

  • 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

  • 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)

Disconnect

  • 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

  • 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.

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 newNode

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 color
color = 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)
	end
end)

local long
long = 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)
	end
end)

DialogTree:Connect(investigate, color)
DialogTree:Connect(investigate, long)

local nevermind
nevermind = 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)
	end
end)

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

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
	end
end

local function exitDialog()
	resetGUI()
	DialogGui.NPCDialog.Visible = false
end

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
	end
end

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.

Tags:
  • dialog
  • rpg
  • npc
  • storytelling