在 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与信号的结合实际解耦示例掌握信号后你会发现写游戏逻辑时少了很多“找节点”的烦恼。