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" 并更新它们,或者在节点面板中重新连接。当脚本应保持自包含时,在代码中连接;当设计师连接场景时,在编辑器中连接。避免对同一个信号和方法同时进行两种操作:第二次连接会记录“已连接”。
向下调用,向上信号
节点调用其子节点的方法(它拥有它们并且知道它们存在),并发射信号供其父节点或兄弟节点响应(它不应知道谁在监听)。父节点,或组合两者的节点,进行连接:
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 |