Dialogue Systems
Create branching dialogue trees with choices and consequences.
Introduction
Dialogue systems power conversations with NPCs. This guide covers dialogue trees, branching choices, conditions, and consequences with data-driven design.
Guide 18 of 25
Screenshot
Step-by-Step Guide
Design dialogue data
Create a data structure for dialogue: nodes with text, speaker, choices, conditions, and consequences.
Create a dialogue UI
Build a panel with speaker name, text (typewriter effect), and choice buttons at the bottom.
Implement branching
Each choice links to a different node. Some choices have conditions (requires item, quest flag).
Add typewriter effect
Reveal text character by character (30-50 chars/sec). Allow skipping to full text on click.
Handle consequences
Choices can give items, set quest flags, change reputation, or trigger events. Process on choice selection.
Save dialogue state
Track which dialogue nodes have been visited. Prevents repeating and unlocks new dialogue based on history.
FluxScript Example
Here is a complete FluxScript implementation for this guide:
const DIALOGUE = {
npc_blacksmith: {
start: {
speaker: "Borin the Blacksmith",
text: "Ah, a traveler! My daughter was kidnapped by goblins. Will you help?",
choices: [
{ text: "I will help.", next: "accept" },
{ text: "Not my problem.", next: "refuse" },
{ text: "What is the reward?", next: "reward" },
{ text: "[Persuade] Tell me more.", next: "persuade", condition: "persuasion > 5" }
]
},
accept: {
speaker: "Borin",
text: "Thank you! Take this sword.",
action: { giveItem: "iron_sword", startQuest: "rescue_daughter" },
next: "end"
},
refuse: {
speaker: "Borin",
text: "I see. If you change your mind, I will be here.",
next: "end"
},
end: { end: true }
}
}
currentNPC = null
currentNode = null
textIndex = 0
func startDialogue(npcId) {
currentNPC = npcId
currentNode = DIALOGUE[npcId].start
gameRunning = false
ui.show("dialoguePanel")
showNode(currentNode)
}
func showNode(node) {
if node.end { endDialogue(); return }
if node.action applyAction(node.action)
ui.setText("speakerName", node.speaker)
textIndex = 0
ui.setText("dialogueText", "")
timer.start("typewriter", 0.03, node.text.length)
ui.clearChoices()
for i = 0 to (node.choices || []).length {
choice = node.choices[i]
if choice.condition == null || evalCondition(choice.condition) {
ui.addChoice(choice.text, choice.next)
}
}
}
on timer("typewriter") {
textIndex += 1
ui.setText("dialogueText", string.substring(currentNode.text, 0, textIndex))
if textIndex >= currentNode.text.length timer.stop("typewriter")
}
func endDialogue() {
ui.hide("dialoguePanel")
gameRunning = true
}Node Graph Example
Prefer visual programming? Here is the equivalent Node Graph:
[Function: startDialogue] --> [Set: currentNPC, node]
--> [Pause: gameplay] --> [Show: dialoguePanel]
--> [Show Node]
[Function: showNode] --> [Branch: end]
+-- True --> [End Dialogue]
+-- False --> [Apply: action]
--> [Set: speaker name]
--> [Start: typewriter timer]
--> [Add: filtered choices]
[On Timer "typewriter"] --> [textIndex += 1]
--> [Set Text: substring]
--> [Branch: complete] --> [Stop]Optimization Tips
Keep your implementation fast and efficient with these optimization tips:
- Use data-driven dialogue. Store dialogue in JSON-like structures, not hardcoded in scripts. Lets writers edit without code.
- Cache dialogue text. Loading and parsing dialogue data every conversation is wasteful. Load once and cache.
- Use conditions for branching. Gate choices behind stats, items, or quest flags for meaningful player agency.
- Pool dialogue UI elements. Reuse choice buttons instead of creating/destroying them per node. Prevents GC spikes.
Common Mistakes
Hardcoding dialogue in scripts
Use a data structure. Hardcoded dialogue is unmaintainable — writers cannot edit it.
No typewriter skip
Always allow clicking to skip the typewriter effect. Forced slow text frustrates fast readers.
