Godot信号架构Skill godot-signal-architecture

这个技能是关于在Godot游戏引擎中使用信号驱动架构,实现松耦合的事件系统。它提供了定义类型化信号、管理信号连接、使用AutoLoad事件总线进行跨场景通信的最佳实践模式。适用于游戏开发中的事件处理、UI交互、节点解耦等场景,提升代码可维护性和扩展性。关键词:Godot信号、事件总线、松耦合架构、游戏开发、事件系统、信号模式、类型化信号、信号链。

游戏开发 0 次安装 0 次浏览 更新于 3/23/2026

name: godot-signal-architecture description: “使用"信号向上,调用向下"模式的信号驱动架构专家蓝图,实现松耦合。涵盖类型化信号、信号链、一次性连接和AutoLoad事件总线。适用于实现事件系统或解耦节点。关键词:信号、发射、连接、CONNECT_ONE_SHOT、CONNECT_REFERENCE_COUNTED、事件总线、AutoLoad、解耦。”

信号架构

信号向上/调用向下模式、类型化信号和事件总线定义了松耦合、可维护的架构。

可用脚本

global_event_bus.gd

专家级AutoLoad事件总线,具有类型化信号和连接管理。

signal_debugger.gd

运行时信号连接分析器。显示场景层次结构中的所有连接。

signal_spy.gd

测试实用工具,用于观察信号发射,带有计数跟踪和历史记录。

强制要求 - 对于事件总线:在实现跨场景通信之前,阅读global_event_bus.gd。

信号架构中绝不做的事

  • 绝不创建循环信号依赖 — 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进行跨场景通信

使用直接调用用于:

  • 父节点控制子节点行为
  • 访问子节点属性
  • 简单的本地交互

实现模式

模式1:定义类型化信号

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

模式2:在状态变化时发射信号

# 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,发射信号

模式3:在父节点中连接信号

# 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

模式4:通过AutoLoad的全局信号

用于跨场景通信:

# 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)

高级模式

模式5:信号链

# 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()

模式6:一次性连接

用于单次使用的信号连接:

# 使用CONNECT_ONE_SHOT标志连接
timer.timeout.connect(_on_timer_timeout, CONNECT_ONE_SHOT)

func _on_timer_timeout() -> void:
    print(\"这只会触发一次\")
    # 连接自动移除

模式7:自定义信号参数

# 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
    )

最佳实践

1. 描述性信号名称

# ✅ 好
signal button_pressed()
signal enemy_defeated(enemy_type: String)
signal animation_finished(animation_name: String)

# ❌ 坏
signal pressed()
signal done()
signal finished()

2. 避免循环依赖

# ❌ 坏: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)

3. 当节点释放时断开信号

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)

4. 分组相关信号

# ✅ 好的组织
# 战斗信号
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, \"信号未发射\")
    assert(received_health == 50, \"健康值不正确\")

常见陷阱

问题:信号未触发

  • 检查:连接时信号拼写是否正确?
  • 检查:发射代码路径是否实际执行?
  • 检查:在emit()前使用print()验证

问题:信号触发多次

  • 原因:多个连接到同一信号
  • 解决方案:检查连接或使用CONNECT_ONE_SHOT

问题:“尝试在空实例上调用函数”

  • 原因:节点已释放但信号仍连接
  • 解决方案:在_exit_tree()中断开或使用CONNECT_REFERENCE_COUNTED

参考

相关