Godot 4의 시그널
Godot 4에서 시그널은 일급 Signal 값이며, 메서드 참조는 Callable입니다. 모든 곳에서 객체 문법(button.pressed.connect(_on_pressed))을 사용하세요. 그러면 파서가 철자가 틀린 핸들러와 self의 철자가 틀린 시그널을 거부합니다; 다른 타입 노드의 철자가 틀린 시그널은 unsafe_property_access가 Error로 설정된 경우 잡힙니다(godot-4-api-pitfalls 스킬 참조). 문자열 형식(connect("pressed", ...), emit_signal("died"))은 런타임에만 확인됩니다.
선언, emit, 연결
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")
핸들러는 시그널이 방출하는 인수를 정확히 받아들여야 합니다. 불일치는 파싱 오류가 아닙니다. emit 시점에 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이 필요합니다. 다시 생성된_handler.bind(1)는 동등하게 비교되어 작동합니다. 람다는 그렇지 않습니다: 변수에 저장하고 해당 변수로 연결 해제하세요.- 동일한 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"를 기록합니다.
Call down, signal up
노드는 자식의 메서드를 호출하고(자식의 존재를 알고 소유함) 부모나 형제가 반응할 시그널을 방출합니다(누가 듣고 있는지 알 필요 없음). 부모 또는 두 노드를 구성하는 노드가 연결을 수행합니다:
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 |