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 更新,都可以用信號組織得更清楚。