godot-signal-architecture by thedivergentai/gd-agentic-skills
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-signal-architecture信号向上传递/调用向下模式、类型化信号和事件总线定义了松耦合、可维护的架构。
实现松耦合的层级通信:子节点信号向上传递,父节点调用向下。
用于系统级事件路由(成就、UI、保存)的专家级 AutoLoad 事件总线。
使用 Callable.bind() 向信号回调中注入额外的静态上下文。
使用 unbind() 丢弃不需要的信号参数,以清理函数签名。
使用线性、可读的 await 信号序列替代混乱的计时器和状态标志。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
使用 is_connected() 验证连接状态,以防止运行时多重连接错误。
切换跟踪目标时断开信号连接的关键内存管理模式。
使用 CONNECT_ONE_SHOT 和 CONNECT_DEFERRED 实现自清理和物理安全的回调。
使用 CONNECT_APPEND_SOURCE_OBJECT 识别哪个节点触发了共享信号。
使用顺序信号等待来管理多步骤异步加载和过渡。
Object.connect() — 拼写错误会导致静默失败。始终使用 signal.connect(_callback) 以获得编译时验证 [1]。CONNECT_REFERENCE_COUNTED,否则这会在运行时抛出 ERR_INVALID_PARAMETER 错误 [3]。unbind() 丢弃不需要的参数,保持 API 的整洁 [5]。signal moved 缺乏编辑器支持。始终使用 signal moved(dir: Vector2) [27]。_exit_tree() 中断开连接或使用 bind_node() [28]。died.emit() 调用了 queue_free(),监听器可能无法响应。先发射信号 [31]。使用信号处理:
使用直接调用处理:
extends CharacterBody2D
# ✅ 良好 - 类型化信号 (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
signal died
# 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 # 触发 setter,从而发射信号
# game.gd (父节点)
extends 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
用于跨场景通信:
# events.gd (AutoLoad)
extends Node
signal level_completed(level_number: int)
signal player_spawned(player: Node2D)
signal boss_defeated(boss_name: String)
# 任何脚本都可以发射:
Events.level_completed.emit(3)
# 任何脚本都可以监听:
Events.level_completed.connect(_on_level_completed)
# enemy.gd
signal died(score_value: int)
func _on_health_depleted() -> void:
died.emit(100)
queue_free()
# 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()
用于单次使用的信号连接:
# 使用 CONNECT_ONE_SHOT 标志连接
timer.timeout.connect(_on_timer_timeout, CONNECT_ONE_SHOT)
func _on_timer_timeout() -> void:
print("This only fires once")
# 连接自动移除
# 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
})
# inventory.gd
func _on_item_picked_up(item_data: Dictionary) -> void:
add_item(
item_data.name,
item_data.type,
item_data.value
)
# ✅ 良好
signal button_pressed()
signal enemy_defeated(enemy_type: String)
signal animation_finished(animation_name: String)
# ❌ 不佳
signal pressed()
signal done()
signal finished()
# ❌ 不佳:A 向 B 发信号,B 又向 A 发信号
# A.gd
signal data_requested
func _ready():
B.data_ready.connect(_on_data_ready)
data_requested.emit()
# B.gd
signal data_ready
func _ready():
A.data_requested.connect(_on_data_requested)
# ✅ 良好:使用中介者(父节点或 AutoLoad)
# Parent.gd
func _ready():
A.data_requested.connect(_on_A_data_requested)
B.data_ready.connect(_on_B_data_ready)
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)
或使用自动清理:
# 当此节点释放时,信号自动断开连接
player.died.connect(_on_player_died, CONNECT_REFERENCE_COUNTED)
# ✅ 良好的组织方式
# 战斗信号
signal health_changed(current: int, max: int)
signal died()
signal respawned()
# 移动信号
signal jumped()
signal landed()
signal direction_changed(direction: Vector2)
# 库存信号
signal item_added(item: Dictionary)
signal item_removed(item: Dictionary)
signal inventory_full()
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")
问题:信号未触发
emit() 前使用 print() 来验证问题:信号多次触发
CONNECT_ONE_SHOT问题:“尝试在空实例上调用函数”
_exit_tree() 中断开连接或使用 CONNECT_REFERENCE_COUNTED每周安装数
76
仓库
GitHub 星标数
59
首次出现
2026年2月10日
安全审计
安装于
gemini-cli74
codex73
opencode72
kimi-cli70
github-copilot70
amp70
Signal Up/Call Down pattern, typed signals, and event buses define decoupled, maintainable architectures.
Clean implementation of decoupled hierarchy communication: children signal up, parents call down.
Expert AutoLoad event bus for system-level event routing (Achievements, UI, Saving).
Injecting extra static context into signal callbacks using Callable.bind().
Cleaning up function signatures by discarding unneeded signal arguments with unbind().
Replacing messy timers and state flags with linear, readable await signal sequences.
Verifying connection state using is_connected() to prevent runtime multi-connection errors.
Crucial memory management pattern for disconnecting signals when switching tracking targets.
Using CONNECT_ONE_SHOT and CONNECT_DEFERRED for self-cleaning and physics-safe callbacks.
Identifying which node fired a shared signal using CONNECT_APPEND_SOURCE_OBJECT.
Managing multi-step asynchronous loading and transitions using sequential signal awaits.
Object.connect() — Typos result in silent failures. Always use signal.connect(_callback) for compile-time validation [1].ERR_INVALID_PARAMETER at runtime unless using CONNECT_REFERENCE_COUNTED [3].unbind() to drop unwanted parameters and keep your API clean [5].signal moved without types lacks editor support. Always use [27].Use Signals For:
Use Direct Calls For:
extends CharacterBody2D
# ✅ Good - typed signals (Godot 4.x)
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
# 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
# game.gd (parent)
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
For cross-scene communication:
# events.gd (AutoLoad)
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)
# Any script can listen:
Events.level_completed.connect(_on_level_completed)
# enemy.gd
signal died(score_value: int)
func _on_health_depleted() -> void:
died.emit(100)
queue_free()
# 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()
For single-use signal connections:
# Connect with CONNECT_ONE_SHOT flag
timer.timeout.connect(_on_timer_timeout, CONNECT_ONE_SHOT)
func _on_timer_timeout() -> void:
print("This only fires once")
# Connection automatically removed
# 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
})
# inventory.gd
func _on_item_picked_up(item_data: Dictionary) -> void:
add_item(
item_data.name,
item_data.type,
item_data.value
)
# ✅ Good
signal button_pressed()
signal enemy_defeated(enemy_type: String)
signal animation_finished(animation_name: String)
# ❌ Bad
signal pressed()
signal done()
signal finished()
# ❌ BAD: A signals to B, B signals back to A
# A.gd
signal data_requested
func _ready():
B.data_ready.connect(_on_data_ready)
data_requested.emit()
# B.gd
signal data_ready
func _ready():
A.data_requested.connect(_on_data_requested)
# ✅ GOOD: Use a mediator (parent or AutoLoad)
# Parent.gd
func _ready():
A.data_requested.connect(_on_A_data_requested)
B.data_ready.connect(_on_B_data_ready)
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:
# Signal auto-disconnects when this node is freed
player.died.connect(_on_player_died, CONNECT_REFERENCE_COUNTED)
# ✅ Good organization
# Combat signals
signal health_changed(current: int, max: int)
signal died()
signal respawned()
# Movement signals
signal jumped()
signal landed()
signal direction_changed(direction: Vector2)
# Inventory signals
signal item_added(item: Dictionary)
signal item_removed(item: Dictionary)
signal inventory_full()
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")
Issue : Signal not firing
print() before emit() to verifyIssue : Signal firing multiple times
CONNECT_ONE_SHOTIssue : "Attempt to call function on a null instance"
_exit_tree() or use CONNECT_REFERENCE_COUNTEDWeekly Installs
76
Repository
GitHub Stars
59
First Seen
Feb 10, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
gemini-cli74
codex73
opencode72
kimi-cli70
github-copilot70
amp70
Android 整洁架构指南:模块化设计、依赖注入与数据层实现
1,300 周安装
signal moved(dir: Vector2)_exit_tree() or use bind_node() [28].died.emit() calls queue_free(), listeners might fail to respond. Emit first [31].