Qué son las señales en Godot: signal, connect y emit explicados

Explicación práctica del sistema de señales de Godot usando un ejemplo de recoger monedas: qué es signal, cómo conectar señales, cuándo emitirlas y cómo Main recibe la señal collected de Coin para actualizar la puntuación.

En Godot, una señal puede entenderse como una notificación emitida por un nodo.

Algo ocurrió. Quien esté interesado puede responder.

Ejemplos:

  • Botón: me han pulsado.
  • Moneda: el jugador me recogió.
  • Jugador: mi vida cambió.
  • Enemigo: morí.
  • Temporizador: se acabó el tiempo.

Otros nodos pueden conectarse a estas señales y ejecutar una función cuando se emiten. El emisor no necesita buscar directamente al receptor, lo que reduce el acoplamiento entre nodos.

Los tres pasos básicos

Un flujo completo de señales suele tener tres pasos:

  1. Declarar la señal.
  2. Conectar la señal.
  3. Emitir la señal.

Puedes imaginarlo como un timbre:

Concepto Analogía
signal Instalar el timbre
connect Conectar el timbre al sonido interior
emit Pulsar el timbre
Función receptora Alguien lo oye y abre la puerta

La forma sencilla de recordarlo:

1
2
3
signal  = definir la notificación
connect = asignar receptor
emit    = enviar la notificación

Dos tipos de señales en Godot

En Godot, las señales habituales se dividen en señales integradas y señales personalizadas.

Señales integradas

Las señales integradas ya vienen con los nodos de Godot.

Ejemplos comunes:

  • Button.pressed
  • Timer.timeout
  • Area2D.body_entered
  • Area2D.body_exited

Si el nodo de moneda hereda de Area2D, ya tiene la señal body_entered. Cuando un CharacterBody2D entra en el área de colisión de la moneda, Godot emite esa señal automáticamente.

Señales personalizadas

Las señales personalizadas las declaramos nosotros.

Por ejemplo, una moneda recogida:

1
signal collected

Esta señal collected no viene integrada en Godot. Es un tipo de notificación que definimos nosotros.

Dos capas de señales en el ejemplo de la moneda

Un script simple para la moneda puede ser:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
extends Area2D

signal collected


func _ready() -> void:
    body_entered.connect(_on_body_entered)


func _on_body_entered(body: Node2D) -> void:
    if body.name != "Player":
        return

    collected.emit()
    queue_free()

Aquí se usan realmente dos capas de señales.

Primera capa: body_entered

Esta línea:

1
body_entered.connect(_on_body_entered)

significa:

Cuando un objeto físico entra en el Area2D de Coin, llama a _on_body_entered().

Flujo:

1
2
3
4
5
Player entra en el área de colisión de la moneda
Area2D emite body_entered
se ejecuta _on_body_entered(body)

El parámetro body es el nodo que entró en el área de la moneda. Si entra Player:

1
body == Player

Segunda capa: collected

Esta línea:

1
signal collected

declara una señal, indicando que Coin puede enviar una notificación llamada collected.

Después:

1
collected.emit()

significa emitir formalmente la notificación:

Esta moneda ya fue recogida.

Pero cuidado: emitir una señal no significa que alguien la reciba.

Si ningún nodo está conectado a collected, la señal se emite igualmente, pero no produce otros efectos. La moneda desaparece por esta línea:

1
queue_free()

No desaparece automáticamente por collected.emit().

Flujo completo de comunicación

Cuando el jugador toca la moneda, el flujo completo es:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Player entra en Coin
El Area2D de Coin emite body_entered
Coin ejecuta _on_body_entered()
Confirma que quien entró es Player
Coin emite collected
Main recibe collected
Main aumenta la puntuación
Coin ejecuta queue_free()
La moneda desaparece

Aquí hay dos emisores:

1
2
Area2D → emite body_entered
Coin   → emite collected

body_entered le dice a Coin: “Algo entró en tu área.”

collected le dice a Main: “Esta moneda fue recogida.”

Cómo hacer que Main reciba la señal de la moneda

Supongamos esta estructura de escena:

1
2
3
4
5
Main  Node2D
├─ Player
├─ Coin
└─ CanvasLayer
   └─ ScoreLabel

Puedes conectar la señal en el editor o desde código.

Método 1: conectar en el editor de Godot

Abre main.tscn.

Primero, selecciona en el árbol de escena:

1
Coin

Luego, en el panel derecho, cambia de Inspector a:

1
Señales

En la interfaz de Godot, este panel aparece como:

1
Node

Busca la señal personalizada:

1
collected()

Haz doble clic.

Elige como nodo receptor:

1
Main

Pulsa Connect. Godot generará una función parecida en el script de Main:

1
2
func _on_coin_collected() -> void:
    pass

Cámbiala por:

1
2
3
4
5
6
var score: int = 0


func _on_coin_collected() -> void:
    score += 1
    print("Puntuación actual:", score)

Al ejecutar y recoger la moneda, verás:

1
Puntuación actual:1

Método 2: conectar señales desde código

También puedes conectarla directamente en main.gd:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
extends Node2D

var score: int = 0

@onready var coin: Area2D = $Coin


func _ready() -> void:
    coin.collected.connect(_on_coin_collected)


func _on_coin_collected() -> void:
    score += 1
    print("Puntuación actual:", score)

La línea importante es:

1
coin.collected.connect(_on_coin_collected)

Significa:

1
2
3
Emisor: coin
Señal: collected
Función receptora: _on_coin_collected

En Godot 4 se recomienda llamar connect() directamente desde el objeto Signal:

1
coin.collected.connect(_on_coin_collected)

No copies sin revisar la sintaxis antigua de algunos tutoriales de Godot 3.

Hacer que una señal lleve datos

Una señal no solo puede decir “algo ocurrió”; también puede llevar datos.

Por ejemplo, distintas monedas pueden tener distinto valor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
extends Area2D

signal collected(value: int)

@export var value: int = 1


func _ready() -> void:
    body_entered.connect(_on_body_entered)


func _on_body_entered(body: Node2D) -> void:
    if body.name != "Player":
        return

    collected.emit(value)
    queue_free()

Una moneda normal puede usar:

1
value = 1

Una moneda grande puede usar:

1
value = 5

Main también debe recibir el parámetro:

1
2
3
4
5
6
7
var score: int = 0


func _on_coin_collected(value: int) -> void:
    score += value
    print("Obtenido:", value)
    print("Puntuación total:", score)

El proceso:

1
2
3
4
5
Coin: fui recogida, mi valor es 5
Main: recibe value = 5
score += 5

Por qué Coin no debería modificar directamente la UI

No se recomienda escribir esto:

1
2
3
func _on_body_entered(body: Node2D) -> void:
    get_node("../CanvasLayer/ScoreLabel").text = "1"
    queue_free()

Porque Coin dependería de la estructura exacta de nodos de Main:

  • Coin debe saber dónde está ScoreLabel.
  • Coin debe saber cómo se guarda la puntuación.
  • Coin debe saber cómo se actualiza la UI.

Si mueves el nodo de UI, la ruta de Coin puede romperse.

Una mejor estructura:

1
2
3
4
5
6
7
8
Coin solo se encarga de:
"Fui recogida."

Main se encarga de:
"Aumentar la puntuación."

UI se encarga de:
"Mostrar la puntuación."

Es decir:

1
2
3
4
5
6
7
Coin
  │ collected(value)
Main
  │ actualiza score
ScoreLabel

Esa es la función de las señales: reducir el acoplamiento entre nodos.

Tres conceptos fáciles de confundir

signal

Declara una señal:

1
signal collected

Equivale a crear un tipo de notificación.

connect()

Indica qué función se llamará cuando la señal se emita:

1
collected.connect(_on_collected)

emit()

Emite la señal:

1
collected.emit()

Recordatorio:

1
2
3
signal  = definir la notificación
connect = asignar receptor
emit    = enviar la notificación

Código recomendado para la moneda

coin.gd:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
extends Area2D

signal collected(value: int)

@export var value: int = 1


func _ready() -> void:
    body_entered.connect(_on_body_entered)


func _on_body_entered(body: Node2D) -> void:
    if body.name != "Player":
        return

    collected.emit(value)
    queue_free()

main.gd:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
extends Node2D

var score: int = 0

@onready var score_label: Label = $CanvasLayer/ScoreLabel
@onready var coin: Area2D = $Coin


func _ready() -> void:
    coin.collected.connect(_on_coin_collected)
    update_score_label()


func _on_coin_collected(value: int) -> void:
    score += value
    update_score_label()


func update_score_label() -> void:
    score_label.text = "Puntuación: %d" % score

La idea central es:

Las señales notifican qué ocurrió. No deciden todo lo que los demás nodos deben hacer después.

Cuando entiendes esto, botones, monedas, enemigos, temporizadores, cambios de vida y actualizaciones de UI en Godot se vuelven mucho más fáciles de organizar.

记录并分享
Creado con Hugo
Tema Stack diseñado por Jimmy