Instanciar Cenas no Godot: Spawn de Inimigos e Projéteis na Prática

Aprenda a instanciar cena no Godot do jeito certo: preload, instantiate() e add_child para spawnar inimigos e projéteis sem bug, com código Godot 4.
Instanciar Cenas no Godot: Spawn de Inimigos e Projéteis na Prática
Todo jogo precisa criar coisas em tempo real: a bala que sai da arma, o inimigo que aparece na borda da tela, a moeda que pinga quando o baú abre. No Godot, tudo isso passa pelo mesmo caminho: instanciar cena. Você monta o objeto uma vez como cena salva, e o código fabrica cópias dele quando o jogo pede.
Se você veio da Unity, é o equivalente do prefab com Instantiate(). A diferença é que no Godot qualquer cena é "prefab": não existe um tipo especial, todo .tscn pode ser instanciado dentro de outro.
O fluxo inteiro cabe em três passos: carregar o PackedScene, chamar instantiate() e pendurar o resultado na árvore com add_child(). O que separa o spawn que funciona do spawn cheio de bug é o detalhe em volta desses três passos: onde carregar, em quem pendurar, quando posicionar e como passar dados pra instância. É isso que esse tutorial cobre, com código Godot 4 que roda como está.
Como instanciar cena no Godot: o fluxo básico
Primeiro, o objeto que vai ser spawnado precisa ser uma cena própria, salva no projeto. Uma bala simples, por exemplo:
Bala (Area2D)
├── Sprite2D
└── CollisionShape2D
Salva como res://bala.tscn. Agora qualquer script consegue fabricar balas:
const BALA = preload("res://bala.tscn")
func atirar():
var bala = BALA.instantiate()
bala.global_position = global_position
get_tree().current_scene.add_child(bala)
Três linhas que valem destrinchar, porque cada uma esconde uma decisão.
preload vs load
preload carrega o arquivo em tempo de compilação do script, então o custo de leitura de disco acontece uma vez, quando a cena que contém o script entra na memória. load carrega na hora da chamada, em runtime.
# Carrega junto com o script. Caminho precisa ser string fixa.
const BALA = preload("res://bala.tscn")
# Carrega quando a linha executa. Aceita caminho dinâmico.
var cena = load("res://inimigos/" + nome + ".tscn")
Regra prática: preload pra tudo que você sabe que vai usar (bala, inimigo comum, partícula). load só quando o caminho é decidido em runtime ou quando o recurso é pesado e raro o suficiente pra não valer ocupar memória desde o início. Se você chamar load toda vez que atira, não vai travar o jogo, porque o Godot mantém cache de recursos carregados, mas é trabalho desnecessário num código que roda dezenas de vezes por segundo.
Existe uma terceira opção que eu uso bastante: expor a cena como @export e arrastar o .tscn pelo Inspector. O script fica desacoplado do caminho do arquivo:
@export var cena_bala: PackedScene
func atirar():
var bala = cena_bala.instantiate()
Se você renomear ou mover o arquivo da bala, o editor atualiza a referência sozinho. Com caminho em string, vira erro em runtime.
O que instantiate() devolve
instantiate() cria a árvore de nodes inteira da cena, com scripts anexados e valores do Inspector aplicados, e devolve o node raiz. Mas atenção: nesse momento o objeto existe só na memória. Ele não aparece na tela, não roda _process, não colide com nada. O _ready() dele ainda não rodou.
add_child() e a escolha do pai
O objeto só passa a existir de verdade quando entra na árvore de cena, e é o add_child() que faz isso. A pergunta importante é: filho de quem?
A resposta errada mais comum é pendurar a bala no próprio atirador:
# Armadilha: a bala vira filha da arma.
add_child(bala)
Filho herda a transformação do pai. Se a bala é filha do player e o player vira pra esquerda, a bala que estava voando pra direita vira junto. Se o inimigo morre e dá queue_free(), todas as balas que ele atirou somem no ar com ele.
Projétil e inimigo spawnado devem ser filhos de algo neutro: a cena do nível, ou um node Node2D que você cria só pra organizar os spawns:
# Opção 1: pendura na cena atual.
get_tree().current_scene.add_child(bala)
# Opção 2: pendura num container dedicado do nível.
get_node("/root/Nivel/Projeteis").add_child(bala)
E um detalhe de ordem que morde muita gente: sete a posição antes ou logo depois do add_child, sempre em global_position. Se você setar position antes de adicionar, o valor é interpretado como local ao futuro pai, o que pode ou não ser o que você quer. global_position depois do add_child não tem ambiguidade.
Spawnando projéteis
Juntando as peças num caso completo. O script da bala:
extends Area2D
var velocidade = 600.0
var dano = 1
func _physics_process(delta):
# transform.x é o vetor "pra frente" da bala, já considerando a rotação.
position += transform.x * velocidade * delta
func _on_body_entered(body):
if body.has_method("receber_dano"):
body.receber_dano(dano)
queue_free()
func _on_visible_on_screen_notifier_2d_screen_exited():
queue_free()
Dois sinais conectados pelo editor: o body_entered da própria Area2D, e o screen_exited de um node VisibleOnScreenNotifier2D filho da bala. O segundo é o que impede a bala perdida de viver pra sempre fora da tela acumulando custo. Esquecer esse detalhe é vazamento clássico: depois de dois minutos de tiroteio o jogo está simulando centenas de balas que ninguém vê.
No atirador, um Marker2D na ponta da arma marca de onde a bala sai:
extends CharacterBody2D
const BALA = preload("res://bala.tscn")
@onready var ponta_arma = $PontaArma # Marker2D
func _unhandled_input(event):
if event.is_action_pressed("atirar"):
atirar()
func atirar():
var bala = BALA.instantiate()
bala.global_position = ponta_arma.global_position
bala.rotation = rotation
get_tree().current_scene.add_child(bala)
A bala nasce na ponta da arma, apontando pra onde o personagem aponta, e segue a vida dela sem depender do atirador.
Spawnando inimigos com Timer
Spawn de inimigo é o mesmo mecanismo com um gatilho diferente: em vez de input do jogador, um Timer. A estrutura que eu monto em quase todo projeto:
Nivel (Node2D)
├── SpawnerInimigos (Node2D)
│ ├── Timer
│ ├── PontoSpawn1 (Marker2D)
│ ├── PontoSpawn2 (Marker2D)
│ └── PontoSpawn3 (Marker2D)
└── Inimigos (Node2D)
O script do spawner sorteia um ponto e fabrica o inimigo:
extends Node2D
@export var cena_inimigo: PackedScene
@export var maximo_vivos = 10
@onready var pontos = [$PontoSpawn1, $PontoSpawn2, $PontoSpawn3]
@onready var container = get_node("../Inimigos")
func _on_timer_timeout():
# Trava de população: sem isso o spawner enche a tela até derrubar o FPS.
if container.get_child_count() >= maximo_vivos:
return
var inimigo = cena_inimigo.instantiate()
inimigo.global_position = pontos.pick_random().global_position
container.add_child(inimigo)
O container Inimigos rende dois benefícios de graça. O limite de população vira uma contagem de filhos, sem precisar manter array manual sincronizado. E qualquer lógica de "afeta todos os inimigos" vira um loop simples:
for inimigo in container.get_children():
inimigo.receber_dano(999) # bomba que limpa a tela
Passando dados pra instância
Quase sempre o spawn precisa configurar a instância: inimigo mais forte na wave 5, bala com dano do upgrade atual. O timing aqui importa.
Propriedade setada depois do instantiate() e antes do add_child() já está com o valor certo quando o _ready() da instância rodar, porque o _ready() só dispara na entrada da árvore:
var inimigo = cena_inimigo.instantiate()
inimigo.vida_maxima = 50 + wave * 10
inimigo.velocidade = 120.0
container.add_child(inimigo) # _ready() roda aqui, com os valores prontos
Pra configuração com mais de dois ou três campos, prefiro um método explícito na cena instanciada, tipo func configurar(dados: Dictionary). O contrato fica visível em um lugar só, em vez de espalhado em atribuições soltas no spawner.
O que não funciona: chamar método que mexe em node filho da instância antes do add_child(). Os @onready da instância ainda não resolveram, então qualquer acesso a $Sprite2D dentro desse método quebra com null. Dados puros antes, manipulação de nodes depois.
Os dois erros que travam todo iniciante
"Parent node is busy setting up children"
Esse erro aparece quando você chama add_child() num momento em que a árvore está travada: dentro de um callback de colisão (body_entered), durante o flush de física, ou no meio do setup de outra cena. Caso típico: o inimigo morre numa colisão e quer spawnar a moeda do drop ali mesmo, dentro do callback.
A correção é adiar a chamada pro fim do frame com call_deferred:
func _on_body_entered(body):
var moeda = CENA_MOEDA.instantiate()
moeda.global_position = global_position
# Adia o add_child pra quando a árvore estiver liberada.
get_tree().current_scene.add_child.call_deferred(moeda)
queue_free()
call_deferred enfileira a chamada pra rodar quando o frame atual terminar de processar. A moeda aparece um instante depois, imperceptível pro jogador, e sem erro.
Instância órfã: instantiate() sem add_child()
Se o código instancia e algum caminho de execução sai da função sem chamar add_child(), o objeto fica órfão: existe na memória, não está na árvore, e nada vai liberar ele sozinho. É vazamento de memória silencioso, e o Godot avisa no fim da execução com mensagens de "ObjectDB instances leaked".
Pra caçar órfãos em runtime, o monitor do editor mostra a contagem em Debug > Performance Monitor, no item Orphan Nodes. Se o número só cresce enquanto você joga, tem instantiate() sem destino em algum lugar. Órfão que você criou de propósito (pra adicionar depois) você libera com free() se desistir dele; queue_free() também funciona, mas a regra é simples: toda instância criada precisa de um destino, árvore ou liberação.
Quando o spawn vira gargalo
Instanciar não é de graça: a engine monta a árvore de nodes, anexa scripts e aloca memória. Pra uma bala a cada meio segundo, irrelevante. Pra um bullet hell com centenas de projéteis por segundo, vira custo visível, com picos de frame na hora do spawn.
A técnica clássica é object pooling: criar um lote de balas no carregamento da fase, escondidas e desativadas, e em vez de instanciar e destruir, ligar e desligar. "Destruir" vira esconder e devolver pro pool; "spawnar" vira pegar do pool, reposicionar e mostrar.
Minha recomendação honesta: não comece por aí. Pooling adiciona estado e complexidade (bala precisa resetar tudo ao ser reusada, e bug de reset é chato de rastrear). O instantiate() puro aguenta muito mais do que parece, e a maioria dos jogos nunca chega no volume que justifica pool. Meça primeiro no Performance Monitor; otimize quando o número mandar, não por reflexo.
Fechando
Instanciar cena no Godot é um fluxo de três passos: preload do PackedScene, instantiate() pra fabricar, add_child() pra dar vida. Os bugs moram em volta: pendurar projétil no atirador em vez do nível, setar posição local em vez de global, mexer na árvore dentro de callback de física sem call_deferred, e deixar instância órfã sem destino.
Pra fixar, monta o par clássico: um atirador com Marker2D na ponta da arma e um spawner de inimigos com Timer e pontos de spawn. São duas cenas pequenas, e quando elas funcionam você entendeu o mecanismo que sustenta drop de item, partícula, popup de dano e qualquer outra coisa que nasce em tempo real no seu jogo. É o mesmo padrão em todo lugar, só muda o gatilho.


