在 Godot 里写游戏逻辑,迟早会遇到一个情况:某个事件发生了,其他节点需要知道并做出反应。比如玩家按了按钮,门开了;敌人死了,分数增加了;计时器到时间了,角色能行动了。这些“发生了什么”的通信,Godot 提供了一套专门的机制,叫信号(signal)。信号本质上就是事件的广播系统,某个节点发出一个信号,其他关心这个信号的节点可以提前连接并收到通知。这篇笔记就聊聊信号怎么用,怎么自定义信号,以及它在实际项目里的好处。
一、为什么需要信号
假设你有一个玩家节点,当玩家死亡时,你要做很多事情:播放死亡动画、更新 UI、扣除生命数、显示游戏结束画面。如果不用信号,你可能会在玩家脚本里直接去调用其他节点的函数,比如:
func die(): $AnimationPlayer.play("die") $UI.update_score() $GameManager.player_died()这样写,玩家脚本就不得不知道动画、UI、游戏管理器这些节点的存在,耦合很紧。一旦这些节点改了名字或结构,玩家脚本也得跟着改。
信号解决了这个问题。玩家只需要在死亡时发射一个信号,比如died,而关心这个信号的其他节点各自在自己的脚本里连接这个信号并处理,玩家根本不需要知道它们是谁。
这种模式让代码更容易维护和扩展,也是 Godot 官方推荐的做法。
二、信号的基本概念
信号是 Godot 对象(所有继承自Object的类,包括节点)的一种特殊属性。你可以把信号理解成一个“事件名称”,当事件发生时,用emit_signal()把它发射出去,所有连接到这个信号的函数都会被调用。
- 发射信号:由某个对象发起,通知“某件事发生了”。
- 连接信号:其他对象监听这个信号,当信号发射时,它们注册的函数就会被执行。
一个信号可以连接多个函数,同一个函数也可以连接到多个信号。
三、使用内置信号
Godot 的很多节点自带信号,比如按钮的pressed、计时器的timeout、碰撞体的body_entered。这些信号我们不需要定义,直接连接就能用。
代码连接
最常见的方式是在_ready()里用connect()方法连接信号。
extends Node2D @onready var button = $Button func _ready(): button.pressed.connect(_on_button_pressed) func _on_button_pressed(): print("按钮被按下了")button.pressed是按钮节点的信号。.connect(_on_button_pressed)把这个信号连接到_on_button_pressed函数。- 当按钮被按下,
pressed信号发射,_on_button_pressed就会被调用。
连接的方法名没有强制要求,但通常以_on_开头,表示这是一个信号回调,方便阅读。
编辑器连接
在 Godot 编辑器里,你也可以通过“节点”面板的“信号”页签来连接信号。选中节点,点击信号旁边的“连接”,然后选择目标节点和方法,编辑器会自动生成连接代码。这样更直观,但代码连接更灵活、可读性更好,尤其在动态创建节点时。
四、自定义信号
除了内置信号,你也可以在脚本中定义自己的信号。语法是用signal关键字,写在成员变量区域。
extends Node2D signal health_changed(new_health) signal died定义信号时可以带参数,表示发射信号时会传递的数据。health_changed带一个参数new_health,而died不带参数。
发射自定义信号
用emit_signal()发射信号,参数按定义时的顺序传递。
func take_damage(amount): health -= amount emit_signal("health_changed", health) if health <= 0: emit_signal("died")- 第一个参数是信号名字符串,后面是信号携带的参数。
- 注意信号名字不要写错,否则会报错。为了安全,也可以用
emit_signal("health_changed", health),但 GDScript 4 支持直接用信号名:health_changed.emit(health),更简洁,也能避免拼写错误。
使用信号名直接发射(推荐)
GDScript 4 中,signal定义后,可以像调用方法一样使用.emit()发射,不需要用字符串:
health_changed.emit(health) died.emit()这样更安全,编辑器也会检查。
五、连接自定义信号
连接自定义信号和连接内置信号一样,用connect()。
# 在另一个节点脚本中 @onready var player = $Player func _ready(): player.health_changed.connect(_on_player_health_changed) player.died.connect(_on_player_died) func _on_player_health_changed(new_health): print("玩家血量变化:", new_health) func _on_player_died(): print("玩家死了")连接时,回调函数的参数需要和信号定义的参数匹配。如果信号带一个参数,回调函数也要有一个参数。
六、断开信号
如果不再需要接收信号,可以用disconnect()断开连接。
player.health_changed.disconnect(_on_player_health_changed)通常在节点销毁前,如果连接的信号来自其他对象,最好手动断开,但 Godot 在节点释放时会自动断开相关连接(如果是对象间的连接,当接收者被释放时自动断开)。不过如果你手动管理连接,还是显式断开更清晰。
七、信号连接的几种写法
1. 使用connect()方法
这是最标准的方式,上面已经演示。
2. 使用 lambda 表达式
GDScript 4 支持用 lambda 作为回调,适合简单逻辑,不用单独定义函数。
button.pressed.connect(func(): print("按钮被按下") )如果信号带参数,lambda 也可以接收:
player.health_changed.connect(func(new_health): print("血量:", new_health) )3. 使用Callable
可以把函数引用存成变量,再连接。
var callback = Callable(self, "_on_button_pressed") button.pressed.connect(callback)不过最常用的还是直接方法名。
八、信号参数的传递
信号可以传递任意数量和类型的参数。定义时写参数名,发射时按顺序传入,连接时回调函数按顺序接收。
signal player_scored(player_name, points) func _ready(): player_scored.emit("Tom", 10) func _on_player_scored(player_name, points): print(player_name, " 获得 ", points, " 分")参数个数必须匹配,否则连接时会出错或运行时报错。
九、await 与信号
GDScript 的await关键字可以暂停函数执行,直到某个信号发射。这常用于协程式逻辑,比如等待按钮点击再继续。
func wait_for_button(): await button.pressed print("按钮终于被按了")这行代码会让函数暂停在await处,直到button.pressed信号发射,然后继续执行后面的代码。await还可以获取信号参数(如果信号带参数):
func wait_for_health_change(): var new_health = await player.health_changed print("血量变化为:", new_health)注意:await会挂起整个函数的执行,但不会阻塞主线程,其他节点的逻辑照常运行。它是实现异步逻辑的好工具。
十、信号与代码解耦实例
假设我们有一个游戏管理器,需要知道玩家死亡和得分事件。不用信号时,管理器要引用玩家节点。用信号后,玩家只负责发信号,管理器连接信号即可。
玩家脚本:
class_name Player extends CharacterBody2D signal died signal score_changed(new_score) var score = 0 func add_score(amount): score += amount score_changed.emit(score) func die(): died.emit() queue_free()游戏管理器脚本:
extends Node @onready var player = $Player func _ready(): player.died.connect(_on_player_died) player.score_changed.connect(_on_score_changed) func _on_player_died(): print("游戏结束") func _on_score_changed(new_score): print("当前分数:", new_score)这样,玩家脚本完全不知道管理器的存在,管理器只依赖玩家的信号。当玩家死亡,管理器自动收到通知,耦合度大大降低。
十一、信号使用中的注意事项
- 发射信号时,必须确保有连接吗?不需要。如果没人连接,发射信号不会做任何事,也不会报错。
- 信号发射是同步的:所有连接的回调会立即执行,执行完毕后
emit_signal()才返回。所以不要在回调里做可能无限递归的事情。 - 连接要避免重复:如果同一个信号连接了同一个函数两次,信号发射时函数会被调用两次。需要小心,尤其是在动态连接时,可能无意识重复连接。可以在连接前先检查
is_connected()。 - 回调函数不要依赖顺序:多个回调的执行顺序不保证,不要依赖。
- 信号名命名:习惯用过去式或事件名,比如
died、health_changed、item_picked_up。
十二、总结
信号是 Godot 中最重要的通信机制,它让节点之间解耦,代码更清晰。这篇笔记涵盖了:
- 信号的概念和为什么使用信号
- 使用内置信号
- 自定义信号及发射
- 连接和断开信号
- 信号参数的传递
await与信号的结合- 实际解耦示例
掌握信号后,你会发现写游戏逻辑时少了很多“找节点”的烦恼。