
Godot Dialogue System
- 230 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-dialogue-system for development tasks
About
godot-dialogue-system: A skill for development. This provides functionality for development workflows.
- godot-dialogue-system
Godot Dialogue System by the numbers
- 230 all-time installs (skills.sh)
- +11 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #1,668 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-dialogue-systemAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 230 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-dialogue-system for development tasks
Files
Dialogue System
Expert guidance for building flexible, data-driven dialogue systems.
Available Scripts
dialogue_resource.gd
Data-driven conversation tree container using Resources for modular, branching narrative paths.
dialogue_node_data.gd
Serialized data structure for a single line of dialogue, including speaker metadata and portraits.
dialogue_option_data.gd
Interactive player choice definition with branching logic and scriptable availability conditions.
dialogue_manager_singleton.gd
Centralized AutoLoad orchestrator for traversing dialogue trees and broadcasting state signals.
dialogue_ui_controller.gd
Reactive UI bridge that maps dialogue data to visual labels and dynamic choice buttons.
typebox_effect.gd
Polished "Character-by-character" text reveal effect using Godot's built-in Tweens.
dialogue_event_bridge.gd
Bridge node for triggering external game events (e.g. starting a quest) from conversation nodes.
branching_condition_validator.gd
Expert logic for evaluating player stats or global flags to toggle dialogue choices.
localized_dialogue_resource.gd
Advanced strategy for supporting multi-language conversation text via translation keys.
dialogue_portrait_manager.gd
Visual controller for managing character expressions and entry animations during dialogue.
NEVER Do in Dialogue Systems
- NEVER hardcode dialogue text directly in your GDScript files — This makes translation impossible. Store text in Resources or external JSON/CSV files [12].
- NEVER display choices that the player hasn't met the criteria for — Hidden choices should stay hidden unless they are "grayed out" intentionally to show a missed path [13].
- NEVER use loose strings for node transitions without validation — Typos in
next_node_idwill crash the dialogue mid-convo. Useassert()or a central ID registry [14]. - NEVER force a typewriter effect without a "Skip" option — Forcing players to read at a fixed speed leads to frustration. Always allow clicking to finish the line [15].
- NEVER store the current dialogue state inside a UI node — If the UI is closed or the scene changes, the player loses their place. Use an AutoLoad
DialogueManager[16]. - NEVER use `get_node()` to find dialogue UI from the NPC script — Use signals like
DialogueManager.start_dialogue(res)to maintain a decoupled architecture. - NEVER use complex regex for simple text tags — Godot's
RichTextLabelsupports BBCode tags natively. Use[b],[i], and[url]for formatting. - NEVER perform save/load operations inside a dialogue node — Conversation nodes should be pure data. Delegate persistence to a dedicated
SaveSystem. - NEVER block the main thread for text reveal timing — Never use
OS.delay_msec(). Usecreate_timer()orTweento maintain smooth 60fps performance. - NEVER hardcode portrait paths — Assign textures directly to the
DialogueNoderesource in the inspector or use a centralPortraitDatabase.
---
Available Scripts
MANDATORY: Read the appropriate script before implementing the corresponding pattern.
dialogue_engine.gd
Graph-based dialogue with BBCode signal tags. Parses [trigger:event_id] tags from text, fires signals, and loads external JSON dialogue graphs.
dialogue_manager.gd
Data-driven dialogue engine with branching, variable storage, and conditional choices.
---
Dialogue Data
# dialogue_line.gd
class_name DialogueLine
extends Resource
@export var speaker: String
@export_multiline var text: String
@export var portrait: Texture2D
@export var choices: Array[DialogueChoice] = []
@export var conditions: Array[String] = [] # Quest flags, etc.
@export var next_line_id: String = ""# dialogue_choice.gd
class_name DialogueChoice
extends Resource
@export var choice_text: String
@export var next_line_id: String
@export var conditions: Array[String] = []
@export var effects: Array[String] = [] # Set flags, give itemsDialogue Manager
# dialogue_manager.gd (AutoLoad)
extends Node
signal dialogue_started
signal dialogue_ended
signal line_displayed(line: DialogueLine)
signal choice_selected(choice: DialogueChoice)
var dialogues: Dictionary = {}
var flags: Dictionary = {}
func load_dialogue(path: String) -> void:
var data := load(path)
dialogues[path] = data
func start_dialogue(dialogue_id: String, start_line: String = "start") -> void:
dialogue_started.emit()
display_line(dialogue_id, start_line)
func display_line(dialogue_id: String, line_id: String) -> void:
var line: DialogueLine = dialogues[dialogue_id].lines[line_id]
# Check conditions
if not check_conditions(line.conditions):
# Skip to next
if line.next_line_id:
display_line(dialogue_id, line.next_line_id)
else:
end_dialogue()
return
line_displayed.emit(line)
# Auto-advance or wait for player
if line.choices.is_empty() and line.next_line_id:
# Wait for player to click
await get_tree().create_timer(0.1).timeout
elif line.choices.is_empty():
end_dialogue()
func select_choice(dialogue_id: String, choice: DialogueChoice) -> void:
choice_selected.emit(choice)
# Apply effects
for effect in choice.effects:
apply_effect(effect)
# Continue to next line
if choice.next_line_id:
display_line(dialogue_id, choice.next_line_id)
else:
end_dialogue()
func end_dialogue() -> void:
dialogue_ended.emit()
func check_conditions(conditions: Array[String]) -> bool:
for condition in conditions:
if not flags.get(condition, false):
return false
return true
func apply_effect(effect: String) -> void:
# Parse effect string, e.g., "set_flag:met_npc"
var parts := effect.split(":")
match parts[0]:
"set_flag":
flags[parts[1]] = true
"give_item":
# Integration with inventory
passDialogue UI
# dialogue_ui.gd
extends Control
@onready var speaker_label := $Panel/Speaker
@onready var text_label := $Panel/Text
@onready var portrait := $Panel/Portrait
@onready var choices_container := $Panel/Choices
var current_dialogue: String
var current_line: DialogueLine
func _ready() -> void:
DialogueManager.line_displayed.connect(_on_line_displayed)
DialogueManager.dialogue_ended.connect(_on_dialogue_ended)
visible = false
func _on_line_displayed(line: DialogueLine) -> void:
visible = true
current_line = line
speaker_label.text = line.speaker
portrait.texture = line.portrait
# Typewriter effect
text_label.text = ""
for char in line.text:
text_label.text += char
await get_tree().create_timer(0.03).timeout
# Show choices
if line.choices.is_empty():
# Wait for input to continue
pass
else:
show_choices(line.choices)
func show_choices(choices: Array[DialogueChoice]) -> void:
# Clear existing
for child in choices_container.get_children():
child.queue_free()
# Add choice buttons
for choice in choices:
if not DialogueManager.check_conditions(choice.conditions):
continue
var button := Button.new()
button.text = choice.choice_text
button.pressed.connect(func(): _on_choice_selected(choice))
choices_container.add_child(button)
func _on_choice_selected(choice: DialogueChoice) -> void:
DialogueManager.select_choice(current_dialogue, choice)
func _on_dialogue_ended() -> void:
visible = falseNPC Interaction
# npc.gd
extends CharacterBody2D
@export var dialogue_path: String = "res://dialogues/npc_1.tres"
@export var start_line: String = "start"
func interact() -> void:
DialogueManager.start_dialogue(dialogue_path, start_line)Dialogue Graph (Resource)
# dialogue_graph.gd
class_name DialogueGraph
extends Resource
@export var lines: Dictionary = {} # line_id → DialogueLine
func _init() -> void:
# Example structure
lines["start"] = create_line("Hero", "Hello!")
lines["response"] = create_line("NPC", "Greetings, traveler!")
func create_line(speaker: String, text: String) -> DialogueLine:
var line := DialogueLine.new()
line.speaker = speaker
line.text = text
return lineLocalization
# Use Godot's built-in CSV import
# dialogue_en.csv:
# dialogue_id,speaker,text
# npc_1_start,Hero,"Hello!"
# npc_1_response,NPC,"Greetings!"
func get_localized_line(line_id: String) -> String:
return tr(line_id)Advanced: Voice Acting
@onready var voice_player := $AudioStreamPlayer
func play_voice_line(line_id: String) -> void:
var audio := load("res://voice/" + line_id + ".mp3")
if audio:
voice_player.stream = audio
voice_player.play()Best Practices
1. Resource-Based - Store dialogues as resources 2. Flag System - Track player choices 3. Typewriter Effect - Adds polish 4. Skip Button - Let players skip
---
Elite Godot 4.x Patterns
1. Custom Dialogue Graph Editor
Leverage GraphEdit and GraphNode to build visual authoring tools for complex branching narratives.
@tool
class_name DialogueGraphEditor extends GraphEdit
func link_nodes(from: StringName, from_port: int, to: StringName, to_port: int) -> void:
# Programmatic connection of visual dialogue blocks
var err := connect_node(from, from_port, to, to_port)
if err == OK:
print_rich("[color=green]Branch linked.[/color]")2. Audio-Driven Dialogue (TTS & Lipsync)
Use DisplayServer for asynchronous Text-to-Speech and register utterance callbacks to drive mouth animations or viseme changes in real-time.
# dialogue_lipsync.gd
func start_speaking(text: String) -> void:
# 1. Register boundary callback
var cb := Callable(self, "_on_tts_boundary")
DisplayServer.tts_set_utterance_callback(DisplayServer.TTS_UTTERANCE_BOUNDARY, cb)
# 2. Speak asynchronously
var voices := DisplayServer.tts_get_voices_for_language("en")
DisplayServer.tts_speak(text, voices[0])
func _on_tts_boundary(char_idx: int, _id: int) -> void:
# Drive lipsync/animation based on current character index
_update_mouth_shape(char_idx)3. Dialogue Analytics Logger
Implement a custom Logger to intercept and record player choices without cluttering conversation logic with I/O calls.
# dialogue_stat_logger.gd
class_name DialogueStatLogger extends Logger
func _log_message(msg: String, is_error: bool) -> void:
if not is_error and msg.begins_with("[CHOICE]"):
# Process and record choice analytics (e.g., save to file or send to server)
_record_analytics(msg)
# Register in an AutoLoad's _init()
static func initialize() -> void:
OS.add_logger(DialogueStatLogger.new())Reference
- Master Skill: godot-master
# branching_condition_validator.gd
# Evaluating player stats for dialogue choices
extends Node
# EXPERT NOTE: Use a validator to check if options should
# be hidden based on player variables (e.g. Strength > 10).
func is_option_available(option: DialogueOption) -> bool:
if option.condition_script == "": return true
# Dynamic evaluation logic...
return true
# skills/dialogue-system/code/dialogue_engine.gd
extends Node
## Dialogue Engine Expert Pattern
## Features Graph-Based Branching and Signal-Tag Parsing.
signal narrative_event(event_id: String)
signal dialogue_finished
## BBCode Custom Tags Example: [trigger:shake]
var trigger_regex = RegEx.new()
func _ready() -> void:
trigger_regex.compile("\\[trigger:(\\w+)\\]")
func process_dialogue_line(line_text: String) -> String:
# 1. Parse Signal Callbacks
# Scans text for tags like [trigger:shake_screen] and fires signals.
var matches = trigger_regex.search_all(line_text)
for m in matches:
var event_name = m.get_string(1)
narrative_event.emit(event_name)
# 2. Cleanup Text
# Returns the 'clean' text without the tags for the UI to display.
return trigger_regex.sub(line_text, "", true)
func load_dialogue_graph(file_path: String) -> Dictionary:
# 3. Graph Serialization
# Professionals store dialogue in external JSON or Resources.
if FileAccess.file_exists(file_path):
var file = FileAccess.open(file_path, FileAccess.READ)
return JSON.parse_string(file.get_as_text())
return {}
## EXPERT NOTE:
## Use Godot's 'RichTextLabel' for dialogue. It natively supports
## BBCode and custom 'RichTextEffect' objects for 'wavy' or 'shaking' text.
# dialogue_event_bridge.gd
# Triggering game logic from conversation nodes
extends Node
func _ready():
DialogueManager.line_started.connect(_on_line_started)
func _on_line_started(node: DialogueNode):
if node.event_signal != "":
# Assumes a GlobalEventBus or similar
if get_tree().root.has_node("GlobalBus"):
get_node("/root/GlobalBus").emit_signal(node.event_signal)
# dialogue_manager_singleton.gd
# Managing conversation state and signals
extends Node
# EXPERT NOTE: The DialogueManager handles the traversal
# of the DialogueResource tree.
signal line_started(node: DialogueNode)
signal dialogue_finished
var current_dialogue: DialogueResource
var current_node: DialogueNode
func start_dialogue(res: DialogueResource):
current_dialogue = res
_show_node(res.start_node)
func select_option(index: int):
var option = current_node.options[index]
_show_node(option.next_node_id)
func _show_node(node_id: String):
if node_id == "end" or not current_dialogue.nodes.has(node_id):
dialogue_finished.emit()
return
current_node = current_dialogue.nodes[node_id]
line_started.emit(current_node)
# skills/dialogue-system/scripts/dialogue_manager.gd
extends Node
## Dialogue Manager Expert Pattern
## Data-driven dialogue system with branching and conditional logic.
class_name DialogueManager
signal dialogue_started
signal line_started(character: String, text: String)
signal choices_presented(choices: Array)
signal dialogue_ended
var _current_dialogue_resource: Dictionary
var _current_node_id: String
var _variables: Dictionary = {}
# Example Data Structure:
# {
# "start": {
# "text": "Hello traveler.",
# "character": "Guard",
# "next": "choice_1"
# },
# "choice_1": {
# "type": "choice",
# "choices": [
# {"text": "Hi!", "next": "greeting_friendly"},
# {"text": "Move aside.", "next": "confrontation"}
# ]
# }
# }
func start_dialogue(dialogue_data: Dictionary, start_node := "start") -> void:
_current_dialogue_resource = dialogue_data
_current_node_id = start_node
dialogue_started.emit()
_process_node(_current_node_id)
func advance_dialogue(choice_index := -1) -> void:
var node = _current_dialogue_resource.get(_current_node_id)
if not node:
end_dialogue()
return
if node.get("type") == "choice":
if choice_index < 0:
push_error("Must provide choice index for choice node")
return
var choices = node.get("choices", [])
if choice_index >= choices.size():
push_error("Invalid choice index")
return
var next_id = choices[choice_index].get("next")
_process_node(next_id)
else:
# Standard text node
if node.has("next"):
_process_node(node["next"])
else:
end_dialogue()
func _process_node(node_id: String) -> void:
_current_node_id = node_id
var node = _current_dialogue_resource.get(node_id)
if not node:
end_dialogue()
return
# Execute side effects (variable setting)
if node.has("set_var"):
var setter = node["set_var"]
_variables[setter.key] = setter.value
# Check conditions
if node.has("condition_key"):
var key = node["condition_key"]
var req_val = node["condition_value"]
if _variables.get(key) != req_val:
# Condition failed, go to else_next
if node.has("else_next"):
_process_node(node["else_next"])
return
var type = node.get("type", "text")
if type == "text":
line_started.emit(node.get("character", "???"), node.get("text", "..."))
elif type == "choice":
choices_presented.emit(node.get("choices", []))
func end_dialogue() -> void:
dialogue_ended.emit()
## EXPERT USAGE:
## var dlg = DialogueManager.new()
## dlg.line_started.connect(_on_line)
## dlg.start_dialogue(json_data)
# dialogue_node_data.gd
# Single step in a conversation
class_name DialogueNode extends Resource
@export var speaker_name: String = ""
@export var portrait: Texture2D
@export_multiline var text: String = ""
@export var options: Array[DialogueOption] = []
@export var event_signal: String = "" # Optional signal to emit
# dialogue_option_data.gd
# Interactive player choices
class_name DialogueOption extends Resource
@export var text: String = ""
@export var next_node_id: String = ""
@export var condition_script: String = "" # Optional GDScript snippet
# dialogue_portrait_manager.gd
# Handling speaker expressions visually
extends TextureRect
func _ready():
DialogueManager.line_started.connect(_on_line_started)
func _on_line_started(node: DialogueNode):
if node.portrait:
texture = node.portrait
# Add tween for "entry" animation
# dialogue_resource.gd
# Data-driven conversation tree
class_name DialogueResource extends Resource
# EXPERT NOTE: Dialogue should be stored in Resources to
# allow for branching paths and localization keys.
@export var start_node: String = "start"
@export var nodes: Dictionary = {} # node_id -> DialogueNode
# dialogue_ui_controller.gd
# Visual rendering of conversation lines
extends Control
@onready var text_label = $TextLabel
@onready var options_container = $OptionsContainer
func _ready():
DialogueManager.line_started.connect(_on_line_started)
func _on_line_started(node: DialogueNode):
text_label.text = node.text
_clear_options()
for i in range(node.options.size()):
var btn = Button.new()
btn.text = node.options[i].text
btn.pressed.connect(DialogueManager.select_option.bind(i))
options_container.add_child(btn)
func _clear_options():
for child in options_container.get_children():
child.queue_free()
# localized_dialogue_resource.gd
# Professional localization strategy
extends DialogueResource
# EXPERT NOTE: Store translation keys in nodes instead
# of raw text to support multiple languages via .csv files.
func get_node_text(node_id: String) -> String:
var node = nodes[node_id]
return tr(node.text_key)
# typebox_effect.gd
# Character-by-character text reveal
extends Label
@export var chars_per_second: float = 30.0
func display_text(new_text: String):
text = new_text
visible_ratio = 0.0
var duration = new_text.length() / chars_per_second
var tween = create_tween()
tween.tween_property(self, "visible_ratio", 1.0, duration)