Erros Comuns no Godot e Como Resolver: Nil, Null Instance e Sinais Mudos

Os erros mais comuns no Godot e como resolver: null instance, Nil, node not found e sinais que não disparam, com código GDScript corrigido na prática.
Erros Comuns no Godot e Como Resolver: Nil, Null Instance e Sinais Mudos
Os erros do Godot que mais derrubam iniciante não são bugs raros. São sempre os mesmos três: alguma coisa retornou Nil quando você esperava um node, um sinal que parece conectado mas nunca dispara, e um objeto que foi liberado da memória antes da hora. Eu já perdi tarde inteira em cada um deles, e hoje resolvo em minutos, porque a mensagem de erro diz exatamente onde olhar. O problema é que ninguém ensina a ler.
Esse artigo é o mapa que eu queria ter tido: os erros mais comuns do Godot 4, o que cada mensagem significa de verdade, e o código corrigido pra cada caso.
Antes de tudo: leia o erro inteiro
Quando o jogo quebra, o Godot pausa e abre o painel Debugger na aba Stack Trace. Ali tem três informações, e a maioria das pessoas só lê a primeira:
- A mensagem: o que deu errado ("Invalid get index...", "Node not found...").
- A linha: o editor pula direto pra linha que explodiu.
- O stack trace: a cadeia de chamadas que levou até ali. Se a linha que quebrou é genérica (um
take_damage()qualquer), o stack trace mostra quem chamou, e o culpado quase sempre está um nível acima.
Além disso, com o jogo rodando, a dock Scene ganha uma aba Remote. Ela mostra a árvore de nodes real, em tempo de execução, não a do editor. Quando um erro fala que um node não existe, é nessa aba que você confere se ele existe mesmo, com esse nome, nesse caminho. Essa aba sozinha resolve metade dos casos desse artigo.
Erros de Nil e null instance no Godot
A família de erros mais comum tem variações da mesma mensagem:
Invalid get index 'text' (on base: 'null instance').
Attempt to call function 'play' in base 'null instance' on a null instance.
Dependendo do contexto, a base aparece como null instance ou como Nil. A tradução é uma só: a variável que você está usando não aponta pra nada. Você escreveu label.text = "oi", mas label é null. O erro nunca está na linha que quebrou; está em onde a variável deveria ter recebido um valor e não recebeu. As causas clássicas vêm a seguir, em ordem de frequência.
Causa 1: o caminho do node está errado
@onready var label = $HUD/Label
Se o node não se chama exatamente Label, ou não é filho de HUD, ou você reorganizou a cena depois de escrever o script, o $ falha. No Godot 4 você ainda ganha um erro dedicado no console na hora do load:
Node not found: "HUD/Label" (relative to "/root/Main/Player").
A parte entre parênteses importa: o caminho é relativo ao node dono do script. Se você moveu o Player pra dentro de outro node, todos os $ relativos dele podem quebrar.
Como resolver: rode o jogo, abra a aba Remote e confira o caminho real. Se a estrutura da cena muda com frequência, prefira exportar a referência e arrastar o node no Inspector, que sobrevive a reorganização:
@export var label: Label
E quando um node pode legitimamente não existir, use a versão que não explode:
var label = get_node_or_null("HUD/Label")
if label:
label.text = "oi"
Causa 2: acessar o node cedo demais
@onready resolve a referência quando o node entra na árvore, logo antes do _ready(). Se você tenta usar a variável antes disso, ela ainda é null:
@onready var sprite = $Sprite2D
func _init():
sprite.modulate = Color.RED # quebra: _init roda antes do node entrar na árvore
Tudo que toca em outros nodes vai pra _ready() ou depois. O mesmo vale pra cena instanciada por código: o $ e o get_node() só funcionam depois do add_child(), porque antes disso o node não está em árvore nenhuma.
var inimigo = preload("res://inimigo.tscn").instantiate()
add_child(inimigo) # primeiro entra na árvore
inimigo.get_node("Sprite2D") # agora sim existe caminho pra resolver
Um detalhe que confunde muita gente: dentro de uma cena, o _ready() dos filhos roda antes do _ready() do pai. Então o pai pode acessar os filhos no próprio _ready() com segurança. O contrário não vale: filho que precisa de algo que o pai só cria no _ready() dele precisa esperar, e o jeito limpo é o pai avisar por sinal ou chamar o filho explicitamente.
Causa 3: o objeto foi liberado (previously freed)
Essa variação é mais traiçoeira porque a referência já funcionou antes:
Invalid get index 'position' (on base: 'previously freed').
Significa que a variável apontava pra um objeto que já passou por queue_free(). Cenário típico: o inimigo morre, é liberado, e um projétil ainda guardava referência pra ele como alvo. No frame seguinte o projétil tenta ler alvo.position e quebra.
A proteção é checar a validade antes de usar:
var alvo: Node2D = null
func _physics_process(delta):
if not is_instance_valid(alvo):
alvo = null
return
var direcao = (alvo.position - position).normalized()
Dois detalhes que evitam essa classe de bug na raiz. Primeiro, prefira queue_free() a free(): ele adia a liberação pro fim do frame, então tudo que ainda roda naquele frame não encontra um objeto pela metade. Segundo, quando vários objetos dependem de um, inverta a responsabilidade: o objeto que vai morrer emite um sinal (o próprio tree_exiting serve) e quem depende dele limpa a referência ao receber.
Sinais conectados que nunca disparam
O segundo grupo de erro é silencioso, e por isso pior: nenhuma mensagem vermelha, o jogo roda, mas o sinal não chega. O botão não responde, o body_entered não detecta, a morte do inimigo não atualiza o placar. Quase sempre é uma destas quatro causas.
Sintaxe do Godot 3 em projeto Godot 4
Se você aprendeu por tutorial antigo, é provável que tenha escrito isto:
# Godot 3 (não use no Godot 4)
connect("body_entered", self, "_on_body_entered")
No Godot 4 a conexão usa o sinal como propriedade e recebe um Callable:
# Godot 4
body_entered.connect(_on_body_entered)
Emitir também mudou: emit_signal("morreu") ainda funciona, mas o jeito atual é morreu.emit(). A vantagem da sintaxe nova não é estética: como o sinal e o método são referências reais e não strings, erro de digitação vira erro de editor na hora, em vez de silêncio em runtime.
signal morreu(pontos: int)
func take_damage(dano: int):
vida -= dano
if vida <= 0:
morreu.emit(100)
queue_free()
Conectou no editor e depois renomeou o método
Quando você conecta um sinal pela aba Node > Signals do editor, o Godot grava o nome do método na cena. Se depois você renomeia o método no script, a conexão aponta pro nada e o console mostra um erro de Callable inválido quando o sinal dispara. A conexão feita no editor aparece com um ícone verde ao lado da função no script; se o ícone sumiu depois de um rename, a conexão quebrou. Abra a aba Signals, desconecte a antiga e conecte de novo.
Minha regra prática: sinal de UI estática (botão, slider) eu conecto no editor; sinal de coisa instanciada em runtime eu conecto por código, porque o editor não tem como conhecer um node que ainda não existe:
func spawn_inimigo():
var inimigo = preload("res://inimigo.tscn").instantiate()
inimigo.morreu.connect(_on_inimigo_morreu)
add_child(inimigo)
Conectar duas vezes
Se o código de conexão roda mais de uma vez (uma tela que abre e fecha, por exemplo), o Godot reclama que o sinal já está conectado e, dependendo do caso, o callback passa a rodar em dobro. A checagem é direta:
if not morreu.is_connected(_on_inimigo_morreu):
morreu.connect(_on_inimigo_morreu)
Ou conecte com a flag CONNECT_ONE_SHOT quando o sinal só interessa uma vez: a conexão se desfaz sozinha depois do primeiro disparo.
O sinal dispara, mas a física não deixa
Caso específico de body_entered e area_entered: o código está perfeito e mesmo assim nada acontece. Aí o problema não é o sinal, é a detecção. Confira nesta ordem: as collision layers e masks dos dois objetos se cruzam? Os dois têm CollisionShape com shape atribuída (uma CollisionShape2D vazia não detecta nada e o editor avisa com um ícone amarelo)? No caso de RigidBody, o contact_monitor está ligado e max_contacts_reported é maior que zero? Ligue Debug > Visible Collision Shapes e rode: ver as shapes na tela responde a maioria dessas perguntas em segundos.
Três hábitos que aceleram qualquer debug
Breakpoint em vez de print. Clique na faixa à esquerda do número da linha (ou escreva breakpoint numa linha do script) e rode o jogo. Quando a execução chega ali, o Godot pausa e o painel do debugger mostra o valor de todas as variáveis locais e membros naquele instante. É a resposta definitiva pra "o que tem dentro dessa variável aqui?", sem encher o código de print.
push_error e push_warning pra erro seu. Quando o seu código detecta um estado inválido, registre com push_error("inventário cheio sem slot livre") em vez de print. A mensagem aparece vermelha na aba Errors do debugger, com stack trace, junto dos erros da engine. Print se perde no meio do console; erro registrado tem contexto.
Tipagem estática onde importa. Declarar var alvo: Node2D em vez de var alvo faz o Godot acusar no editor, sublinhado em vermelho antes de rodar, boa parte dos erros que sem tipo só apareceriam em runtime como Nil. Não precisa tipar o projeto inteiro de uma vez; comece pelas variáveis que cruzam fronteiras entre cenas, que é onde esses erros nascem.
Fechando
Os erros comuns do Godot se repetem porque as causas se repetem: referência que não foi resolvida (caminho errado, timing errado ou objeto liberado) e sinal que não chega (sintaxe antiga, conexão quebrada ou física mal configurada). Com a aba Remote, o Visible Collision Shapes e um breakpoint bem colocado, nenhum deles segura você mais que alguns minutos.
Da próxima vez que aparecer um null instance, resista ao impulso de mexer no código no chute. Leia a mensagem inteira, olhe o stack trace, confira a árvore Remote. O Godot quase sempre está dizendo exatamente onde está o problema. A gente é que aprende a ouvir.


