Godot 中的“信号”可以理解成节点发出的通知:
某件事情发生了,谁关心这件事,谁就来处理。
例如:
- 按钮:我被点击了。
- 金币:我被玩家收集了。
- 玩家:我的生命值改变了。
- 敌人:我死亡了。
- 计时器:时间到了。
其他节点可以连接这些信号,并在信号发出时执行一个函数。发送者不需要直接找到接收者,这能减少节点之间的相互依赖。
信号的三个核心步骤
一个完整的信号流程通常有三步:
- 声明信号。
- 连接信号。
- 发出信号。
可以把它想象成门铃:
| 概念 |
类比 |
signal |
安装门铃 |
connect |
把门铃接到屋内响铃器 |
emit |
按下门铃 |
| 接收函数 |
屋里的人听到后开门 |
所以最重要的记忆方式是:
1
2
3
|
signal = 定义通知
connect = 安排接收者
emit = 发出通知
|
Godot 有两类信号
Godot 中常见的信号可以分为两类:内置信号和自定义信号。
内置信号
内置信号是 Godot 节点本身已经提供的信号。
常见例子:
Button.pressed
Timer.timeout
Area2D.body_entered
Area2D.body_exited
如果金币节点继承自 Area2D,那么它天然就有 body_entered 信号。当 CharacterBody2D 进入金币的碰撞区域时,Godot 会自动发出这个信号。
自定义信号
自定义信号由我们自己声明。
例如金币被收集:
这个 collected 不是 Godot 自带的,而是我们自己定义的通知类型。
金币示例中的两层信号
一个简单的金币脚本可以这样写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
extends Area2D
signal collected
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
if body.name != "Player":
return
collected.emit()
queue_free()
|
这里实际使用了两层信号。
第一层:body_entered
这句代码:
1
|
body_entered.connect(_on_body_entered)
|
意思是:
当有物理对象进入 Coin 的 Area2D 时,调用 _on_body_entered()。
流程如下:
1
2
3
4
5
|
Player 进入金币碰撞区域
↓
Area2D 发出 body_entered
↓
执行 _on_body_entered(body)
|
传入的 body 就是进入金币区域的节点。如果 Player 进入金币区域,那么:
第二层:collected
这句代码:
是在声明信号,表示 Coin 可以发出一个名叫 collected 的通知。
接着:
表示正式发出通知:
这个金币已经被收集了。
但要注意:发出信号,不代表一定有人接收。
如果没有节点连接 collected,它仍然会发出,但不会产生额外效果。金币之所以消失,是因为后面还有:
并不是因为 collected.emit() 会自动让金币消失。
完整通信流程
金币被玩家碰到以后,完整流程是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
Player 进入 Coin
↓
Coin 的 Area2D 发出 body_entered
↓
Coin 执行 _on_body_entered()
↓
确认进入者是 Player
↓
Coin 发出 collected
↓
Main 接收 collected
↓
Main 增加分数
↓
Coin 执行 queue_free()
↓
金币消失
|
这里有两个发送者:
1
2
|
Area2D → 发出 body_entered
Coin → 发出 collected
|
body_entered 负责告诉 Coin:“有东西进入了你的区域。”
collected 负责告诉 Main:“这个金币被收集了。”
怎样让 Main 接收金币信号
假设场景结构如下:
1
2
3
4
5
|
Main Node2D
├─ Player
├─ Coin
└─ CanvasLayer
└─ ScoreLabel
|
可以用编辑器连接,也可以用代码连接。
方法一:在 Godot 编辑器里连接
打开 main.tscn。
第一步,选中场景树里的:
第二步,在右侧面板从“检查器”切换到:
英文界面是:
第三步,找到自定义信号:
双击它。
第四步,选择接收节点:
点击“连接”。Godot 会在 Main 的脚本中生成类似函数:
1
2
|
func _on_coin_collected() -> void:
pass
|
把它改成:
1
2
3
4
5
6
|
var score: int = 0
func _on_coin_collected() -> void:
score += 1
print("当前分数:", score)
|
运行后,收集金币时会输出:
方法二:用代码连接信号
也可以不通过编辑器,直接在 main.gd 中连接:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
extends Node2D
var score: int = 0
@onready var coin: Area2D = $Coin
func _ready() -> void:
coin.collected.connect(_on_coin_collected)
func _on_coin_collected() -> void:
score += 1
print("当前分数:", score)
|
核心代码是:
1
|
coin.collected.connect(_on_coin_collected)
|
它表示:
1
2
3
|
发送者:coin
信号:collected
接收函数:_on_coin_collected
|
Godot 4 推荐直接通过 Signal 对象调用 connect(),例如:
1
|
coin.collected.connect(_on_coin_collected)
|
不要照搬一些 Godot 3 老教程里的旧写法。
让信号携带数据
信号不只能通知“事情发生了”,还可以携带数据。
例如不同金币分值不同:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
extends Area2D
signal collected(value: int)
@export var value: int = 1
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
if body.name != "Player":
return
collected.emit(value)
queue_free()
|
普通金币可以设置:
大金币可以设置:
Main 接收时也要增加参数:
1
2
3
4
5
6
7
|
var score: int = 0
func _on_coin_collected(value: int) -> void:
score += value
print("获得:", value)
print("总分:", score)
|
通信过程:
1
2
3
4
5
|
Coin:我被收集了,价值是 5
↓
Main:收到 value = 5
↓
score += 5
|
为什么不让 Coin 直接修改 UI
不推荐这样写:
1
2
3
|
func _on_body_entered(body: Node2D) -> void:
get_node("../CanvasLayer/ScoreLabel").text = "1"
queue_free()
|
因为这样会让 Coin 依赖 Main 的具体节点结构:
- Coin 必须知道
ScoreLabel 在哪里。
- Coin 必须知道分数如何保存。
- Coin 必须知道 UI 怎样更新。
只要你移动 UI 节点,Coin 的路径就可能失效。
更好的结构是:
1
2
3
4
5
6
7
8
|
Coin 只负责:
“我被收集了。”
Main 负责:
“增加分数。”
UI 负责:
“显示分数。”
|
也就是:
1
2
3
4
5
6
7
|
Coin
│ collected(value)
▼
Main
│ 更新 score
▼
ScoreLabel
|
这正是信号降低节点耦合的作用。
最容易混淆的三个概念
signal
声明一个信号:
相当于创建一种通知类型。
connect()
指定信号发出后调用哪个函数:
1
|
collected.connect(_on_collected)
|
emit()
正式发出信号:
记忆方式:
1
2
3
|
signal = 定义通知
connect = 安排接收者
emit = 发出通知
|
推荐的金币代码
coin.gd:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
extends Area2D
signal collected(value: int)
@export var value: int = 1
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
if body.name != "Player":
return
collected.emit(value)
queue_free()
|
main.gd:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
extends Node2D
var score: int = 0
@onready var score_label: Label = $CanvasLayer/ScoreLabel
@onready var coin: Area2D = $Coin
func _ready() -> void:
coin.collected.connect(_on_coin_collected)
update_score_label()
func _on_coin_collected(value: int) -> void:
score += value
update_score_label()
func update_score_label() -> void:
score_label.text = "分数:%d" % score
|
最核心的一句话是:
信号负责通知发生了什么,不负责决定所有节点接下来怎么做。
理解这一点后,Godot 里的按钮、金币、敌人、计时器、生命值、UI 更新,都可以用信号组织得更清晰。