Godot 信号是什么:signal、connect 和 emit 用法详解

用金币收集示例解释 Godot 信号机制:什么是 signal,如何 connect,什么时候 emit,如何让 Main 接收 Coin 的 collected 信号并更新分数。

Godot 中的“信号”可以理解成节点发出的通知:

某件事情发生了,谁关心这件事,谁就来处理。

例如:

  • 按钮:我被点击了。
  • 金币:我被玩家收集了。
  • 玩家:我的生命值改变了。
  • 敌人:我死亡了。
  • 计时器:时间到了。

其他节点可以连接这些信号,并在信号发出时执行一个函数。发送者不需要直接找到接收者,这能减少节点之间的相互依赖。

信号的三个核心步骤

一个完整的信号流程通常有三步:

  1. 声明信号。
  2. 连接信号。
  3. 发出信号。

可以把它想象成门铃:

概念 类比
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 会自动发出这个信号。

自定义信号

自定义信号由我们自己声明。

例如金币被收集:

1
signal collected

这个 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 进入金币区域,那么:

1
body == Player

第二层:collected

这句代码:

1
signal collected

是在声明信号,表示 Coin 可以发出一个名叫 collected 的通知。

接着:

1
collected.emit()

表示正式发出通知:

这个金币已经被收集了。

但要注意:发出信号,不代表一定有人接收。

如果没有节点连接 collected,它仍然会发出,但不会产生额外效果。金币之所以消失,是因为后面还有:

1
queue_free()

并不是因为 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

第一步,选中场景树里的:

1
Coin

第二步,在右侧面板从“检查器”切换到:

1
节点

英文界面是:

1
Node

第三步,找到自定义信号:

1
collected()

双击它。

第四步,选择接收节点:

1
Main

点击“连接”。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)

运行后,收集金币时会输出:

1
当前分数:1

方法二:用代码连接信号

也可以不通过编辑器,直接在 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()

普通金币可以设置:

1
value = 1

大金币可以设置:

1
value = 5

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

声明一个信号:

1
signal collected

相当于创建一种通知类型。

connect()

指定信号发出后调用哪个函数:

1
collected.connect(_on_collected)

emit()

正式发出信号:

1
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 更新,都可以用信号组织得更清晰。

记录并分享
使用 Hugo 构建
主题 StackJimmy 设计