Voltar para o Blog
Quest Log

Erros Comuns no Godot: Como Resolver Nil e Null Instance

Desenvolvedor analisando um erro de null instance no debugger do Godot Engine

Os erros mais comuns no Godot e como resolver: null instance, Nil, node not found e sinais que não disparam, com o código GDScript corrigido na prática.

Ferramenta:Godot

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:

  1. A mensagem: o que deu errado ("Invalid get index...", "Node not found...").
  2. A linha: o editor pula direto pra linha que explodiu.
  3. 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: 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: 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 = $Sprite2D

func _init() -> void:
    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: Node = 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: float) -> void:
    if not is_instance_valid(alvo):
        alvo = null
        return
    var direcao: Vector2 = (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.

Curso de Godot
Quer fazer seu primeiro jogo na Godot?

No curso de Godot da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Godot
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

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) -> void:
    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() -> void:
    var inimigo: Node = 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.

Erro de índice em Array e Dictionary

A terceira família de erro que enche o console não vem de node nenhum, vem de coleção. A mensagem tem duas caras:

Invalid get index '5' (on base: 'Array').
Invalid access to property or key 'vida' on a base object of type 'Dictionary'.

No array, o número entre aspas é o índice que você pediu e que não existe: você acessou inimigos[5] num array de 3 elementos, ou leu fila[0] numa fila vazia. No dicionário, a chave que você buscou não está lá, quase sempre por erro de digitação ou porque o dado ainda não foi inserido.

A correção é checar o tamanho antes de indexar e a existência da chave antes de ler:

var inimigos: Array[Node2D] = []
var atributos: Dictionary = {}

func alvo_mais_proximo() -> Node2D:
    if inimigos.is_empty():
        return null            # nunca indexe um array que pode estar vazio
    return inimigos[0]

func ler_vida() -> int:
    if not atributos.has("vida"):
        return 0               # chave pode não ter sido preenchida ainda
    return atributos["vida"]

Duas pegadinhas específicas que confundem. A primeira: o índice negativo no Godot 4 conta de trás pra frente (lista[-1] é o último elemento), então um cálculo que escorrega pra -1 numa lista vazia ainda estoura, porque -1 de um array vazio não existe. A segunda, no dicionário, é que ler uma chave ausente com colchetes atributos["vida"] quebra, mas o método atributos.get("vida", 0) devolve um padrão (o segundo argumento) em vez de explodir. Pra estado de jogo que pode ou não existir, get com valor padrão é o caminho limpo. Tipar a coleção (Array[Node2D], e não Array solto) faz o editor acusar boa parte dessas confusões antes de rodar.

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. Uma fonte comum de bug difícil de reproduzir é a aleatoriedade mal controlada, então vale entender como usar RNG e semente na Godot pra tornar o erro repetível. E quando o jogo cresce, ler bem o erro deixa de bastar sozinho: vale montar um processo de QA para caçar os bugs antes do jogador, com testes estruturados e reprodução clara, e organizar tudo o que aparecer num fluxo de rastreio e triagem de bugs para corrigir na ordem certa. Pra ver esses erros no contexto do resto da engine, o hub de Godot e das técnicas que a casa já documentou reúne os tutoriais vizinhos de código e cena.

Perguntas frequentes

O que significa null instance no Godot?

Significa que a variável que você está usando não aponta para objeto nenhum. O erro quase nunca está na linha que quebrou, e sim onde a variável deveria ter recebido um valor e não recebeu: caminho de node errado, acesso cedo demais ou objeto já liberado.

Por que meu sinal não dispara no Godot 4?

As causas mais comuns são sintaxe do Godot 3 (connect com strings), método renomeado depois de conectar pelo editor, conexão feita duas vezes, ou detecção de física mal configurada no caso de body_entered. Nenhuma delas gera erro vermelho, por isso é silenciosa.

Como sei qual node realmente existe em runtime?

Com o jogo rodando, a dock Scene ganha a aba Remote, que mostra a árvore de nodes real em tempo de execução, não a do editor. Quando um erro diz que um node não existe, é nessa aba que você confere o nome e o caminho verdadeiros.

Qual a diferença entre queue_free e free no Godot?

free libera o objeto na hora, o que pode deixar outro código daquele frame segurando uma referência pela metade. queue_free adia a liberação para o fim do frame, então tudo que ainda roda encontra o objeto inteiro. Prefira queue_free quase sempre.