godot-dialogue-system
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDialogue 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 before showing.
check_conditions() - NEVER use string IDs without validation — Typos in cause silent failures. Add
next_line_idchecks.assert(dialogues.has(line_id)) - 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.
- 绝对不要在脚本中硬编码对话——使用基于资源的DialogueLine/DialogueGraph。硬编码对话不利于本地化维护。
- 绝对不要忘记检查选择条件——显示不可用的选择会让玩家困惑。显示前通过过滤选项。
check_conditions() - 绝对不要使用未验证的字符串ID——中的拼写错误会导致静默失败。添加
next_line_id检查。assert(dialogues.has(line_id)) - 绝对不要在没有玩家选项的情况下跳过打字机效果——有些玩家希望文本立即显示。添加“跳过打字机”按钮或设置。
- 绝对不要在UI中存储对话状态——UI应仅负责显示。在DialogueManager(AutoLoad)中存储current_line/dialogue_id以支持场景切换。
Available Scripts
可用脚本
MANDATORY: Read the appropriate script before implementing the corresponding pattern.
强制要求:在实现对应模式前,请阅读相应脚本。
dialogue_engine.gd
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.
基于图的对话系统,支持BBCode信号标签。解析文本中的[trigger:event_id]标签,触发信号,并加载外部JSON对话图。
dialogue_manager.gd
dialogue_manager.gd
Data-driven dialogue engine with branching, variable storage, and conditional choices.
支持分支、变量存储和条件选择的数据驱动型对话引擎。
Dialogue Data
对话数据
gdscript
undefinedgdscript
undefineddialogue_line.gd
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 = ""
```gdscriptclass_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 = ""
```gdscriptdialogue_choice.gd
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
undefinedclass_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
undefinedDialogue Manager
对话管理器
gdscript
undefinedgdscript
undefineddialogue_manager.gd (AutoLoad)
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
undefinedextends 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
undefinedDialogue UI
对话UI
gdscript
undefinedgdscript
undefineddialogue_ui.gd
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
undefinedextends 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
undefinedNPC Interaction
NPC交互
gdscript
undefinedgdscript
undefinednpc.gd
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)
undefinedextends 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)
undefinedDialogue Graph (Resource)
对话图(资源)
gdscript
undefinedgdscript
undefineddialogue_graph.gd
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
undefinedclass_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
undefinedLocalization
本地化
gdscript
undefinedgdscript
undefinedUse Godot's built-in CSV import
Use Godot's built-in CSV import
dialogue_en.csv:
dialogue_en.csv:
dialogue_id,speaker,text
dialogue_id,speaker,text
npc_1_start,Hero,"Hello!"
npc_1_start,Hero,"Hello!"
npc_1_response,NPC,"Greetings!"
npc_1_response,NPC,"Greetings!"
func get_localized_line(line_id: String) -> String:
return tr(line_id)
undefinedfunc get_localized_line(line_id: String) -> String:
return tr(line_id)
undefinedAdvanced: Voice Acting
进阶:配音
gdscript
@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()gdscript
@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-systemsgodot-ui-rich-text
- 相关:,
godot-signal-architecture,godot-save-load-systemsgodot-ui-rich-text
Related
相关内容
- Master Skill: godot-master
- 核心技能:godot-master