信号是 Godot 的"事件总线",用好它可以让 UI 与玩法解耦、模块复用。本文讲清概念、图形化连接 vs 代码连接、全局信号封装,并附常见坑与最佳实践(精力有限,附件后面慢慢补)。
信号是什么
- 类似 C# 事件/UnityEvent:发送者发射,接收者监听,双方无需相互引用具体类型。
- 任意节点都可以定义/发射信号,参数自定义。
定义与发射
gdscript
signal hit(damage: int, from)
func _on_body_entered(body):
emit_signal("hit", 10, body)
- 信号定义放在脚本顶部,
emit_signal传参顺序与定义一致。
两种连接方式
图形化(适合固定场景)
- 选中信号源节点 → Node 面板 → Signals → 双击信号。
- 选择接收节点(通常场景根),确认后 Godot 生成回调函数并连接。
- 优点:快、可视化;缺点:动态实例化时容易忘记保存连接。
代码连接(推荐通用写法)
gdscript
func _ready():
$Button.pressed.connect(_on_btn_pressed)
func _on_btn_pressed():
print("clicked")
- 动态实例:
gdscript
var enemy = EnemyScene.instantiate()
enemy.hit.connect(_on_enemy_hit)
add_child(enemy)
- 优点:连接逻辑集中、可条件化,便于重构。
全局信号总线(解耦模块)
- 在 Autoload 添加
SignalsBus.gd:
gdscript
extends Node
signal player_died
signal coin_collected(amount)
- 发射:
SignalsBus.emit_signal("coin_collected", 1) - 监听:
SignalsBus.coin_collected.connect(_on_coin) - 用途:UI/玩法互不引用,依靠总线通信;多人项目可统一管理事件名。
典型场景示例
UI 按钮 → 游戏逻辑
gdscript
# Menu.gd
func _ready():
$PlayButton.pressed.connect(_on_play)
func _on_play():
SignalsBus.emit_signal("start_game")
gdscript
# GameController.gd
func _ready():
SignalsBus.start_game.connect(_start)
区域触发器 → 计分
gdscript
# Coin.gd
signal collected(value)
func _on_body_entered(body):
if body.is_in_group("player"):
emit_signal("collected", 1)
queue_free()
gdscript
# Level.gd
func _ready():
for coin in $Coins.get_children():
coin.collected.connect(_on_coin)
信号管理的最佳实践
- 命名 :用动词过去式或事件名:
hit,died,collected,finished。 - 参数少而准:只传接收方需要的数据,避免传整个节点引用(或传引用但注明只读)。
- 连接位置统一 :集中在
_ready或初始化函数,便于搜索和断开。 - 断开连接 :长生命周期节点监听短生命周期对象时,记得在对象销毁前
disconnect或在_exit_tree断开。 - 避免多次连接 :先检查
is_connected或确保连接代码只执行一次。
常见坑
- 未保存连接:编辑器连完信号没保存场景,运行时无回调;改用脚本连接或养成保存习惯。
- 重复回调 :同一信号多次连接同一函数导致执行多次,排查
connect是否在循环内。 - 跨场景引用 null :监听的节点被销毁,回调报 null。使用
weakref或在disconnect后再 free。 - Autoload 信号未注册:忘记在项目设置里勾选 Autoload,导致发射无监听。
调试技巧
- 临时打印:在回调里
print("signal hit", value)确认触发。 is_connected检查:
gdscript
if not source.hit.is_connected(_on_hit):
source.hit.connect(_on_hit)
- 编辑器 Signal 面板有"Go to Function"可快速跳到生成的回调。
进阶:一次性/延迟信号
- 一次性:
gdscript
var conn = button.pressed.connect(func():
print("once")
button.pressed.disconnect(conn)
)
- 延迟发射:
call_deferred("emit_signal", "hit", 10)避免当前帧状态冲突。
总结复盘
- 信号=解耦:源的一端发射、接收一端监听,互不依赖具体实现。
- 固定场景可用图形化连接,动态/可复用模块优先代码连接;全局事件用 Autoload 总线。
- 关注连接时机、重复连接与断开,避免"回调触发两次"和 null 报错。掌握这些,UI/玩法/系统间通信会更清晰。
