Godot 4 訊號模式 — 完整指南

1. 簡介 — Godot 4 有哪些改變

Godot 4 徹底翻新了訊號的運作方式。如果你是從 Godot 3 過來的:舊有以字串為基礎的語法 object.connect("signal_name", target, "method_name") 已經不復存在。取而代之的是一套以 Callable 為基礎的 API,它型別安全、對重構友善,而且能在編譯時而非執行時捕捉錯誤。

本指南涵蓋你需要知道的關於 Godot 4.4+ 訊號的一切:宣告、連接、中斷、發出、await,以及最實用的實戰模式。

重點

在 Godot 4 中,訊號是一級物件(first-class objects)。你把它們當作屬性來存取(例如 button.pressed),並在其上呼叫方法(.connect().emit().disconnect())。不再有以字串為基礎的 API。

2. 宣告自訂訊號

在腳本的最上方使用 signal 關鍵字宣告訊號。你可以選擇性地指定參數名稱與型別,用於文件說明與編輯器的自動完成。

player.gd
# Simple signal with no parameters
signal game_over

# Signal with typed parameters
signal health_changed(new_health: int)

# Signal with multiple parameters
signal item_picked_up(item_name: String, quantity: int)

# Signal with no type hints (works, but typed is recommended)
signal something_happened(data)
提示

務必為訊號參數加上型別提示。這能啟用編輯器中的自動完成,並讓你的程式碼自我文件化。帶有型別提示的訊號還會在節點 Dock 的 Signals 分頁中顯示參數資訊。

3. 連接訊號(全新做法)

Godot 4 最大的改變在於你如何連接訊號。不再傳入字串,而是把訊號當作屬性使用,並以一個 Callable(指向某個函式的參照)呼叫 .connect()

基本連接

GDScript
# Godot 3 (OLD — no longer works in Godot 4)
# button.connect("pressed", self, "_on_button_pressed")

# Godot 4 — callable-based connection
button.pressed.connect(_on_button_pressed)

func _on_button_pressed() -> void:
    print("Button was pressed!")

透過 bind() 連接並傳入額外引數

使用 .bind() 隨訊號一併傳入額外資料。當你把多個訊號連接到同一個方法時,這很方便。

GDScript
# Pass extra data via bind()
buy_button.pressed.connect(_on_item_button.bind("sword"))
sell_button.pressed.connect(_on_item_button.bind("shield"))

func _on_item_button(item_id: String) -> void:
    print("Selected item: ", item_id)

Lambda(行內)連接

對於簡短的處理函式,你可以直接使用 lambda 函式。這樣能讓簡單的邏輯緊鄰連接點。

GDScript
# Lambda — great for one-liners
button.pressed.connect(func(): print("Button pressed!"))

# Lambda with parameters
health_changed.connect(func(hp: int): health_label.text = str(hp))

# Multi-line lambda
enemy.died.connect(func():
    score += 100
    score_label.text = "Score: %d" % score
    print("Enemy defeated!")
)
警告

Lambda 連接無法輕易被中斷,因為你沒有指向該匿名函式的參照。如果之後需要中斷連接,請把 Callable 存進變數,或改用具名方法。

在編輯器中連接(節點 Dock)

你仍然可以透過 Godot 編輯器 UI 連接訊號。選取一個節點,開啟 節點 Dock > Signals 分頁,對某個訊號按兩下,然後選擇目標節點與方法。編輯器會在你的腳本中自動產生像 _on_button_pressed() 這樣的方法。這與在程式碼中呼叫 .connect() 完全相同 — 只是替你省去打字。

4. 中斷訊號連接

當你不再想要接收某個訊號時就將它中斷連接。這對於避免節點被釋放或遊戲狀態切換時發生錯誤非常重要。

GDScript
# Disconnect a signal
button.pressed.disconnect(_on_button_pressed)

# Always check before disconnecting to avoid errors
if button.pressed.is_connected(_on_button_pressed):
    button.pressed.disconnect(_on_button_pressed)
最佳實踐

對於跨場景的訊號連接(例如連接到某個 Autoload),務必在 _exit_tree() 中中斷連接,以避免節點被釋放時留下懸置參照:

GDScript
func _ready() -> void:
    EventBus.player_died.connect(_on_player_died)

func _exit_tree() -> void:
    if EventBus.player_died.is_connected(_on_player_died):
        EventBus.player_died.disconnect(_on_player_died)

5. 發出訊號

在 Godot 4 中,使用 .emit() 取代舊有的 emit_signal()。訊號是一個物件,因此你直接在其上呼叫方法。

player.gd
signal health_changed(new_health: int)
signal died

var health: int = 100

func take_damage(amount: int) -> void:
    health -= amount
    health_changed.emit(health)  # Emit with argument

    if health <= 0:
        died.emit()  # Emit with no arguments
注意

舊有的 emit_signal("signal_name") 在 Godot 4 中技術上仍可運作,但已不建議使用。新程式碼請一律優先採用 signal_name.emit()。以字串為基礎的版本可能會在未來的 Godot 版本中被移除。

6. 等待訊號(await)

Godot 4 以 await 取代了 yield()。這讓你能暫停一個函式,直到某個訊號發出為止 — 非常適合過場動畫、教學、連續動畫以及計時事件。

GDScript
func play_cutscene() -> void:
    # Wait for a timer
    await get_tree().create_timer(2.0).timeout

    # Wait for an animation to finish
    animation_player.play("intro")
    await animation_player.animation_finished

    # Wait for player input (custom signal)
    dialogue_label.text = "Press any key to continue..."
    await player_pressed_continue

    # Continue execution after the signal fires
    print("Cutscene complete!")

從被 await 的訊號取得值

如果訊號在發出時帶有引數,await 會將它們回傳。若只有一個引數,你會直接取得該值;若有多個引數,你會取得一個陣列。

GDScript
# Single parameter — returns the value directly
var final_health: int = await health_changed
print("Health is now: ", final_health)

# Multiple parameters — returns an array
var result = await item_picked_up
var item_name: String = result[0]
var quantity: int = result[1]

7. 訊號連接旗標

Godot 提供連接旗標作為 .connect() 的第二個引數,讓你能調整連接的行為。

CONNECT_ONE_SHOT

當訊號發出一次後,連接會自動被移除。非常適合一次性事件,例如死亡動畫或成就解鎖。

GDScript
# Auto-disconnects after firing once
enemy.died.connect(_on_first_kill, CONNECT_ONE_SHOT)

func _on_first_kill() -> void:
    unlock_achievement("first_blood")

CONNECT_DEFERRED

被連接的方法會在目前影格的結尾(閒置時間內)才被呼叫,而非立即呼叫。當訊號處理函式會修改場景樹時很有用,因為在物理或訊號處理期間這麼做並不安全。

GDScript
# Called at end of frame — safe for scene tree changes
button.pressed.connect(_on_restart, CONNECT_DEFERRED)

func _on_restart() -> void:
    get_tree().reload_current_scene()

組合旗標

GDScript
# One-shot AND deferred
trigger.body_entered.connect(_on_trigger, CONNECT_ONE_SHOT | CONNECT_DEFERRED)

8. 常見模式

EventBus(全域訊號中樞)

EventBus 模式使用一個 Autoload 單例來將節點解耦。任何節點都能發出或監聽全域事件,而不需要對另一個節點的直接參照。這是 Godot 中最強大的模式之一。

event_bus.gd (Autoload)
extends Node

# Define all global signals in one place
signal player_died
signal score_changed(new_score: int)
signal level_completed(level_id: int)
signal item_collected(item_name: String)
signal settings_changed
player.gd (emitter)
func die() -> void:
    # Any script can emit global signals
    EventBus.player_died.emit()
hud.gd (listener)
func _ready() -> void:
    # Any script can listen to global signals
    EventBus.score_changed.connect(_on_score_changed)
    EventBus.player_died.connect(_on_player_died)

func _on_score_changed(new_score: int) -> void:
    score_label.text = "Score: %d" % new_score

func _on_player_died() -> void:
    game_over_screen.show()
設定 Autoload

前往 專案 > 專案設定 > Autoload,加入你的 event_bus.gd 腳本,並將它命名為 EventBus。它會自動成為一個可全域存取的單例。

訊號轉遞(父子節點通訊)

父節點監聽其子節點的訊號,並轉遞或彙整資訊。子節點彼此之間永遠不需要知道對方的存在。

inventory.gd
signal inventory_updated

func _ready() -> void:
    # Connect to all slot children
    for slot in get_children():
        if slot.has_signal("item_changed"):
            slot.item_changed.connect(_on_slot_changed)

func _on_slot_changed() -> void:
    inventory_updated.emit()

Signal + Await 打造順序式遊戲流程

level_manager.gd
func run_level() -> void:
    spawn_enemies()
    await EventBus.all_enemies_defeated

    show_treasure_chest()
    await EventBus.chest_opened

    play_exit_animation()
    await get_tree().create_timer(1.5).timeout

    load_next_level()

為生成的節點做動態連接

enemy_spawner.gd
func spawn_enemy(pos: Vector2) -> void:
    var enemy = enemy_scene.instantiate()
    enemy.position = pos

    # Connect signals before adding to scene tree
    enemy.died.connect(_on_enemy_died.bind(enemy))
    enemy.health_changed.connect(_on_enemy_health_changed)

    add_child(enemy)

func _on_enemy_died(enemy: Node) -> void:
    enemies_alive -= 1
    enemy.queue_free()

9. 遷移速查表(Godot 3 → 4)

把這張表加入書籤,在移植你的 Godot 3 專案時可作為快速參考。

操作 Godot 3 Godot 4
連接 connect("sig", obj, "method") sig.connect(method)
中斷連接 disconnect("sig", obj, "method") sig.disconnect(method)
發出 emit_signal("sig", args) sig.emit(args)
檢查連接 is_connected("sig", obj, "method") sig.is_connected(method)
等待訊號 yield(obj, "sig") await obj.sig
綁定額外引數 connect("sig", obj, "method", [data]) sig.connect(method.bind(data))
一次性 connect("sig", obj, "method", [], CONNECT_ONESHOT) sig.connect(method, CONNECT_ONE_SHOT)

10. 常見錯誤

1. 使用以字串為基礎的 connect(Godot 3 風格)

GDScript
# WRONG — Godot 3 syntax, will not compile
button.connect("pressed", self, "_on_button_pressed")

# CORRECT — Godot 4 syntax
button.pressed.connect(_on_button_pressed)

2. 使用 emit_signal() 而非 .emit()

雖然 emit_signal() 仍可運作,但它會繞過編譯時檢查,且已正式不建議使用。請改用 .emit()

GDScript
# AVOID — deprecated, no compile-time checks
emit_signal("health_changed", health)

# PREFER — type-safe, catches typos at compile time
health_changed.emit(health)

3. 連接到已被釋放的節點

如果你把一個訊號連接到某個節點上的方法,而該節點被 queue_free() 釋放了,下一次訊號發出時就會當機。解決方式:

  • _exit_tree() 中中斷連接
  • 對一次性事件使用 CONNECT_ONE_SHOT
  • 在發出前以 is_instance_valid(target) 檢查
GDScript
# Safe emission pattern
for connection in my_signal.get_connections():
    if is_instance_valid(connection["callable"].get_object()):
        pass  # Connection is still valid
my_signal.emit()  # Godot handles invalid connections gracefully in 4.x

4. 忘記中斷跨場景訊號的連接

連接到 Autoload 訊號的連線會在場景切換後仍然存在。如果你在 _ready() 中連接卻從不中斷,當場景重新載入時就會出現錯誤或非預期的行為。

GDScript
# WRONG — never disconnects, leaks across scene changes
func _ready() -> void:
    EventBus.score_changed.connect(_on_score_changed)

# CORRECT — clean disconnect
func _ready() -> void:
    EventBus.score_changed.connect(_on_score_changed)

func _exit_tree() -> void:
    EventBus.score_changed.disconnect(_on_score_changed)

5. 把同一個訊號連接兩次

如果 _ready() 被呼叫多次(例如重新設定節點的父節點),你可能會不小心把同一個方法連接兩次。這樣一來處理函式每次發出都會觸發兩次。

GDScript
# Guard against double-connection
func _ready() -> void:
    if not EventBus.score_changed.is_connected(_on_score_changed):
        EventBus.score_changed.connect(_on_score_changed)

想讓 AI 管理你的訊號架構嗎?

Godot MCP Pro 將 Claude 等 AI 助手直接連接到你的 Godot 編輯器。它能在你的整個專案中自動連接、中斷、稽核並視覺化訊號流。

analyze_signal_flow find_signal_connections connect_signal disconnect_signal get_signals
取得 Godot MCP Pro — $15