Signals no Godot: como usar sinais no GDScript

Aprenda signals no Godot 4 com GDScript: conectar sinais pelo editor e por código, criar custom signals, usar await, bind e o padrão de event bus.
Signals no Godot: como usar sinais no GDScript
Signals são o sistema de comunicação do Godot: um node anuncia que algo aconteceu ("morri", "botão apertado", "alguém entrou na área") e quem quiser ouvir, ouve. Quem emite não precisa saber quem está escutando. É o padrão observer embutido na engine, e dominar signals no Godot é o que separa um projeto que cresce organizado de um emaranhado de referências quebradas.
A diferença prática é grande. Sem signals, sua moeda precisa de uma referência direta pra HUD pra atualizar o contador. Aí você muda a cena da HUD, a referência quebra, e o jogo crasha com null instance. Com signals, a moeda só grita "fui coletada" e segue a vida. Quem cuida do placar se vira pra escutar.
Esse tutorial cobre o caminho completo no Godot 4.x: conectar pelo editor, conectar por código, criar seus próprios sinais, passar argumentos, usar await, e o padrão de event bus pra comunicação global. Todo código roda como está.
Como signals funcionam
Todo node do Godot já vem com sinais embutidos. Um Button tem pressed, um Timer tem timeout, uma Area2D tem body_entered e body_exited. Você não cria nada disso, só conecta uma função sua que será chamada quando o sinal disparar.
A regra de ouro da arquitetura com signals é uma frase: chame pra baixo, sinalize pra cima. Um node pai pode chamar métodos dos filhos diretamente (ele é dono deles, conhece a estrutura). Mas um filho nunca deveria alcançar o pai ou um "primo" na árvore com get_parent() ou get_node("../../HUD"). Quando a informação precisa subir ou atravessar a árvore, ela vai por sinal. Seguir isso deixa cada cena independente e testável sozinha.
Conectando sinais pelo editor
O jeito mais rápido de começar. Selecione o node, abra a aba Node (do lado do Inspector), e você vê a lista de todos os sinais disponíveis. Dê dois cliques no sinal, escolha o node que vai receber, e o Godot cria o método pra você no script:
func _on_button_pressed():
print("clicou!")
O nome segue o padrão _on_<nome_do_node>_<nome_do_sinal>. Note o ícone verde na margem esquerda do editor de script: ele indica que aquela função está conectada a um sinal. Clicar nele mostra de onde a conexão vem.
Conectar pelo editor é ótimo pra relações fixas dentro da mesma cena: o botão da UI que sempre chama a mesma função, o Timer que sempre dispara o mesmo spawn. A limitação é que só funciona entre nodes que já existem na cena salva. Pra qualquer coisa instanciada em runtime (inimigo spawnado, projétil, item), você conecta por código.
Conectando sinais por código
No Godot 4, sinais são objetos de verdade (tipo Signal) e a conexão usa o método connect direto no sinal:
func _ready():
$Button.pressed.connect(_on_button_pressed)
$Timer.timeout.connect(_on_timer_timeout)
$Area2D.body_entered.connect(_on_body_entered)
func _on_button_pressed():
print("clicou")
func _on_timer_timeout():
print("tempo esgotado")
func _on_body_entered(body):
print("entrou na área: ", body.name)
Repare que você passa a função sem parênteses: _on_button_pressed, não _on_button_pressed(). Você está entregando a função em si (um Callable), não chamando ela agora. Esquecer isso e colocar parênteses é o erro de sintaxe mais comum de quem está começando com signals.
Se você veio do Godot 3, a sintaxe antiga era connect("pressed", self, "_on_button_pressed"), com strings. A nova é melhor em tudo: o editor autocompleta, e erro de digitação aparece em tempo de parse em vez de falhar silenciosamente em runtime.
Pra funções curtas que não valem um método nomeado, dá pra conectar uma lambda:
$Button.pressed.connect(func(): print("clicou"))
Útil pra protótipo e pra UI simples. Se a lógica passar de uma linha ou duas, extraia pra um método com nome, seu eu do futuro agradece na hora de debugar.
Conectando em objetos instanciados
O caso onde conectar por código é obrigatório. Você spawna um inimigo e quer saber quando ele morre:
const ENEMY_SCENE = preload("res://enemies/enemy.tscn")
func spawn_enemy():
var enemy = ENEMY_SCENE.instantiate()
enemy.died.connect(_on_enemy_died)
add_child(enemy)
func _on_enemy_died():
score += 100
A conexão acontece antes do add_child, e tudo bem: conectar não exige que o node esteja na árvore.
Custom signals: criando seus próprios sinais
Sinais embutidos cobrem o que a engine sabe. Pro que o seu jogo sabe (vida mudou, fase terminou, item raro dropou), você declara sinais próprios com a keyword signal:
extends CharacterBody2D
signal health_changed(new_health)
signal died
var health = 100
func take_damage(amount: int):
health -= amount
health_changed.emit(health)
if health <= 0:
died.emit()
Declarou no topo do script, emite com .emit() passando os argumentos na ordem declarada. Quem escuta recebe os mesmos argumentos:
# Na HUD:
func _ready():
var player = get_node("/root/Main/Player")
player.health_changed.connect(_on_player_health_changed)
player.died.connect(_on_player_died)
func _on_player_health_changed(new_health):
health_bar.value = new_health
func _on_player_died():
show_game_over()
O detalhe importante de design: o player não sabe que a HUD existe. Ele só anuncia o fato. Amanhã você pluga um sistema de som que toca um grunhido no health_changed, um efeito de tela vermelha, um achievement. Nenhuma dessas adições toca no script do player. É isso que signals compram: cada novo ouvinte custa zero mudança em quem emite.
Uma boa prática de nomenclatura: sinal descreve fato passado (died, health_changed, coin_collected), não comando (update_ui, play_sound). Se o nome do seu sinal é uma ordem pra outro node, a dependência só mudou de roupa, e você perdeu o desacoplamento.
Argumentos extras com bind
Às vezes quem conecta sabe algo que quem emite não sabe. Clássico: dez botões de seleção de fase, todos com o mesmo sinal pressed, que não carrega argumento nenhum. O bind() anexa argumentos extras na hora da conexão:
func _ready():
for i in range(10):
var button = level_buttons[i]
button.pressed.connect(_on_level_button_pressed.bind(i + 1))
func _on_level_button_pressed(level_number: int):
load_level(level_number)
Os argumentos do bind chegam depois dos argumentos do próprio sinal. Se o sinal fosse body_entered(body) com um bind de spawn_point, a função receberia (body, spawn_point), nessa ordem.
Await: esperando um sinal
O await pausa a função até um sinal disparar, sem travar o resto do jogo. É o que transforma sequência de eventos em código linear legível:
func play_death_sequence():
set_physics_process(false)
animation_player.play("death")
await animation_player.animation_finished
await get_tree().create_timer(1.0).timeout
get_tree().reload_current_scene()
Sem await, essa sequência viraria três funções encadeadas por conexões manuais. Com ele, lê de cima pra baixo como a sequência realmente acontece. O create_timer é o truque pra "espera X segundos" descartável: cria um timer temporário e aguarda o timeout dele, sem precisar de node Timer na cena.
Um cuidado: se o node for destruído enquanto a função está suspensa no await, o resto da função simplesmente não executa. Em geral é o comportamento que você quer, mas vale saber que o código depois do await não tem garantia de rodar.
Desconectando e conexões one-shot
Conexão criada por código vive até o node morrer ou você desconectar na mão:
if player.health_changed.is_connected(_on_player_health_changed):
player.health_changed.disconnect(_on_player_health_changed)
O is_connected evita o erro de desconectar algo que não estava conectado. E pra sinal que só interessa uma vez (um tutorial que dispara na primeira moeda, um checkpoint que ativa uma vez só), existe a flag CONNECT_ONE_SHOT, que desconecta sozinha após o primeiro disparo:
area.body_entered.connect(_on_first_coin_collected, CONNECT_ONE_SHOT)
Bem mais limpo que conectar, guardar um booleano already_triggered e desconectar manualmente.
Quando o node que escuta é destruído com queue_free(), o Godot remove as conexões dele automaticamente. Você não precisa desconectar tudo no _exit_tree por paranoia; desconexão manual é pra quando você quer parar de ouvir antes de morrer.
Event bus: sinais globais com autoload
Signals diretos funcionam quando emissor e ouvinte estão razoavelmente próximos na árvore. Mas e quando a moeda no fundo do level precisa avisar a HUD, o sistema de save e o de achievements ao mesmo tempo? Encadear referências atravessando a cena inteira vira sofrimento.
O padrão consagrado na comunidade Godot é o event bus: um script autoload que só declara sinais, servindo de quadro de avisos global. Crie um events.gd:
extends Node
# events.gd, registrado como autoload com o nome "Events"
signal coin_collected(value)
signal player_died
signal level_completed(level_number)
Registre em Project Settings > Globals > Autoload com o nome Events. Agora qualquer script emite e qualquer script escuta, sem ninguém conhecer ninguém:
# Na moeda:
func _on_body_entered(body):
Events.coin_collected.emit(10)
queue_free()
# Na HUD:
func _ready():
Events.coin_collected.connect(_on_coin_collected)
func _on_coin_collected(value):
score += value
score_label.text = str(score)
Funciona muito bem, com uma ressalva de quem já se queimou: o event bus é tentador demais. Se tudo vira sinal global, você troca o emaranhado de referências por um emaranhado de eventos, e debugar "quem emitiu isso e por quê" fica difícil do mesmo jeito. Meu critério na prática: comunicação dentro da mesma cena usa sinal direto ou chamada do pai; o bus fica pra eventos de interesse genuinamente global, aqueles que três ou mais sistemas distantes precisam ouvir.
Erros comuns com signals
Alguns tropeços que aparecem em todo projeto iniciante, pra você pular essa fila:
Conectar duas vezes. Se o _ready roda de novo (cena reinstanciada) ou você conecta dentro de um loop sem perceber, o sinal dispara a função em dobro. Sintoma clássico: som tocando duas vezes, dano duplicado. O is_connected antes do connect resolve.
Emitir antes de alguém conectar. Sinal não tem memória: se a HUD conecta no _ready dela mas o player emitiu health_changed antes, aquele disparo se perdeu. Pra estado inicial, não dependa de sinal: leia o valor direto na inicialização e use o sinal só pras mudanças seguintes.
Sinal como chamada de função disfarçada. Se você emite um sinal e na linha seguinte precisa do resultado do que o ouvinte fez, você não queria um sinal, queria uma chamada de método. Sinal é "avisar e esquecer".
Fechando
Signals são daquelas ferramentas que mudam como você estrutura projeto inteiro, não só um detalhe de sintaxe. O resumo que vale levar: conecte pelo editor o que é fixo na cena, conecte por código o que nasce em runtime, declare custom signals pra anunciar fatos do seu jogo, e reserve o event bus pro que é realmente global. E a frase que guia tudo: chame pra baixo, sinalize pra cima.
Pra fixar, pegue um projeto seu que tenha um get_parent() ou um get_node("../...") apontando pra cima da árvore e refatore pra sinal. Você vai sentir na prática a cena ficando independente, e depois da primeira refatoração dessas, signals viram reflexo.


