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.

Ferramenta:Godot

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. A perseguição de alvo já é uma forma simples de IA de jogo; para ir além do míssil e dar comportamento de verdade a inimigos, veja o guia de inteligência artificial para jogos na Godot.

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.

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

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.