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 設計