Godot 4 中的信號
在 Godot 4 中,信號是第一級 Signal 值,而方法參考是 Callable。請全面使用物件語法(button.pressed.connect(_on_pressed))。這樣解析器會在編譯時拒絕拼錯的處理器、以及 self 上拼錯的信號;如果是其他型別節點上拼錯的信號,則在 unsafe_property_access 設定為錯誤時(參見 godot-4-api-pitfalls 技能)會被捕捉。字串形式(connect("pressed", ...)、emit_signal("died"))僅在執行階段檢查。
宣告、發射、連接
extends Node
signal health_changed(old_value: int, new_value: int)
signal died
var health := 100
func take_damage(amount: int) -> void:
var old := health
health = maxi(health - amount, 0)
health_changed.emit(old, health)
if health == 0:
died.emit()
extends Control
@onready var start_button: Button = %StartButton
@onready var quit_button: Button = %QuitButton
func _ready() -> void:
start_button.pressed.connect(_on_start_pressed)
quit_button.pressed.connect(get_tree().quit)
start_button.mouse_entered.connect(func() -> void: start_button.grab_focus())
func _on_start_pressed() -> void:
get_tree().change_scene_to_file("res://level_1.tscn")
處理器必須接受信號發射的確切參數數量。參數不匹配不是解析錯誤;在發射時,Godot 會記錄 Error calling from signal 'x' to callable: '...': Method expected 0 argument(s), but called with 1. 並跳過該處理器。
bind() 與 unbind()
bind(a, b)在信號自身參數之後附加額外參數。對於綁定了一個值的pressed處理器,它會接受一個參數;對於綁定了一個值的health_changed(old, new)處理器,它則接受三個參數,順序為old, new, bound。unbind(n)會丟棄信號的最後n個參數,因此一個無參數的函式可以處理發送某些參數的信號。
extends HBoxContainer
signal slot_chosen(index: int)
var slot_buttons: Array[Button] = []
func _ready() -> void:
for i in 4:
var button := Button.new()
button.text = "Slot %d" % (i + 1)
add_child(button)
slot_buttons.append(button)
button.pressed.connect(_on_slot_pressed.bind(i))
var timer := Timer.new()
add_child(timer)
timer.timeout.connect(_refresh)
slot_chosen.connect(_refresh.unbind(1))
timer.start(1.0)
func _on_slot_pressed(index: int) -> void:
slot_chosen.emit(index)
func _refresh() -> void:
for button in slot_buttons:
button.disabled = false
斷開連線、重複連線與一次性連線
disconnect()需要一個與已連線的 Callable 相等的 Callable。重新構建的_handler.bind(1)會相等並能正常工作。但 lambda 表達式不行:請將其保存在一個變數中,然後斷開該變數的連線。- 連接相同的 Callable 兩次會記錄
Signal 'x' is already connected to given callable ...並保留一個連線。當程式碼可能執行兩次時(例如在重新加入場景樹的節點的_ready()中),請使用is_connected()進行防護。 connect(callable, CONNECT_ONE_SHOT)在第一次發射後自動斷開連線。CONNECT_DEFERRED會讓處理器在空閒時間執行。當物理回呼(例如body_entered)改變物理狀態時,請使用它(或set_deferred());直接改變會記錄Can't change this state while flushing queries.。- 當物件被釋放時,對該物件的連線也會被移除。
extends Area2D
@onready var shape: CollisionShape2D = $CollisionShape2D
var _on_first_touch := func(body: Node2D) -> void: print(body.name, " touched first")
func _ready() -> void:
body_entered.connect(_on_first_touch, CONNECT_ONE_SHOT)
body_entered.connect(_close_after_entry, CONNECT_DEFERRED)
if not body_entered.is_connected(_on_body_entered):
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
print(body.name, " entered")
func _close_after_entry(_body: Node2D) -> void:
shape.disabled = true
func stop_listening() -> void:
body_entered.disconnect(_on_body_entered)
await
await signal 會暫停函式,直到信號觸發並返回其參數:無參數時返回 null,一個參數時返回該值,兩個或更多參數時返回 Array。包含 await 的函式是協程;需要其結果的呼叫者也必須 await 它,否則它們會立即繼續執行。
extends Node2D
signal door_opened(door_name: String)
func _ready() -> void:
await get_tree().create_timer(0.5).timeout
var opened_by: String = await door_opened
print("Door opened: ", opened_by)
await play_intro()
print("Intro finished")
func play_intro() -> void:
var tween := create_tween()
tween.tween_property(self, "modulate:a", 1.0, 1.0).from(0.0)
await tween.finished
await 永遠不會超時。如果信號從未被發射(或其發射者先被釋放),函式只會停在那裡,不會產生任何錯誤。當需要備用路徑時,請使用 Timer 或一次性連線。
編輯器連線
在節點面板中建立的連線會儲存在場景檔案中,而非腳本中:
[connection signal="pressed" from="StartButton" to="." method="_on_start_button_pressed"]
如果方法在腳本中被重新命名或刪除,該連線仍會留在 .tscn 檔案中。在 Godot 4.7.2 中,發射一個連線方法不存在的信號不會列印任何錯誤,因此按鈕會直接失效。重新命名處理器後,請搜尋場景中的 method="old_name" 並更新它們,或在節點面板中重新連線。當腳本應該自包含時,請在程式碼中連線;當設計師需要配置場景時,請在編輯器中連線。避免對同一個信號和方法同時使用兩種方式:第二次連線會記錄「already connected」。
向下呼叫,向上發訊
節點對其子節點呼叫方法(因為它擁有它們且知道它們的存在),並發射信號讓其父節點或兄弟節點做出反應(它不應該知道誰在監聽)。父節點,或組合兩者的節點,負責進行連線:
extends Node2D
@onready var player: Node2D = $Player
@onready var health_bar: ProgressBar = $HUD/HealthBar
func _ready() -> void:
player.connect(&"health_changed", _on_player_health_changed)
func _on_player_health_changed(_old_value: int, new_value: int) -> void:
health_bar.value = new_value
這個父節點只知道 Player 是一個 Node2D,因此它將自訂信號名稱作為字串。為玩家腳本賦予一個 class_name Player,將變數型別設為 Player,並寫成 player.health_changed.connect(_on_player_health_changed),這樣解析器也會檢查名稱。
對於許多不相關場景都關心的事件(玩家死亡、關卡完成),常見的模式是使用一個僅宣告信號的自動載入腳本作為事件匯流排:Events.player_died.emit() 和 Events.player_died.connect(_on_player_died)。
要取代的 Godot 3 形式
| Godot 3 | Godot 4 |
|---|---|
connect("pressed", self, "_on_pressed") | pressed.connect(_on_pressed) |
connect("pressed", self, "_on_pressed", [id]) | pressed.connect(_on_pressed.bind(id)) |
connect("timeout", self, "_on_timeout", [], CONNECT_ONESHOT) | timeout.connect(_on_timeout, CONNECT_ONE_SHOT) |
disconnect("pressed", self, "_on_pressed") | pressed.disconnect(_on_pressed) |
is_connected("pressed", self, "_on_pressed") | pressed.is_connected(_on_pressed) |
emit_signal("died") | died.emit() |
yield(obj, "signal_name") | await obj.signal_name |
funcref(obj, "method") | Callable(obj, "method") 或 obj.method |