Voltar para o Blog
Quest Log

Como Fazer um Jogo de Nave (Shmup) na Godot: do Movimento ao Chefe

Nave espacial em jogo 2D estilo pixel art atirando projéteis contra inimigos que descem em formação no espaço, sem texto

Aprenda como fazer um jogo de nave (shmup) na Godot 4: movimento 8 direções, sistema de tiro, waves de inimigos, power-ups e polish, com GDScript tipado.

Como Fazer um Jogo de Nave (Shmup) na Godot: do Movimento ao Chefe

Se você quer aprender como fazer um jogo de nave, está escolhendo um dos projetos mais bem calibrados que existem para quem está começando. O shoot 'em up (ou shmup, para os íntimos) tem escopo pequeno, loop de jogo claro e resultado visível em poucas horas de trabalho. Neste guia a gente monta as peças de um jogo de nave espacial completo na Godot 4: movimento em 8 direções, sistema de tiro com cooldown, inimigos em waves, power-ups e aquele polish que separa um protótipo de um jogo gostoso de jogar.

Todo o código aqui é GDScript tipado e usa API real da Godot 4. Nada de pseudocódigo: você pode copiar, colar e ajustar os números.

Por que jogo de nave é um dos melhores primeiros projetos

Tem um motivo para Space Invaders (1978) ser um dos jogos que definiram a indústria: a fórmula é simples de entender e difícil de largar. Você atira, desvia e pontua. Fim. Não existe gravidade para acertar, não existe animação de caminhada, não existe level design de plataformas. A "fase" de um shmup é, na prática, uma sequência de inimigos aparecendo na tela.

Isso importa para você por três razões:

  1. Escopo que termina. Um shmup jogável cabe em um fim de semana. Projeto que termina ensina mais que projeto ambicioso abandonado.
  2. Loop claro desde o minuto um. Assim que a nave se move e atira, o jogo já "existe". Cada adição (inimigo, power-up, chefe) melhora algo que já funciona.
  3. Tudo que você aprende é transferível. Instanciar cenas, conectar sinais, usar Timer, detectar colisão com Area2D, controlar dificuldade: essas são as ferramentas de qualquer jogo 2D, de RPG a puzzle. O shmup só as apresenta na ordem certa.

Se você ainda está decidindo qual gênero atacar primeiro, vale ler o guia de gêneros de jogos explicados para comparar o custo de cada um. Spoiler: o jogo de nave está no grupo dos baratos.

Anatomia de um shmup: o que define o gênero

Antes de abrir a engine, entenda as peças que você vai construir.

Direção do scroll. Nos shmups verticais (Space Invaders, e a maioria dos bullet hells japoneses), a nave fica embaixo e os inimigos descem. Nos horizontais (Gradius, de 1985, é o clássico da Konami), a nave fica à esquerda e a fase corre para a direita. Vertical costuma ser mais simples para começar porque o espelhamento de sprites e os padrões de tiro ficam mais intuitivos. Este guia assume scroll vertical.

Padrões de tiro. O inimigo fraco atira reto. O médio atira mirando em você. O forte atira em leque ou espiral. A graça do gênero está em ler esses padrões e achar o espaço seguro entre as balas.

Waves de inimigos. Inimigos não aparecem aleatoriamente soltos: eles entram em ondas coreografadas, com formação, timing e ritmo. É isso que dá sensação de fase projetada em vez de caos.

Power-ups. Gradius popularizou a ideia de progressão dentro da fase: você coleta cápsulas e escolhe melhorias na barra de power-up. No seu primeiro shmup, basta um upgrade de tiro que faz o jogador se sentir mais forte.

Chefes. Todo shmup que se respeita fecha a fase com um chefe de padrões próprios. Ikaruga (da Treasure) leva isso ao extremo com a mecânica de polaridade, em que absorver tiros da cor certa é parte do combate. Você não precisa disso agora, mas é bom saber o teto do gênero.

Como fazer um jogo de nave se mover na Godot 4

Primeira decisão: qual node usar para a nave. As duas opções honestas são CharacterBody2D (movimento com move_and_slide e colisão física pronta) e Area2D (só detecção de sobreposição, sem física). Em shmup clássico a nave não empurra nada, então Area2D bastaria, mas CharacterBody2D dá colisão com bordas e obstáculos de graça e é o caminho que eu recomendo para iniciante.

Monte a cena da nave assim: um CharacterBody2D chamado Jogador, com um Sprite2D e um CollisionShape2D como filhos. Antes do script, abra Project Settings, aba Input Map, e crie as ações mover_esquerda, mover_direita, mover_cima, mover_baixo e atirar, mapeando setas ou WASD e a barra de espaço.

O movimento em 8 direções na Godot 4 se resolve com uma única chamada, Input.get_vector, que já devolve o vetor normalizado (andar na diagonal não fica mais rápido):

extends CharacterBody2D

@export var velocidade: float = 400.0
@export var margem: float = 24.0

func _physics_process(delta: float) -> void:
    var direcao: Vector2 = Input.get_vector("mover_esquerda", "mover_direita", "mover_cima", "mover_baixo")
    velocity = direcao * velocidade
    move_and_slide()

    var tela: Rect2 = get_viewport_rect()
    var minimo: Vector2 = tela.position + Vector2(margem, margem)
    var maximo: Vector2 = tela.end - Vector2(margem, margem)
    position = position.clamp(minimo, maximo)

Três detalhes que valem nota. O código roda em _physics_process, que tem passo fixo, então a nave se move igual em qualquer máquina. O @export deixa a velocidade editável no Inspector, o que acelera muito o ajuste fino. E o position.clamp com get_viewport_rect() tranca a nave dentro da tela, com uma margem para o sprite não cortar na borda. Rode a cena: você já tem uma nave pilotável em menos de vinte linhas.

Sistema de tiro: projétil, cooldown e cadência

O tiro em Godot segue um padrão que você vai repetir a vida inteira: o projétil é uma cena separada, e a nave instancia essa cena. Crie uma cena nova com um Area2D na raiz (chame de Projetil), um Sprite2D e um CollisionShape2D. Adicione também um VisibleOnScreenNotifier2D para destruir o tiro quando ele sair da tela, senão os projéteis se acumulam para sempre na memória.

Script do projétil:

extends Area2D

@export var velocidade: float = 700.0
@export var dano: int = 1

func _ready() -> void:
    area_entered.connect(_ao_atingir)
    $VisibleOnScreenNotifier2D.screen_exited.connect(queue_free)

func _physics_process(delta: float) -> void:
    position.y -= velocidade * delta

func _ao_atingir(alvo: Area2D) -> void:
    if alvo.is_in_group("inimigos"):
        alvo.receber_dano(dano)
        queue_free()

O sinal area_entered dispara quando o projétil sobrepõe outra Area2D (o inimigo, no nosso caso). Se você fizer inimigos com corpo físico, o sinal equivalente é body_entered. O grupo inimigos evita que o tiro se destrua ao encostar em power-ups ou em outros tiros.

Na nave, o cooldown de tiro é um node Timer (chame de CooldownTiro, marque One Shot e defina o Wait Time, algo como 0.2 segundos). Adicione também um Marker2D chamado PontoDeTiro na frente da nave. O trecho de tiro entra no mesmo script do movimento:

@export var cena_projetil: PackedScene
@onready var cooldown_tiro: Timer = $CooldownTiro
@onready var ponto_de_tiro: Marker2D = $PontoDeTiro

func _process(_delta: float) -> void:
    if Input.is_action_pressed("atirar") and cooldown_tiro.is_stopped():
        atirar()

func atirar() -> void:
    var projetil: Area2D = cena_projetil.instantiate()
    projetil.global_position = ponto_de_tiro.global_position
    get_tree().current_scene.add_child(projetil)
    cooldown_tiro.start()

A lógica é limpa: segurou o botão e o Timer está parado, atira e reinicia o Timer. Diminuir o Wait Time aumenta a cadência, o que já é seu primeiro power-up em potencial. Uma observação de desempenho: instanciar e destruir projéteis assim funciona muito bem para um shmup normal. Se um dia você for para o território bullet hell, com milhares de tiros na tela, a otimização clássica é object pooling (criar os projéteis uma vez e reciclar em vez de destruir). Eu detalho essa e outras variações no guia de sistema de tiro e projéteis.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

Inimigos e waves: o coração do desafio

O inimigo mais simples é um Area2D com sprite, colisão e um script que o move para baixo. Adicione ele ao grupo inimigos (aba Node, seção Groups, ou via código) para o projétil reconhecê-lo. O pulo de qualidade vem com o segundo padrão de movimento: a senoide. Descer reto é previsível; descer serpenteando obriga o jogador a mirar de verdade.

extends Area2D

@export var velocidade_descida: float = 150.0
@export var amplitude: float = 80.0
@export var frequencia: float = 3.0
@export var vida: int = 2

var tempo_vivo: float = 0.0
var x_inicial: float = 0.0

func _ready() -> void:
    x_inicial = position.x

func _physics_process(delta: float) -> void:
    tempo_vivo += delta
    position.y += velocidade_descida * delta
    position.x = x_inicial + sin(tempo_vivo * frequencia) * amplitude

func receber_dano(quantidade: int) -> void:
    vida -= quantidade
    if vida <= 0:
        queue_free()

A mágica está em sin(tempo_vivo * frequencia) * amplitude: o seno oscila entre -1 e 1, a amplitude vira a largura do zigue-zague em pixels e a frequência controla a rapidez da oscilação. Com dois @export você cria três inimigos diferentes do mesmo script (reto com amplitude 0, serpente larga, serpente nervosa).

Para colocar esses inimigos em jogo, você precisa de um spawner: um node com um Timer que instancia inimigos em posições e ritmos controlados.

extends Node2D

@export var cena_inimigo: PackedScene
@onready var timer_spawn: Timer = $TimerSpawn

func _ready() -> void:
    timer_spawn.timeout.connect(_spawnar_inimigo)
    timer_spawn.start()

func _spawnar_inimigo() -> void:
    var inimigo: Area2D = cena_inimigo.instantiate()
    var largura: float = get_viewport_rect().size.x
    inimigo.position = Vector2(randf_range(32.0, largura - 32.0), -48.0)
    add_child(inimigo)

Isso gera um fluxo constante, que serve para testar. Um jogo de verdade agrupa inimigos em ondas com formação e pausa entre elas, o que muda completamente a sensação de ritmo. A arquitetura completa disso, com waves configuráveis por Resource, está no tutorial de spawner de inimigos em waves.

Power-ups e progressão de dificuldade

Um power-up é mecanicamente igual a um inimigo que não machuca: uma Area2D que desce a tela e, ao tocar o jogador, aplica um efeito e some. Três upgrades clássicos e baratos de implementar:

  • Cadência: reduza o wait_time do CooldownTiro (com um mínimo, senão vira laser).
  • Tiro duplo: adicione um segundo Marker2D e instancie dois projéteis no atirar().
  • Escudo: uma variável tem_escudo: bool que absorve o próximo dano.

O truque de design é fazer o power-up cair de inimigos abatidos, não aparecer do nada. Isso cria a decisão que move o gênero inteiro: vale a pena atravessar a chuva de balas para pegar o item?

Já a progressão de dificuldade pode ser embaraçosamente simples: a cada wave, multiplique a velocidade dos inimigos por 1.05 e reduza o intervalo do spawner em 5 por cento. Guarde esses valores em variáveis do spawner e aumente um contador wave_atual: int a cada onda. Em dez minutos de jogo a curva já aperta sem você desenhar fase nenhuma. Só estabeleça um teto, ou o jogo vira injogável em vez de difícil.

Polish que transforma o jogo: shake, partículas e som

A diferença entre "protótipo funcional" e "jogo gostoso" quase nunca é mecânica nova. São três coisas pequenas:

Screen shake. Quando um inimigo explode, sacuda a câmera por um décimo de segundo. Na prática: uma Camera2D na cena, e a cada frame do shake você aplica um offset aleatório pequeno (algo como Vector2(randf_range(-4.0, 4.0), randf_range(-4.0, 4.0))) que decai até zero. Explosão sem shake parece que aconteceu em outro jogo.

Partículas. Um GPUParticles2D com One Shot ligado, meia dúzia de fragmentos laranja e lifetime de meio segundo já vende a explosão. Instancie no lugar do inimigo destruído, do mesmo jeito que você instancia projéteis.

Som. Um AudioStreamPlayer para o tiro (com pitch levemente aleatório via pitch_scale, para não hipnotizar) e outro para a explosão. Sites como freesound.org e o gerador sfxr resolvem os efeitos do primeiro projeto sem custo.

Regra prática: implemente os três no mesmo dia em que o primeiro inimigo morrer. O retorno por hora investida é maior que o de qualquer mecânica extra.

Próximos passos

Com nave, tiro, waves, power-ups e polish, falta o gran finale: o chefe. Um chefe de shmup é um inimigo com muita vida, fases de comportamento (padrões de tiro que mudam conforme a vida cai) e telegraph claro antes de cada ataque. É um assunto com profundidade própria, e o guia de design de boss fight cobre estrutura de fases, telegraphs e barras de vida em detalhe.

Depois disso, o caminho natural do gênero: placar com high score persistente, segunda fase com inimigos novos, um padrão de tiro em leque para inimigos avançados e, se o bicho pegar, o mergulho em bullet hell com object pooling.

O mais importante: termine. Um jogo de nave com uma fase, cinco inimigos e um chefe é um jogo completo no seu portfólio, e as ferramentas que você usou (cenas instanciadas, sinais, Timers, grupos, Area2D) são exatamente as mesmas que você vai usar em qualquer projeto 2D daqui para frente. O shmup não é um desvio do caminho. Ele é o caminho.

Perguntas frequentes

Qual node usar para a nave do jogador na Godot 4?

CharacterBody2D é a escolha mais prática: você define a velocity e chama move_and_slide(), e ainda ganha colisão com o cenário de graça. Area2D também funciona em shmups clássicos, porque a nave não empurra nada, só detecta sobreposição com tiros e inimigos. Se está em dúvida, vá de CharacterBody2D.

Como fazer a nave atirar na Godot 4?

Crie o projétil como uma cena separada (Area2D com sprite e colisão), exporte um PackedScene no script da nave e chame instantiate() quando o jogador apertar o botão. Um node Timer controla o cooldown: só atira de novo se o Timer estiver parado, o que define a cadência do tiro.

O que significa shmup?

Shmup é a abreviação de shoot em up, o gênero de jogos de nave em que você atravessa fases atirando em ondas de inimigos e desviando de projéteis. Space Invaders, Gradius e Ikaruga são exemplos clássicos. O termo cobre tanto scroll vertical quanto horizontal.

Jogo de nave é um bom primeiro projeto?

É um dos melhores. O escopo é pequeno, o loop de jogo é claro (atirar, desviar, pontuar) e não precisa de gravidade, animação de personagem nem level design complexo. Tudo que você aprende fazendo um shmup (instanciar cenas, sinais, Timers, colisão) serve para qualquer outro jogo 2D.

Como manter a nave dentro da tela?

Depois de mover a nave, aplique clamp na posição usando os limites de get_viewport_rect(). O método position.clamp(minimo, maximo) trava o Vector2 dentro do retângulo da tela. Deixe uma margem do tamanho de meio sprite para a nave não cortar na borda.

Preciso de object pooling num jogo de nave?

No começo, não. Instanciar e dar queue_free() em projéteis funciona bem até centenas de objetos por segundo. Pooling (criar os projéteis uma vez e reciclar) vira otimização relevante em bullet hells com milhares de tiros simultâneos. Faça funcionar primeiro, otimize depois se o profiler apontar necessidade.