Sinais no Godot 4: guia de signals para desacoplar seus nós

Aprenda sinais (signals) no Godot 4 de verdade: o que são, sinais nativos, conectar pelo editor ou por código com .connect(), criar signal customizado e emitir com .emit().
Se você já programou alguma coisa no Godot, uma hora bateu aquela dúvida: como fazer um nó avisar outro que algo aconteceu sem escrever um get_node("../../../Player") gigante e frágil? A resposta são os sinais no Godot 4 (os famosos signals). Sinais são o jeito nativo da engine de dizer "ei, aconteceu isso aqui", e deixar que quem se interessar reaja. Neste tutorial você vai entender o que é um sinal, usar os sinais nativos dos nós, conectar pelo editor e por código, criar seus próprios sinais tipados e evitar os erros clássicos de quem está começando.
O que é um sinal e o padrão observer
Um sinal é um aviso que um nó emite quando algo acontece com ele. Um botão emite quando é clicado. Um Timer emite quando o tempo acaba. Uma Area2D emite quando um corpo entra nela. O nó não decide o que fazer com esse evento, ele só anuncia. Quem quiser reagir se "inscreve" para ouvir aquele aviso.
Esse desenho tem nome: padrão observer. Existe um objeto que fala (o sujeito, quem emite) e existem objetos que escutam (os observadores). O detalhe que muda tudo é que o emissor não conhece os ouvintes. O botão não tem a menor ideia de que o seu menu está escutando o clique dele. Ele só grita "fui clicado" e segue a vida.
Por que isso importa? Porque a alternativa é acoplamento. Sem sinais, o botão precisaria guardar uma referência ao menu e chamar menu.abrir_configuracoes() diretamente. Aí o botão só serve para aquele menu. Com sinais, o mesmo botão serve para qualquer coisa, porque quem decide a reação é quem escuta, não quem emite.
Sinais nativos: o que já vem pronto
Quase todo nó do Godot já emite sinais úteis de fábrica. Você não precisa criar nada, só escutar. Três exemplos que você vai usar o tempo todo:
Buttonemitepressedquando é clicado.Area2Demitebody_enteredquando um corpo físico entra na área (passa o corpo como argumento).Timeremitetimeoutquando a contagem chega a zero.
Repare que alguns sinais mandam informação junto. O body_entered não avisa só "alguém entrou", ele te entrega quem entrou. Isso é ouro para lógica de jogo: colisão de projétil, checkpoint, gatilho de armadilha, tudo passa por aí.
Para ver a lista completa de sinais de um nó, selecione ele na cena e abra a aba Node (do lado da aba Inspector), seção Signals. Ali está tudo que aquele nó sabe emitir.
Conectar um sinal pelo editor
O jeito mais visual de conectar é pelo próprio editor, e para começar ele é ótimo porque você enxerga a ligação acontecendo.
Digamos que você tem um Button e um script na cena. Selecione o botão, vá na aba Node > Signals, dê dois cliques em pressed. O Godot abre uma janela perguntando qual nó vai receber o sinal e qual método vai ser chamado. Ele sugere um nome automático no padrão _on_botao_pressed. Confirme, e o editor cria o método no seu script e registra a conexão.
O resultado no código fica assim:
extends Control
func _on_botao_pressed() -> void:
print("Botão clicado pelo editor!")
Quando um sinal foi conectado pelo editor, aparece um ícone verde de conexão ao lado da declaração da função no script. É o sinal (trocadilho intencional) de que aquele método está ligado a um evento.
O editor é prático, mas tem um efeito colateiral: a conexão fica invisível no código. Quem lê só o script não sabe de onde vem a chamada. Por isso, conforme o projeto cresce, muita gente migra para conectar por código.
Conectar um sinal por código com .connect()
No Godot 4, conectar por código é limpo e legível. A sintaxe é objeto.nome_do_sinal.connect(metodo). Sinal é um membro de primeira classe do objeto, então você acessa botao.pressed e chama .connect() nele, passando a função que vai reagir.
extends Control
@onready var botao: Button = $Button
func _ready() -> void:
botao.pressed.connect(_on_botao_pressed)
func _on_botao_pressed() -> void:
print("Botão clicado por código!")
Faça isso dentro do _ready(), que roda uma vez quando o nó entra na cena. A vantagem é que a ligação está escrita ali, no script, à vista de todo mundo. Bateu o olho, entendeu o fluxo.
Se o sinal manda argumentos, seu método precisa receber os mesmos, na mesma ordem. O body_entered de uma Area2D passa o corpo que entrou:
extends Area2D
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(corpo: Node2D) -> void:
print("Entrou na área: ", corpo.name)
if corpo.is_in_group("player"):
print("Foi o jogador!")
E o Timer? Mesma ideia. Um caso comum é criar um cronômetro e reagir ao timeout:
extends Node
@onready var timer: Timer = $Timer
func _ready() -> void:
timer.timeout.connect(_on_timer_timeout)
timer.start(3.0)
func _on_timer_timeout() -> void:
print("Três segundos se passaram.")
Cuidado: sinais do Godot 3 têm outra sintaxe
Se você achar tutoriais antigos, vai ver conexões escritas assim:
# SINTAXE DO GODOT 3, NÃO USE NO GODOT 4
botao.connect("pressed", self, "_on_botao_pressed")
Isso é Godot 3. Repare que o sinal é uma string ("pressed"), o método também é uma string ("_on_botao_pressed"), e ainda tem um self no meio. No Godot 4 isso foi substituído pela sintaxe de primeira classe, sem strings:
# GODOT 4, a forma correta
botao.pressed.connect(_on_botao_pressed)
A diferença não é só estética. Com string, um erro de digitação no nome do método só estoura em tempo de execução. Com a referência direta, o editor te avisa na hora e ainda oferece autocompletar. Sempre que copiar código da internet, cheque se ele está usando string nesse formato. Se estiver, é Godot 3 e precisa ser convertido.
Declarar e emitir seu próprio sinal
Aqui é onde os sinais deixam de ser detalhe e viram arquitetura. Você pode criar sinais próprios para os seus scripts, não só usar os que já vêm prontos.
Imagine um script de vida do jogador. Quando a vida muda, várias coisas na tela precisam saber: a barra de vida, um efeito de tela vermelha, talvez o som de dano. O jeito acoplado seria o Player chamar cada um desses. O jeito desacoplado é o Player emitir um sinal e deixar cada interessado escutar.
Declare o sinal no topo do script com a palavra-chave signal, e já defina os parâmetros tipados:
extends CharacterBody2D
signal vida_mudou(nova_vida: int)
signal morreu
var vida: int = 100
func tomar_dano(quantidade: int) -> void:
vida -= quantidade
vida_mudou.emit(vida)
if vida <= 0:
morreu.emit()
Três pontos importantes nesse trecho:
signal vida_mudou(nova_vida: int)declara um sinal que carrega um inteiro. O tipointdocumenta o que vai junto e ajuda o editor a te alertar se você emitir errado.vida_mudou.emit(vida)dispara o sinal e manda o valor atual. Essa é a forma moderna do Godot 4:nome_do_sinal.emit(argumentos). Nada deemit_signal("vida_mudou", vida), que é o formato antigo com string.- Um sinal pode não ter parâmetro nenhum, como
morreu. Aí você declara e emite sem argumentos.
Agora qualquer nó pode escutar o Player sem que o Player saiba que eles existem. A barra de vida, por exemplo:
extends ProgressBar
@onready var player: CharacterBody2D = $"../Player"
func _ready() -> void:
player.vida_mudou.connect(_on_player_vida_mudou)
func _on_player_vida_mudou(nova_vida: int) -> void:
value = nova_vida
O Player continua sem uma linha sequer mencionando a barra de vida. Quer adicionar um efeito de tela quando a vida muda? Cria outro nó, conecta no mesmo vida_mudou, pronto. O emissor não muda.
Por que sinais desacoplam seus nós
Volte na dor do começo: o get_node("../../../Player"). Esse caminho é uma bomba-relógio. No dia em que você arrastar o Player para outro lugar na árvore, ou renomear um nó pai, aquele ../../../ quebra e o jogo trava. Você amarrou seu código à estrutura exata da cena.
Sinais atacam o problema por outro lado. Em vez de o nó filho ir caçar o Player lá em cima, o Player avisa quando algo muda e os interessados reagem. A dependência aponta para "um evento aconteceu", não para "o nó que está três níveis acima e se chama Player".
Uma regra prática que ajuda muito: sinais sobem, chamadas descem. Um nó pai pode chamar métodos dos filhos diretamente, porque ele os criou e sabe que existem. Mas um filho falando com o pai deve, de preferência, emitir um sinal, porque o filho não deveria depender de quem é o pai dele. Isso mantém os componentes reutilizáveis. O mesmo Player funciona em qualquer cena, porque ele não presume nada sobre a vizinhança.
Quando o desacoplamento não resolve tudo (por exemplo, um sistema global que muitas cenas precisam acessar, como pontuação ou áudio), o caminho costuma ser um singleton. Vale entender como funciona um autoload no Godot, que combina bem com sinais: o autoload emite, e cenas espalhadas escutam.
O erro clássico: conectar duas vezes (ou esquecer de conectar)
Dois bugs de sinal aparecem em praticamente todo projeto iniciante.
O primeiro é conectar o mesmo sinal duas vezes. Acontece muito quando você conecta pelo editor e, sem lembrar, conecta de novo por código. O resultado é o método rodando duas vezes a cada evento: um clique conta como dois, o inimigo toma dano dobrado, o som toca em eco. Nas versões atuais do Godot isso pode até estourar um erro dizendo que a conexão já existe.
A regra é simples: conecte em um lugar só. Escolha editor ou código e siga esse padrão no projeto inteiro. Se você realmente precisa conectar em um ponto que roda mais de uma vez, cheque antes:
func _ready() -> void:
if not botao.pressed.is_connected(_on_botao_pressed):
botao.pressed.connect(_on_botao_pressed)
O segundo bug é o oposto: esquecer de conectar. Você escreve um método _on_botao_pressed lindo, roda o jogo, clica no botão e nada acontece. O método está lá, mas nunca foi ligado a nenhum sinal, então nunca é chamado. Não existe erro, o jogo só fica mudo. Quando um método _on_alguma_coisa não dispara, a primeira suspeita é sempre: será que eu conectei? Cheque o ícone verde no editor ou procure a linha .connect() no código.
Uma dica de ouro para não se perder: mantenha o padrão de nome _on_ + nome do nó + nome do sinal. Assim você olha _on_timer_timeout e sabe na hora que aquilo é reação ao timeout de um Timer. Consistência de nome é o que salva você três meses depois, quando voltar no projeto sem lembrar de nada.
Fechando o ciclo
Sinais são um daqueles conceitos que, depois que caem a ficha, mudam como você estrutura tudo. Recapitulando o essencial: um sinal é um aviso que um nó emite (padrão observer), os nós já vêm com sinais nativos como pressed, body_entered e timeout, você conecta pelo editor ou por código com .connect(), cria os seus com a palavra-chave signal e parâmetros tipados, e dispara com .emit(). O ganho real é desacoplamento: menos get_node frágil, mais componentes que funcionam sozinhos.
O próximo passo é praticar em algo concreto. Um menu principal é o cenário perfeito, cheio de botões emitindo pressed. Siga o passo a passo de como criar um menu principal no Godot e conecte cada botão via sinal. E se você ainda está decidindo em qual linguagem investir, vale comparar C# e GDScript no Godot, porque os sinais funcionam nas duas, com pequenas diferenças de sintaxe. Comece pequeno, um sinal de cada vez, e logo você vai desenhar cenas inteiras pensando em quem avisa o quê.
Perguntas frequentes
Qual a diferença entre sinais e chamar um método direto no Godot?
Chamar um método direto acopla os dois nós: quem chama precisa saber quem é o outro e onde ele está na árvore. Um sinal inverte isso. O nó que emite não sabe quem escuta, ele só avisa que algo aconteceu. Isso deixa o código flexível e fácil de reorganizar.
Devo usar .emit() ou emit_signal() no Godot 4?
No Godot 4 a forma recomendada é a sintaxe de primeira classe: nome_do_sinal.emit(argumentos). O antigo emit_signal("nome", argumentos) ainda funciona por compatibilidade, mas usa string e não tem autocompletar nem checagem, então prefira sempre o .emit().
Como conectar um sinal por código em vez de pelo editor?
Use a sintaxe objeto.nome_do_sinal.connect(metodo). Exemplo: botao.pressed.connect(_on_botao_pressed). O método passado recebe os mesmos argumentos que o sinal emite, na mesma ordem.
Por que meu sinal está disparando duas vezes?
Quase sempre é sinal conectado duas vezes: uma pelo editor e outra por código, ou um .connect() dentro de uma função que roda mais de uma vez. Conecte em um só lugar. Se precisar checar, use is_connected() antes de conectar.
Sinal customizado no Godot 4 pode ter parâmetros tipados?
Sim. Declare com signal vida_mudou(nova_vida: int). Os tipos servem de documentação e ajudam o editor a te avisar quando você emite ou conecta com o formato errado.
Preciso desconectar um sinal quando o nó é destruído?
Na maioria dos casos não. Se o nó que emite ou o que escuta sai da árvore e é liberado, o Godot limpa a conexão. Você só precisa desconectar manualmente em casos específicos, como reconectar dinamicamente objetos que continuam vivos.


