godot-signal-architecture
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSignal Architecture
信号架构
Signal Up/Call Down pattern, typed signals, and event buses define decoupled, maintainable architectures.
“信号向上/调用向下”模式、类型化信号和事件总线共同定义了解耦且可维护的架构。
Available Scripts
可用脚本
global_event_bus.gd
global_event_bus.gd
Expert AutoLoad event bus with typed signals and connection management.
带有类型化信号和连接管理功能的专家级AutoLoad事件总线。
signal_debugger.gd
signal_debugger.gd
Runtime signal connection analyzer. Shows all connections in scene hierarchy.
运行时信号连接分析器。显示场景层级中的所有连接。
signal_spy.gd
signal_spy.gd
Testing utility for observing signal emissions with count tracking and history.
MANDATORY - For Event Bus: Read global_event_bus.gd before implementing cross-scene communication.
用于观察信号发射的测试工具,包含计数跟踪和历史记录功能。
事件总线必备事项:在实现跨场景通信前,请先阅读global_event_bus.gd。
NEVER Do in Signal Architecture
信号架构中的绝对禁忌
- NEVER create circular signal dependencies — A signals to B, B signals back to A? Infinite loops + stack overflow. Use mediator (parent OR AutoLoad) to break cycle.
- NEVER skip signal typing — without types? No autocomplete OR type safety. Use
signal movedfor editor support.signal moved(direction: Vector2) - NEVER forget to disconnect signals — Node freed but signal still connected? "Attempt to call on null instance" error. Disconnect in OR use
_exit_tree().CONNECT_REFERENCE_COUNTED - NEVER connect signals in _ready() for dynamic nodes — Enemy spawned after level load? Signals not connected. Connect when instantiating OR use groups + pattern.
await - NEVER use signals for parent→child — Parent signaling to child breaks encapsulation. CALL DOWN directly: . Reserve signals for child→parent communication.
child.method() - NEVER emit signals with side effects — calls
died.emit()inside? Listeners can't respond before node freed. Emit FIRST, then cleanup.queue_free() - NEVER use string-based signal names — typo = silent failure. Use direct reference:
connect("heath_chnaged", ...).player.health_changed.connect(...)
Use Signals For:
- UI button presses → game logic
- Player death → game over screen
- Item collected → inventory update
- Enemy killed → score update
- Cross-scene communication via AutoLoad
Use Direct Calls For:
- Parent controlling child behavior
- Accessing child properties
- Simple, local interactions
- 绝对不要创建循环信号依赖 — A向B发送信号,B又向A发送信号?会导致无限循环和栈溢出。使用中介者(父节点或AutoLoad)来打破循环。
- 绝对不要跳过信号类型定义 — 不带类型的?没有自动补全或类型安全保障。请使用
signal moved以获得编辑器支持。signal moved(direction: Vector2) - 绝对不要忘记断开信号连接 — 节点已被释放但信号仍保持连接?会出现“尝试调用空实例”错误。在中断开连接,或使用
_exit_tree()。CONNECT_REFERENCE_COUNTED - 绝对不要在动态节点的_ready()中连接信号 — 敌人在关卡加载后才生成?信号不会被连接。请在实例化时连接,或使用组 + 模式。
await - 绝对不要用信号实现父→子通信 — 父节点向子节点发送信号会破坏封装性。直接向下调用:。信号应保留给子→父通信使用。
child.method() - 绝对不要在信号中包含副作用 — 内部调用
died.emit()?监听器无法在节点被释放前做出响应。先发射信号,再执行清理操作。queue_free() - 绝对不要使用基于字符串的信号名称 — 的拼写错误会导致静默失败。请使用直接引用:
connect("heath_chnaged", ...)。player.health_changed.connect(...)
信号的适用场景:
- UI按钮点击 → 游戏逻辑
- 玩家死亡 → 游戏结束界面
- 物品收集 → 背包更新
- 敌人被击杀 → 分数更新
- 通过AutoLoad实现跨场景通信
直接调用的适用场景:
- 父节点控制子节点行为
- 访问子节点属性
- 简单的本地交互
Implementation Patterns
实现模式
Pattern 1: Define Typed Signals
模式1:定义类型化信号
gdscript
extends CharacterBody2Dgdscript
extends CharacterBody2D✅ Good - typed signals (Godot 4.x)
✅ 推荐 - 类型化信号(Godot 4.x)
signal health_changed(new_health: int, max_health: int)
signal died()
signal item_collected(item_name: String, item_type: int)
signal health_changed(new_health: int, max_health: int)
signal died()
signal item_collected(item_name: String, item_type: int)
❌ Bad - untyped signals
❌ 不推荐 - 无类型信号
signal health_changed
signal died
undefinedsignal health_changed
signal died
undefinedPattern 2: Emit Signals on State Changes
模式2:状态变化时发射信号
gdscript
undefinedgdscript
undefinedplayer.gd
player.gd
extends CharacterBody2D
signal health_changed(current: int, maximum: int)
signal died()
var health: int = 100:
set(value):
health = clamp(value, 0, max_health)
health_changed.emit(health, max_health)
if health <= 0:
died.emit()
var max_health: int = 100
func take_damage(amount: int) -> void:
health -= amount # Triggers setter, which emits signal
undefinedextends CharacterBody2D
signal health_changed(current: int, maximum: int)
signal died()
var health: int = 100:
set(value):
health = clamp(value, 0, max_health)
health_changed.emit(health, max_health)
if health <= 0:
died.emit()
var max_health: int = 100
func take_damage(amount: int) -> void:
health -= amount # 触发setter,进而发射信号
undefinedPattern 3: Connect Signals in Parent
模式3:在父节点中连接信号
gdscript
undefinedgdscript
undefinedgame.gd (parent)
game.gd (父节点)
extends Node2D
@onready var player: CharacterBody2D = $Player
@onready var ui: Control = $UI
func _ready() -> void:
# Connect child signals
player.health_changed.connect(_on_player_health_changed)
player.died.connect(_on_player_died)
func _on_player_health_changed(current: int, maximum: int) -> void:
# Call down to UI
ui.update_health_bar(current, maximum)
func _on_player_died() -> void:
# Orchestrate game over
ui.show_game_over()
get_tree().paused = true
undefinedextends Node2D
@onready var player: CharacterBody2D = $Player
@onready var ui: Control = $UI
func _ready() -> void:
# 连接子节点信号
player.health_changed.connect(_on_player_health_changed)
player.died.connect(_on_player_died)
func _on_player_health_changed(current: int, maximum: int) -> void:
# 向下调用UI
ui.update_health_bar(current, maximum)
func _on_player_died() -> void:
# 统筹游戏结束流程
ui.show_game_over()
get_tree().paused = true
undefinedPattern 4: Global Signals via AutoLoad
模式4:通过AutoLoad实现全局信号
For cross-scene communication:
gdscript
undefined用于跨场景通信:
gdscript
undefinedevents.gd (AutoLoad)
events.gd (AutoLoad)
extends Node
signal level_completed(level_number: int)
signal player_spawned(player: Node2D)
signal boss_defeated(boss_name: String)
extends Node
signal level_completed(level_number: int)
signal player_spawned(player: Node2D)
signal boss_defeated(boss_name: String)
Any script can emit:
任何脚本都可以发射:
Events.level_completed.emit(3)
Events.level_completed.emit(3)
Any script can listen:
任何脚本都可以监听:
Events.level_completed.connect(_on_level_completed)
undefinedEvents.level_completed.connect(_on_level_completed)
undefinedAdvanced Patterns
高级模式
Pattern 5: Signal Chains
模式5:信号链
gdscript
undefinedgdscript
undefinedenemy.gd
enemy.gd
signal died(score_value: int)
func _on_health_depleted() -> void:
died.emit(100)
queue_free()
signal died(score_value: int)
func _on_health_depleted() -> void:
died.emit(100)
queue_free()
combat_manager.gd
combat_manager.gd
func _ready() -> void:
for enemy in get_tree().get_nodes_in_group("enemies"):
enemy.died.connect(_on_enemy_died)
func _on_enemy_died(score_value: int) -> void:
GameManager.add_score(score_value)
Events.enemy_killed.emit()
undefinedfunc _ready() -> void:
for enemy in get_tree().get_nodes_in_group("enemies"):
enemy.died.connect(_on_enemy_died)
func _on_enemy_died(score_value: int) -> void:
GameManager.add_score(score_value)
Events.enemy_killed.emit()
undefinedPattern 6: One-Shot Connections
模式6:一次性连接
For single-use signal connections:
gdscript
undefined用于仅需触发一次的信号连接:
gdscript
undefinedConnect with CONNECT_ONE_SHOT flag
使用CONNECT_ONE_SHOT标志连接
timer.timeout.connect(_on_timer_timeout, CONNECT_ONE_SHOT)
func _on_timer_timeout() -> void:
print("This only fires once")
# Connection automatically removed
undefinedtimer.timeout.connect(_on_timer_timeout, CONNECT_ONE_SHOT)
func _on_timer_timeout() -> void:
print("This only fires once")
# 连接会自动移除
undefinedPattern 7: Custom Signal Arguments
模式7:自定义信号参数
gdscript
undefinedgdscript
undefineditem.gd
item.gd
signal picked_up(item_data: Dictionary)
func _on_player_enter() -> void:
picked_up.emit({
"name": item_name,
"type": item_type,
"value": item_value,
"icon": item_icon
})
signal picked_up(item_data: Dictionary)
func _on_player_enter() -> void:
picked_up.emit({
"name": item_name,
"type": item_type,
"value": item_value,
"icon": item_icon
})
inventory.gd
inventory.gd
func _on_item_picked_up(item_data: Dictionary) -> void:
add_item(
item_data.name,
item_data.type,
item_data.value
)
undefinedfunc _on_item_picked_up(item_data: Dictionary) -> void:
add_item(
item_data.name,
item_data.type,
item_data.value
)
undefinedBest Practices
最佳实践
1. Descriptive Signal Names
1. 使用描述性的信号名称
gdscript
undefinedgdscript
undefined✅ Good
✅ 推荐
signal button_pressed()
signal enemy_defeated(enemy_type: String)
signal animation_finished(animation_name: String)
signal button_pressed()
signal enemy_defeated(enemy_type: String)
signal animation_finished(animation_name: String)
❌ Bad
❌ 不推荐
signal pressed()
signal done()
signal finished()
undefinedsignal pressed()
signal done()
signal finished()
undefined2. Avoid Circular Dependencies
2. 避免循环依赖
gdscript
undefinedgdscript
undefined❌ BAD: A signals to B, B signals back to A
❌ 错误示例:A向B发信号,B向A发信号
A.gd
A.gd
signal data_requested
func _ready():
B.data_ready.connect(_on_data_ready)
data_requested.emit()
signal data_requested
func _ready():
B.data_ready.connect(_on_data_ready)
data_requested.emit()
B.gd
B.gd
signal data_ready
func _ready():
A.data_requested.connect(_on_data_requested)
signal data_ready
func _ready():
A.data_requested.connect(_on_data_requested)
✅ GOOD: Use a mediator (parent or AutoLoad)
✅ 正确示例:使用中介者(父节点或AutoLoad)
Parent.gd
Parent.gd
func _ready():
A.data_requested.connect(_on_A_data_requested)
B.data_ready.connect(_on_B_data_ready)
undefinedfunc _ready():
A.data_requested.connect(_on_A_data_requested)
B.data_ready.connect(_on_B_data_ready)
undefined3. Disconnect Signals When Nodes Are Freed
3. 节点释放时断开信号连接
gdscript
func _ready() -> void:
player.died.connect(_on_player_died)
func _exit_tree() -> void:
if player and player.died.is_connected(_on_player_died):
player.died.disconnect(_on_player_died)Or use automatic cleanup:
gdscript
undefinedgdscript
func _ready() -> void:
player.died.connect(_on_player_died)
func _exit_tree() -> void:
if player and player.died.is_connected(_on_player_died):
player.died.disconnect(_on_player_died)或使用自动清理:
gdscript
undefinedSignal auto-disconnects when this node is freed
当此节点被释放时,信号会自动断开
player.died.connect(_on_player_died, CONNECT_REFERENCE_COUNTED)
undefinedplayer.died.connect(_on_player_died, CONNECT_REFERENCE_COUNTED)
undefined4. Group Related Signals
4. 对相关信号进行分组
gdscript
undefinedgdscript
undefined✅ Good organization
✅ 推荐的组织方式
Combat signals
战斗相关信号
signal health_changed(current: int, max: int)
signal died()
signal respawned()
signal health_changed(current: int, max: int)
signal died()
signal respawned()
Movement signals
移动相关信号
signal jumped()
signal landed()
signal direction_changed(direction: Vector2)
signal jumped()
signal landed()
signal direction_changed(direction: Vector2)
Inventory signals
背包相关信号
signal item_added(item: Dictionary)
signal item_removed(item: Dictionary)
signal inventory_full()
undefinedsignal item_added(item: Dictionary)
signal item_removed(item: Dictionary)
signal inventory_full()
undefinedTesting Signals
信号测试
gdscript
func test_health_signal() -> void:
var signal_emitted := false
var received_health := 0
player.health_changed.connect(
func(current: int, _max: int):
signal_emitted = true
received_health = current
)
player.health = 50
assert(signal_emitted, "Signal was not emitted")
assert(received_health == 50, "Health value incorrect")gdscript
func test_health_signal() -> void:
var signal_emitted := false
var received_health := 0
player.health_changed.connect(
func(current: int, _max: int):
signal_emitted = true
received_health = current
)
player.health = 50
assert(signal_emitted, "Signal was not emitted")
assert(received_health == 50, "Health value incorrect")Common Gotchas
常见陷阱
Issue: Signal not firing
- Check: Is the signal spelled correctly when connecting?
- Check: Is the emitting code path actually being executed?
- Check: Use before
print()to verifyemit()
Issue: Signal firing multiple times
- Cause: Multiple connections to the same signal
- Solution: Check connections or use
CONNECT_ONE_SHOT
Issue: "Attempt to call function on a null instance"
- Cause: Node was freed but signal still connected
- Solution: Disconnect in or use
_exit_tree()CONNECT_REFERENCE_COUNTED
问题:信号未触发
- 排查:连接时信号名称拼写正确吗?
- 排查:发射信号的代码路径是否真的被执行了?
- 排查:在前添加
emit()来验证print()
问题:信号多次触发
- 原因:同一信号被多次连接
- 解决方案:检查连接情况,或使用
CONNECT_ONE_SHOT
问题:“Attempt to call function on a null instance”
- 原因:节点已被释放但信号仍保持连接
- 解决方案:在中断开连接,或使用
_exit_tree()CONNECT_REFERENCE_COUNTED
Reference
参考资料
Related
相关内容
- Master Skill: godot-master
- 核心技能:godot-master