Señales en Godot 4
En Godot 4, una señal es un valor Signal de primera clase y una referencia a un método es un Callable. Usa la sintaxis de objeto (button.pressed.connect(_on_pressed)) en todas partes. El analizador sintáctico rechazará un manejador mal escrito, así como una señal mal escrita en self; una señal mal escrita en otro nodo tipado se detecta cuando unsafe_property_access está configurado como Error (consulta la skill godot-4-api-pitfalls). Las formas de cadena (connect("pressed", ...), emit_signal("died")) solo se verifican en tiempo de ejecución.
Declarar, emitir, conectar
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")
El manejador debe aceptar exactamente los argumentos que emite la señal. Una discrepancia no es un error de análisis sintáctico; al momento de emitir, Godot registra Error calling from signal 'x' to callable: '...': Method expected 0 argument(s), but called with 1. y omite el manejador.
bind() y unbind()
bind(a, b)añade argumentos extra después de los propios de la señal. Un manejador parapressedenlazado con un valor toma un parámetro; parahealth_changed(old, new)enlazado con un valor toma tres, en el ordenold, new, bound.unbind(n)elimina los últimosnargumentos de la señal, permitiendo que una función sin argumentos maneje una señal que envía algunos.
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
Desconectar, duplicados y conexiones de un solo uso
disconnect()necesita un Callable igual al conectado._handler.bind(1)construido de nuevo se compara como igual y funciona. Una lambda no: guárdala en una variable y desconéctala usando esa variable.- Conectar el mismo Callable dos veces registra
Signal 'x' is already connected to given callable ...y mantiene una conexión. Protégete conis_connected()cuando el código pueda ejecutarse dos veces, por ejemplo en_ready()de un nodo que se vuelve a agregar al árbol. connect(callable, CONNECT_ONE_SHOT)se desconecta después de la primera emisión.CONNECT_DEFERREDejecuta el manejador en tiempo de inactividad. Úsalo (oset_deferred()) cuando un callback de física comobody_enteredcambie el estado de la física; hacerlo directamente registraCan't change this state while flushing queries.- Las conexiones a un objeto se eliminan cuando ese objeto es liberado.
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 pausa la función hasta que la señal se active y devuelve sus argumentos: null para ningún argumento, el valor para uno, un Array para dos o más. Una función que contiene await es una corrutina; los llamadores que necesitan su resultado también deben hacer await, de lo contrario continúan inmediatamente.
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 nunca expira. Si la señal nunca se emite (o su emisor se libera primero), la función simplemente se detiene allí, sin error. Usa un Timer o una conexión de un solo uso cuando se necesite una ruta alternativa.
Conexiones del editor
Las conexiones hechas en el dock de Nodos se guardan en el archivo de la escena, no en el script:
[connection signal="pressed" from="StartButton" to="." method="_on_start_button_pressed"]
Si el método se renombra o elimina en el script, la conexión permanece en el .tscn. En Godot 4.7.2, emitir una señal cuyo método conectado no existe no imprime ningún error, por lo que el botón simplemente deja de funcionar. Después de renombrar un manejador, busca en las escenas method="old_name" y actualízalas, o reconecta en el dock de Nodos. Conecta en código cuando el script debe ser autocontenido; conecta en el editor cuando los diseñadores cablean las escenas. Evita hacer ambas cosas para la misma señal y método: la segunda conexión registrará "already connected".
Llamar hacia abajo, señal hacia arriba
Un nodo llama a métodos en sus hijos (los posee y sabe que existen) y emite señales para que su padre o hermanos reaccionen (no debe saber quién está escuchando). El padre, o cualquier nodo que componga los dos, realiza la conexión:
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
Este padre solo conoce a Player como un Node2D, por lo que nombra la señal personalizada como una cadena. Dale al script del jugador un class_name Player, tipifica la variable como Player, y escribe player.health_changed.connect(_on_player_health_changed) para que el analizador también verifique el nombre.
Para eventos de los que muchas escenas no relacionadas se preocupan (el jugador murió, el nivel terminó), un patrón común es un script de autoload que solo declara señales, usado como un bus de eventos: Events.player_died.emit() y Events.player_died.connect(_on_player_died).
Formas de Godot 3 para reemplazar
| 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") o obj.method |