- Dialogue System
- Expert guidance for building flexible, data-driven dialogue systems.
- NEVER Do
- NEVER hardcode dialogue in scripts
- — Use Resource-based DialogueLine/DialogueGraph. Hardcoded dialogue is unmaintainable for localization.
- NEVER forget to check choice conditions
- — Displaying unavailable choices confuses players. Filter choices by
- check_conditions()
- before showing.
- NEVER use string IDs without validation
- — Typos in
- next_line_id
- cause silent failures. Add
- assert(dialogues.has(line_id))
- checks.
- NEVER skip typewriter effect without player option
- — Some players want instant text. Add "skip typewriter" button or setting.
- NEVER store dialogue state in UI
- — UI should only display. Store current_line/dialogue_id in DialogueManager (AutoLoad) for scene transitions.
- 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 items
Dialogue 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
pass Dialogue 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 = false NPC 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 line Localization
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 Resource-Based - Store dialogues as resources Flag System - Track player choices Typewriter Effect - Adds polish Skip Button - Let players skip Reference Related: godot-signal-architecture , godot-save-load-systems , godot-ui-rich-text Related Master Skill: godot-master