Voltar para o Blog
Quest Log

Míssil Teleguiado na Godot 4: Projétil que Persegue o Alvo em 2D

Míssil 2D deixando rastro de fumaça enquanto faz uma curva para perseguir uma nave inimiga na Godot 4

Míssil teleguiado na Godot 4: crie um projétil 2D que persegue o alvo com lerp_angle, taxa de giro limitada, aquisição de alvo e explosão ao colidir.

Míssil teleguiado é daqueles efeitos que parecem exigir matemática pesada, mas na Godot 4 o núcleo cabe em umas dez linhas de GDScript. Neste tutorial você vai montar um míssil teleguiado 2D completo: a cena do projétil, a aquisição do inimigo mais próximo, a perseguição com taxa de giro limitada (o detalhe que separa um míssil justo de um míssil injusto), a perda de alvo, o tempo de vida com autodestruição e a explosão ao colidir. Todo o código é GDScript 4 tipado, sem API inventada.

Se você ainda não tem um projétil básico funcionando, vale ler antes como atirar projéteis na Godot. O míssil deste post é a evolução natural daquele projétil reto.

A matemática do míssil teleguiado sem mistério

O comportamento inteiro se resume a duas perguntas por frame de física:

  1. Para onde está o alvo? Subtraia posições: alvo.global_position - global_position dá o vetor do míssil até o alvo. O método angle() desse vetor devolve o ângulo dessa direção em radianos.
  2. Quanto eu posso girar neste frame? Aqui mora o design. Se você simplesmente fizer rotation = direcao.angle(), o míssil vira instantaneamente e nunca erra. Isso não é um míssil, é um raio laser com sprite de míssil.

A solução é limitar a taxa de giro: o míssil pode girar no máximo N radianos por segundo em direção ao alvo. Com giro limitado, o míssil descreve curvas com raio mínimo. Um jogador atento espera o míssil se aproximar e corta na perpendicular no último instante: a curva do míssil não fecha a tempo e ele passa direto. Esse é o clássico "esperar e cortar" de jogos de nave, e é o que torna o míssil esquivável e divertido em vez de uma sentença de morte.

A Godot te dá duas ferramentas prontas para isso:

  • rotate_toward(de, para, quantidade): gira do ângulo atual em direção ao ângulo desejado, no máximo quantidade radianos, sempre pelo caminho mais curto. Taxa constante, comportamento previsível. Disponível a partir da Godot 4.3.
  • lerp_angle(de, para, peso): interpola entre ângulos respeitando a volta em 2 PI. O giro é proporcional ao erro: rápido quando o míssil aponta longe do alvo, lento quando quase alinhado. Fica com cara de "suavização" e o raio de curva varia, o que dificulta balancear.

Para gameplay, prefira taxa constante. Mais adiante mostro um fallback com wrapf e clampf que funciona em qualquer Godot 4, caso você esteja em uma versão sem rotate_toward.

Montando a cena do míssil

Crie uma cena nova com esta estrutura:

  • Missil (Area2D): raiz. Area2D é a escolha certa aqui porque o míssil não precisa de física de corpo, só detectar sobreposição e explodir. Se o seu jogo exige que o míssil colida com paredes do cenário usando move_and_slide, troque por CharacterBody2D, o resto da lógica é igual.
  • Sprite2D: a arte do míssil. Desenhe (ou rotacione a textura) com o nariz apontando para a direita, porque rotation = 0 na Godot aponta para +X. Isso evita ficar somando 90 graus de correção no código.
  • CollisionShape2D: uma CapsuleShape2D deitada ou um RectangleShape2D fino cobre bem o corpo do míssil.
  • Timer: vai cuidar do tempo de vida. Marque one_shot como ligado.

Na aba de colisão do Area2D, configure as camadas: o míssil fica em uma camada própria de projéteis e a máscara aponta para a camada dos inimigos. Assim ele ignora o jogador e outros projéteis sem nenhum if extra.

Aquisição de alvo: o inimigo mais próximo

Coloque todos os inimigos no grupo inimigos (no editor, aba Node > Groups, ou via add_to_group("inimigos") no _ready do inimigo). Isso já é o padrão se você seguiu o tutorial de spawner de inimigos em waves, que usa o mesmo grupo para controlar a horda.

Para achar o mais próximo, compare distâncias com distance_squared_to em vez de distance_to. A distância ao quadrado preserva a ordem (se A está mais perto que B, o quadrado de A também é menor) e evita calcular raiz quadrada para cada inimigo, todo disparo:

func adquirir_alvo() -> void:
    var inimigos: Array[Node] = get_tree().get_nodes_in_group("inimigos")
    var mais_proximo: Node2D = null
    var menor_dist_sq: float = INF

    for inimigo: Node in inimigos:
        var inimigo_2d := inimigo as Node2D
        if inimigo_2d == null:
            continue
        var dist_sq: float = global_position.distance_squared_to(inimigo_2d.global_position)
        if dist_sq < menor_dist_sq:
            menor_dist_sq = dist_sq
            mais_proximo = inimigo_2d

    alvo = mais_proximo

Variações úteis: filtrar por um raio máximo de aquisição (if dist_sq > raio * raio: continue) ou por cone à frente do míssil (compare o dot da direção do míssil com a direção até o inimigo). Para um primeiro míssil, o mais próximo global resolve.

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

Perseguição no _physics_process

Aqui está o script completo do míssil. Cole em missil.gd na raiz Area2D:

extends Area2D

@export var velocidade: float = 320.0
@export var taxa_de_giro: float = 3.0
@export var tempo_de_vida: float = 4.0
@export var readquirir_ao_perder: bool = false

var alvo: Node2D = null

@onready var timer_vida: Timer = $Timer


func _ready() -> void:
    body_entered.connect(_ao_colidir_corpo)
    area_entered.connect(_ao_colidir_area)
    timer_vida.timeout.connect(explodir)
    timer_vida.start(tempo_de_vida)
    adquirir_alvo()


func _physics_process(delta: float) -> void:
    if alvo != null and not is_instance_valid(alvo):
        alvo = null
        if readquirir_ao_perder:
            adquirir_alvo()

    if alvo != null:
        var angulo_alvo: float = (alvo.global_position - global_position).angle()
        rotation = rotate_toward(rotation, angulo_alvo, taxa_de_giro * delta)

    global_position += Vector2.RIGHT.rotated(rotation) * velocidade * delta

Pontos que importam nesse código:

  • Velocidade constante, curva limitada. O míssil sempre anda velocidade pixels por segundo na direção em que o nariz aponta (Vector2.RIGHT.rotated(rotation)). A perseguição acontece só na rotação, nunca teleportando o vetor de movimento. É isso que produz aquelas curvas de perseguição bonitas em vez de um projétil grudento.
  • taxa_de_giro em radianos por segundo. Com 3.0, o míssil leva pouco mais de um segundo para dar meia-volta (PI radianos). Multiplicar por delta mantém o giro independente do framerate.
  • rotate_toward escolhe o caminho mais curto entre o ângulo atual e o desejado, tratando a passagem de 180 para -180 graus sem você se preocupar com wrap.

Se a sua versão da Godot 4 não tem rotate_toward (ela chegou na 4.3), o mesmo comportamento sai com funções que existem desde a 4.0:

var angulo_alvo: float = (alvo.global_position - global_position).angle()
var erro: float = wrapf(angulo_alvo - rotation, -PI, PI)
rotation += clampf(erro, -taxa_de_giro * delta, taxa_de_giro * delta)

wrapf normaliza o erro de ângulo para o intervalo -PI a PI (caminho mais curto) e clampf limita o giro do frame. E se você quiser o giro proporcional estilo suavização, a linha vira rotation = lerp_angle(rotation, angulo_alvo, 5.0 * delta). Teste os dois e sinta a diferença: o lerp_angle fecha curva agressivamente de longe e amolece de perto, o que costuma deixar o míssil ou fácil demais ou injusto demais dependendo da distância.

Perda de alvo: seguir reto ou readquirir

Quando o inimigo perseguido morre, a referência alvo passa a apontar para um objeto liberado da memória. Comparar com null não basta: um objeto liberado não é null, e acessar global_position nele gera erro. Por isso o script checa is_instance_valid(alvo) todo frame antes de usar a referência.

Ao perder o alvo, existem duas escolhas de design, e o @export var readquirir_ao_perder deixa você alternar por míssil:

  • Seguir reto (false): o míssil mantém a última direção e voa até o tempo de vida acabar. É o comportamento mais justo quando o míssil é inimigo, porque destruir a origem do perigo remove o perigo.
  • Readquirir (true): o míssil chama adquirir_alvo() de novo e passa a perseguir o próximo inimigo mais próximo. Ótimo para mísseis do jogador em um shmup, onde o jogador quer que cada disparo renda.

Tempo de vida e explosão ao colidir

Sem tempo de vida, um míssil que perdeu o alvo voa para sempre e vaza memória de cena. O nó Timer resolve: start(tempo_de_vida) no _ready e, no timeout, o míssil explode sozinho. Quatro segundos é um bom ponto de partida.

A colisão usa os dois sinais do Area2D: body_entered para inimigos que são CharacterBody2D ou RigidBody2D, e area_entered para inimigos ou hurtboxes que são Area2D. Complete o script com:

func _ao_colidir_corpo(corpo: Node2D) -> void:
    if corpo.is_in_group("inimigos"):
        if corpo.has_method("receber_dano"):
            corpo.receber_dano(3)
        explodir()


func _ao_colidir_area(area: Area2D) -> void:
    if area.is_in_group("inimigos"):
        explodir()


func explodir() -> void:
    set_physics_process(false)
    # Aqui entra o efeito: instancie uma cena de explosao,
    # toque um som, aplique dano em area se quiser.
    queue_free()

O set_physics_process(false) antes do queue_free evita que o míssil continue andando e colidindo no mesmo frame da morte. Se você quiser dano em área, a explosão vira uma segunda cena Area2D com raio maior, instanciada na posição do míssil antes do queue_free: o míssil morre, a explosão vive por alguns décimos de segundo e aplica dano em quem encostar.

Um rastro de fumaça fica ótimo com um GPUParticles2D filho emitindo para trás, ou um Line2D que vai acumulando pontos da trajetória. É polimento, não afeta a lógica.

Balanceamento: taxa de giro contra velocidade

A dupla velocidade e taxa_de_giro define sozinha a dificuldade de esquiva, e vale entender a relação:

  • Rápido com giro baixo (por exemplo 450 e 1.5): míssil letal em linha reta, mas qualquer curva do jogador o despista. Raio de curva enorme.
  • Lento com giro alto (por exemplo 200 e 5.0): persegue como cachorro, quase impossível despistar na curva, mas o jogador simplesmente corre mais que ele ou o leva para uma parede.
  • O meio termo (320 e 3.0): exige a manobra clássica de esperar chegar perto e cortar na perpendicular. É onde o míssil fica interessante.

Uma regra prática: o raio mínimo de curva do míssil é velocidade / taxa_de_giro em pixels. Com 320 e 3.0, cerca de 107 pixels. Se esse raio for menor que a hitbox do jogador somada à margem de reação, o míssil vira acerto garantido, então suba a velocidade ou desça o giro.

Daí saem as contramedidas como ideia de design: um item de flare pode spawnar um Node2D no grupo inimigos temporariamente (se o míssil for inimigo, o grupo do alvo do míssil) para roubar a aquisição; um dash do jogador pode zerar a aquisição dos mísseis próximos, forçando o seguir reto; ou o míssil pode ter giro que decai com o tempo, começando obediente e terminando teimoso. Todas essas mecânicas são só variações de mexer em alvo e taxa_de_giro em tempo real.

Próximos passos

Com o míssil funcionando, os encaixes naturais são:

O padrão que você montou aqui, apontar gradualmente para um alvo com giro limitado, serve para muito além de mísseis: inimigos voadores que perseguem, peixes que nadam até a isca, câmera que segue suave. A matemática é a mesma, muda o sprite.

Perguntas frequentes

Qual a diferença entre lerp_angle e rotate_toward na Godot 4?

lerp_angle interpola por proporção: o míssil gira rápido quando o erro de ângulo é grande e devagar quando é pequeno. rotate_toward gira uma quantidade fixa de radianos por chamada, o que dá taxa de giro constante e previsível. Para míssil teleguiado esquivável, prefira taxa constante (rotate_toward ou o fallback com wrapf e clampf).

Devo usar Area2D ou CharacterBody2D para o míssil teleguiado?

Para a maioria dos casos, Area2D basta: o míssil não precisa deslizar em paredes nem reagir a física, só detectar o toque e explodir. Use CharacterBody2D se quiser que o míssil colida com o cenário usando move_and_slide antes de chegar ao alvo.

Como fazer o míssil escolher o inimigo mais próximo?

Coloque os inimigos em um grupo chamado inimigos, percorra get_tree().get_nodes_in_group("inimigos") e compare distâncias com distance_squared_to, que evita a raiz quadrada e é mais barato. Guarde o nó com a menor distância como alvo.

O que acontece quando o alvo do míssil morre?

A referência aponta para um objeto liberado, então cheque is_instance_valid(alvo) todo frame. Ao perder o alvo, você escolhe o comportamento: seguir reto na direção atual (mais justo para o jogador) ou chamar a aquisição de novo e perseguir o próximo inimigo (mais agressivo).

Como deixar o míssil teleguiado possível de esquivar?

Limite a taxa de giro. Se o míssil é rápido mas faz curva fechada demais, ele nunca erra e vira frustração. Com giro limitado, o jogador espera o míssil chegar perto e corta na perpendicular: o raio de curva do míssil não acompanha e ele passa reto. Ajuste velocidade e taxa de giro em conjunto.