godot-dialogue-system

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

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.

  • 绝对不要在脚本中硬编码对话——使用基于资源的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
undefined
gdscript
undefined

dialogue_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 = ""

```gdscript
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 = ""

```gdscript

dialogue_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
undefined
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
undefined

Dialogue Manager

对话管理器

gdscript
undefined
gdscript
undefined

dialogue_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
undefined
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
undefined

Dialogue UI

对话UI

gdscript
undefined
gdscript
undefined

dialogue_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
undefined
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
undefined

NPC Interaction

NPC交互

gdscript
undefined
gdscript
undefined

npc.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)
undefined
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)
undefined

Dialogue Graph (Resource)

对话图(资源)

gdscript
undefined
gdscript
undefined

dialogue_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
undefined
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
undefined

Localization

本地化

gdscript
undefined
gdscript
undefined

Use 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)
undefined
func get_localized_line(line_id: String) -> String: return tr(line_id)
undefined

Advanced: 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

最佳实践

  1. Resource-Based - Store dialogues as resources
  2. Flag System - Track player choices
  3. Typewriter Effect - Adds polish
  4. Skip Button - Let players skip
  1. 基于资源 - 将对话存储为资源
  2. 标记系统 - 跟踪玩家选择
  3. 打字机效果 - 提升质感
  4. 跳过按钮 - 允许玩家跳过

Reference

参考

  • Related:
    godot-signal-architecture
    ,
    godot-save-load-systems
    ,
    godot-ui-rich-text
  • 相关:
    godot-signal-architecture
    ,
    godot-save-load-systems
    ,
    godot-ui-rich-text

Related

相关内容

  • Master Skill: godot-master
  • 核心技能:godot-master