§ How to · with Picoo

How to make a Roblox dialogue system (with AI)

By Sametcan Tasgiran, Founder & Developer·Published ·Updated

A Roblox dialogue system needs four pieces: a dialogue tree, a way to start talking, reply choices, and a panel that shows each line. Picoo's NPC dialogue recipe builds all four: an NPC with a ProximityPrompt, a branching tree the server walks, reply buttons and typewriter text. Prompts like "make a dialogue tree" or "let me talk to an NPC" go straight to that recipe.

8 files · 237 lines · one recipe call. A starter NPC named Sage with a three-node tree; the server checks every reply.

ProximityPrompt on the NPC

The recipe adds Workspace.NPCs.Sage, an anchored 2 × 5 × 2 stud placeholder Part centred at the NPC position (10, 2.5, 0 unless you give one) in the NPC color, with a ProximityPrompt named TalkPrompt. Triggering it asks the server to open that NPC's tree at its start node.

Branching tree

Each node has a line of text and a list of replies, and each reply names the node it leads to. A node marked end shows an End button that closes the conversation.

Server walks the tree

The client sends only the index of the reply it clicked. DialogServer looks it up on the player's current node and picks the next node itself, so a client cannot jump to a node it never reached.

Typewriter text

Each line types out at 0.02 seconds per character. Clicking a reply stops the current line and loads the next node.

Dialogue panel

A panel at the bottom of the screen shows the NPC's name, the line and one button per reply. DialogClient builds it in code with ResetOnSpawn off, and closes it on Escape.

More NPCs, same scripts

TREES in DialogServer is keyed by NPC name. Add a tree under a new name, then give that NPC's part a ProximityPrompt and an NpcDialogName attribute with the same name.

Files Picoo ships for this prompt

8 files · 237 lines · one recipe call

ServerScriptService/DialogServer

The TREES table, each player's current node, and the DialogStart and DialogReply handlers that check every reply.

98 lines

StarterPlayer/StarterPlayerScripts/DialogClient

Builds the dialogue panel, types out each line, draws the reply buttons and opens a tree when a ProximityPrompt fires.

139 lines

ReplicatedStorage/Remotes

Folder for the dialogue remotes.

instance

ReplicatedStorage/Remotes/DialogStart

RemoteFunction: open an NPC's tree at its start node.

instance

ReplicatedStorage/Remotes/DialogReply

RemoteFunction: send the chosen reply's index, get the next node.

instance

Workspace/NPCs

Folder for dialogue NPCs.

instance

Workspace/NPCs/Sage

Placeholder Part for the starter NPC: anchored, 2 × 5 × 2 studs, purple, centred at the NPC position. The server tags it with NpcDialogName = Sage.

instance

Workspace/NPCs/Sage/TalkPrompt

ProximityPrompt that starts the conversation.

instance

Sample output: ServerScriptService/DialogServer

-- Dialog tree per NPC. Extend by adding more NPC keys with start/end nodes.
local TREES = {
	Sage = {
		start = {
			text = "Traveler. What brings you here?",
			replies = {
				{ text = "Tell me about this place.", next = "lore" },
				{ text = "I'm just passing through.", next = "leave" },
			},
		},
		lore = {
			text = "Wisdom lives here in old stones. Walk carefully near the lower halls.",
			replies = {
				{ text = "Thank you.", next = "leave" },
			},
		},
		leave = {
			text = "Walk safely.",
			["end"] = true,
		},
	},
}

local currentNode: { [number]: { npc: string, node: string } } = {}

local function getNode(npcName: string, nodeId: string)
	local tree = TREES[npcName]
	if not tree then return nil end
	return tree[nodeId]
end

local function buildResponse(npcName: string, nodeId: string)
	local node = getNode(npcName, nodeId)
	if not node then return { closed = true } end
	local clientReplies = {}
	if node.replies then
		for i, r in ipairs(node.replies) do
			table.insert(clientReplies, { index = i, text = r.text })
		end
	end
	return {
		npc = npcName,
		text = node.text,
		replies = clientReplies,
		ended = node["end"] == true,
	}
end

DialogStart.OnServerInvoke = function(player: Player, npcArg: any)
	-- Server validates input — never trusts client args
	if type(npcArg) ~= "string" then return { closed = true } end
	if not TREES[npcArg] then return { closed = true } end
	currentNode[player.UserId] = { npc = npcArg, node = "start" }
	return buildResponse(npcArg, "start")
end

DialogReply.OnServerInvoke = function(player: Player, replyIndexArg: any)
	local state = currentNode[player.UserId]
	if not state then return { closed = true } end
	if type(replyIndexArg) ~= "number" then return { closed = true } end

	local replyIndex = math.floor(replyIndexArg)
	local node = getNode(state.npc, state.node)
	if not node or not node.replies or not node.replies[replyIndex] then
		return { closed = true }
	end

	local nextId = node.replies[replyIndex].next
	if not nextId then return { closed = true } end

	local nextNode = getNode(state.npc, nextId)
	if not nextNode then return { closed = true } end

	currentNode[player.UserId] = { npc = state.npc, node = nextId }
	if nextNode["end"] then currentNode[player.UserId] = nil end

	return buildResponse(state.npc, nextId)
end

Building a Roblox dialogue system

Dialogue systems are where Roblox games either feel alive or feel like a quest-vending machine. A good one lets you write a new conversation without touching the engine code, and never lets the client decide where a conversation goes. Picoo's NPC dialogue recipe is built around both.

The recipe writes two scripts and the instances they use. DialogServer holds TREES, a table of conversations keyed by NPC name, and remembers which node each player is on. DialogClient builds the dialogue panel in code and listens for ProximityPrompts. The starter NPC is Workspace.NPCs.Sage, an anchored placeholder part standing at the NPC position with a ProximityPrompt, and its tree has three nodes: a greeting with two replies, a line of lore, and a goodbye.

The tree is plain data. Each node has a line of text and a list of replies, and each reply names the node it leads to. Adding a conversation means adding nodes to TREES, and the engine code stays the same.

The server walks the tree. When a player clicks a reply, the client sends only that reply's index. DialogServer looks it up on the player's current node, resolves the next node itself and returns the text and replies to show. An index that does not exist, or a missing node, closes the dialogue. A client that fires the remote by hand can still only pick from the replies it was shown.

On screen, the panel sits at the bottom with the NPC's name, the line typing out at 0.02 seconds per character, and one button per reply. The last node shows an End button, and Escape closes the panel early. Conditional replies, item or quest actions, portraits and voice lines are follow-up prompts, added to DialogServer on top of the same tree.

See more on the Luau generator, the AI Luau script writer, the 3D model generator and the game builder, read how Picoo compares with ChatGPT, or browse the full blog.

Frequently asked

How do I write my own conversation?+

Edit TREES in DialogServer. Each node has text and replies; each reply has text and next, the id of the node it leads to; a node with ["end"] = true closes the conversation. You can also describe the conversation in your prompt, and the recipe writes it into TREES in place of the starter tree.

Can players skip ahead in a conversation?+

No. The client only sends the index of the reply it clicked. The server looks that reply up on the player's current node, and an index that does not exist or a node that is missing closes the dialogue, so where the conversation goes is always the server's choice.

How do I add more NPCs, or use my own NPC model?+

Add a tree for the new NPC to TREES under its name. Then put a ProximityPrompt in one of the model's parts and set that part's NpcDialogName attribute to the same name. DialogClient listens to every ProximityPrompt in the game and opens the tree named by the prompt's parent.

Can a reply give an item, start a quest or only show when a condition is met?+

Ask for it in a follow-up prompt. In the recipe a reply moves the conversation to its next node; actions and conditions are written into DialogServer on top of the same tree, so the check still runs on the server.

What about portraits, voice lines or translations?+

Ask for them in a follow-up prompt. The recipe's panel shows the NPC's name and the typed line, and a portrait image, voice lines or LocalizationService lookups are added on top of it.

Related Picoo prompts