Voltar para o Blog
Quest Log

Pathfinding no Godot com Navigation2D: NavigationAgent2D e A* na Prática

Inimigo percorrendo um caminho calculado em um labirinto 2D no Godot Engine

Aprenda pathfinding no Godot com NavigationAgent2D, NavigationRegion2D e A* nativo. Tutorial prático: inimigo que persegue o player no Godot 4.

Pathfinding no Godot com Navigation2D: NavigationAgent2D e A* na Prática

Pathfinding no Godot é o que separa um inimigo que persegue o player de verdade de um inimigo que anda reto e fica preso na primeira parede. A boa notícia: você não precisa implementar A* na mão. O Godot 4 traz dois sistemas prontos, o de navegação por malha (navmesh) e as classes de A* nativo, e os dois resolvem o problema com pouco código.

A confusão comum é o nome. No Godot 3 existia um node chamado Navigation2D, e muito tutorial antigo ainda gira em torno dele. No Godot 4 esse node não existe mais: o trabalho passou pro NavigationServer2D, que roda por baixo dos panos, e você interage com ele através de dois nodes: NavigationRegion2D (define onde dá pra andar) e NavigationAgent2D (calcula e segue o caminho). Se você está seguindo um tutorial que manda adicionar um node Navigation2D e ele não aparece na busca, é porque o tutorial é de Godot 3.

Esse artigo monta o caso de uso mais comum, um inimigo que persegue o player desviando de obstáculos, e depois mostra quando o A* nativo (AStar2D e AStarGrid2D) é a escolha melhor. Todo código é GDScript de Godot 4.x e roda como está.

Como funciona o pathfinding no Godot 4

O sistema de navegação tem três peças:

NavigationServer2D: o servidor que mantém o mapa de navegação e responde consultas de caminho. Você raramente chama ele direto; os nodes fazem isso por você.

NavigationRegion2D: o node que diz "essa área é caminhável". Ele guarda um NavigationPolygon, a malha de navegação. Tudo que estiver dentro do polígono é chão válido; tudo fora é parede.

NavigationAgent2D: o node que vai como filho do seu personagem. Você dá um destino (target_position) e ele devolve, frame a frame, o próximo ponto do caminho. Ele também cuida de repath (recalcular quando o alvo se move) e de desvio entre agentes.

O fluxo é sempre o mesmo: a região define o mapa, o agente consulta o mapa, o seu script move o corpo na direção que o agente indicar. O agente não move nada sozinho, e isso é de propósito: o movimento continua sendo seu, com move_and_slide() e tudo que você já usa.

Montando o mapa de navegação com NavigationRegion2D

Antes de qualquer perseguição, o inimigo precisa saber onde pode pisar.

Desenhando o polígono na mão

O caminho mais direto pra uma cena pequena:

  1. Adicione um node NavigationRegion2D na cena.
  2. No Inspector, crie um recurso NavigationPolygon novo no campo Navigation Polygon.
  3. Com o node selecionado, a toolbar do editor 2D mostra as ferramentas de desenho. Clique pra adicionar pontos e feche o contorno da área caminhável.

Pra abrir buracos (a base de um pilar, uma mesa), desenhe um segundo contorno dentro do primeiro. Contorno interno vira área proibida.

Bake a partir da geometria

Desenhar na mão não escala. A partir do Godot 4.2, o NavigationRegion2D tem o botão Bake NavigationPolygon na toolbar: ele varre a geometria da cena (colisores de StaticBody2D, polígonos, conforme o que você configurar no recurso) e gera a malha sozinho, já recortando os obstáculos.

Um detalhe que muda tudo na prática: o campo Agent Radius do NavigationPolygon. Ele encolhe a malha pra dentro pela largura do agente, então o caminho nunca passa colado na parede e o corpo do inimigo não fica raspando em quina. Se o seu inimigo tem uma CollisionShape de raio 16, coloque um valor próximo disso e os caminhos saem limpos.

TileMap

Se o seu mapa é feito de tiles, dá pra pular o desenho manual: o TileSet aceita camadas de navegação, e você pinta o polígono de navegação direto nos tiles caminháveis (na aba de edição do TileSet). O TileMap alimenta o NavigationServer2D automaticamente, sem NavigationRegion2D nenhum na cena. Pra mapa baseado em grid, é o jeito que eu recomendo: o navmesh cresce junto com o level sem trabalho extra.

Com o mapa pronto, o inimigo é um CharacterBody2D com um NavigationAgent2D de filho:

Inimigo (CharacterBody2D)
├── CollisionShape2D
├── Sprite2D
└── NavigationAgent2D

O script de perseguição inteiro:

extends CharacterBody2D

const SPEED = 150.0

@onready var agent: NavigationAgent2D = $NavigationAgent2D

var player: Node2D

func _ready():
    player = get_tree().get_first_node_in_group("player")

func _physics_process(_delta):
    agent.target_position = player.global_position

    if agent.is_navigation_finished():
        return

    var proximo_ponto = agent.get_next_path_position()
    velocity = global_position.direction_to(proximo_ponto) * SPEED
    move_and_slide()

A lógica em três linhas: atualiza o destino, pergunta ao agente qual o próximo ponto do caminho, move na direção dele. O is_navigation_finished() evita o inimigo ficar trepidando em cima do player quando já chegou.

Uma pegadinha de primeiro frame: o mapa de navegação só sincroniza com o NavigationServer2D depois do primeiro frame de física. Se você setar target_position dentro do _ready() e consultar o caminho na mesma hora, vem caminho vazio e um erro no console. Quando precisar consultar logo na criação da cena, espere um frame de física:

func _ready():
    await get_tree().physics_frame
    agent.target_position = destino_inicial

No script de perseguição acima isso não aparece porque o destino é setado todo frame dentro do _physics_process, então o primeiro frame perdido não faz diferença.

Os ajustes que mudam o comportamento

Três propriedades do NavigationAgent2D resolvem a maioria dos ajustes finos, e todas ficam no Inspector:

  • Path Desired Distance: a que distância de um ponto intermediário do caminho o agente considera "cheguei, próximo ponto". Valor baixo demais faz o inimigo orbitar um ponto sem nunca alcançá-lo, principalmente se a velocidade é alta.
  • Target Desired Distance: o mesmo, mas pro destino final. É o que controla a que distância do player o inimigo para. Pra um inimigo de ataque corpo a corpo, esse valor é praticamente o alcance do ataque.
  • Path Max Distance: o quanto o agente pode se afastar do caminho antes de forçar um recálculo. Útil quando o inimigo é empurrado por física pra fora da rota.

Se o inimigo treme ou roda em círculos perto do destino, o ajuste quase sempre é subir essas distâncias. Velocidade alta com tolerância baixa é a receita clássica do inimigo que dança em volta do alvo.

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

Desvio entre agentes: avoidance

Com cinco inimigos perseguindo o player, todos vão calcular caminhos parecidos e virar um bolo de sprites se atropelando. O NavigationAgent2D tem desvio dinâmico (avoidance) pra isso, usando o algoritmo RVO por baixo.

Ligue Avoidance Enabled no Inspector do agente, configure Radius com o tamanho do corpo e Max Speed com a velocidade máxima. Com avoidance ligado, o fluxo do código muda: em vez de aplicar a velocidade direto, você entrega a velocidade desejada pro agente e move com a velocidade segura que ele devolve pelo sinal velocity_computed:

extends CharacterBody2D

const SPEED = 150.0

@onready var agent: NavigationAgent2D = $NavigationAgent2D

var player: Node2D

func _ready():
    player = get_tree().get_first_node_in_group("player")
    agent.velocity_computed.connect(_on_velocity_computed)

func _physics_process(_delta):
    agent.target_position = player.global_position

    if agent.is_navigation_finished():
        return

    var proximo_ponto = agent.get_next_path_position()
    # Entrega a intenção de movimento; o agente devolve a versão segura.
    agent.velocity = global_position.direction_to(proximo_ponto) * SPEED

func _on_velocity_computed(safe_velocity: Vector2):
    velocity = safe_velocity
    move_and_slide()

Cada agente passa a considerar os outros e contorna em vez de empilhar. Importante entender o limite: avoidance desvia de outros agentes e de NavigationObstacle2D, não de parede. Parede é trabalho do navmesh. Se os inimigos atravessam parede com avoidance ligado, o problema é a malha, não o desvio.

A* nativo: AStar2D e AStarGrid2D

O navmesh é a escolha certa pra movimento livre em espaço contínuo. Mas tem jogo onde o movimento é de célula em célula: roguelike, tático por turnos, tower defense em grid. Nesses casos o A* nativo é mais simples e te dá controle total.

A classe AStar2D trabalha com pontos e conexões arbitrárias que você cadastra um a um, e serve pra grafos irregulares (waypoints, rotas de patrulha). Pra grid, a AStarGrid2D é bem mais prática: você define a região e ela monta as conexões sozinha.

var grid = AStarGrid2D.new()

func _ready():
    grid.region = Rect2i(0, 0, 32, 32)      # grid de 32x32 células
    grid.cell_size = Vector2(64, 64)        # cada célula com 64x64 pixels
    grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER
    grid.update()  # obrigatório depois de mudar region ou cell_size

    # Marca paredes como intransitáveis.
    grid.set_point_solid(Vector2i(5, 5), true)
    grid.set_point_solid(Vector2i(5, 6), true)

func caminho_entre(de: Vector2i, ate: Vector2i) -> PackedVector2Array:
    return grid.get_point_path(de, ate)

O get_point_path() devolve as posições em pixels (já convertidas pelo cell_size), prontas pra mover o personagem ou desenhar o preview do caminho, aquele rastro de células destacadas de jogo tático. O diagonal_mode merece atenção: DIAGONAL_MODE_NEVER força movimento só em cruz, e DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES permite diagonal sem deixar o personagem cortar quina de parede.

A regra de decisão que eu uso: movimento livre com obstáculos de formato qualquer, navmesh com NavigationAgent2D. Movimento em células ou custo de terreno controlado na mão, AStarGrid2D. Tentar forçar grid em cima de navmesh, ou espaço contínuo em cima de A* de pontos, gera mais código e mais bug que escolher o sistema certo desde o início.

Debug: enxergando o caminho

Quando o inimigo não anda, a primeira pergunta é sempre: o mapa existe onde eu acho que existe?

Visible Navigation. No menu do editor, Debug > Visible Navigation. Roda o jogo e as áreas caminháveis aparecem pintadas. Metade dos problemas de pathfinding morre aqui: a malha não cobre a área, o bake não recortou um obstáculo, ou o Agent Radius encolheu um corredor até fechar a passagem.

Desenhar o caminho do agente. Pra ver a rota calculada em jogo, pegue o caminho atual e desenhe:

func _process(_delta):
    queue_redraw()

func _draw():
    var caminho = agent.get_current_navigation_path()
    for i in range(caminho.size() - 1):
        draw_line(to_local(caminho[i]), to_local(caminho[i + 1]), Color.CYAN, 2)

Com o caminho visível, fica óbvio se o problema é o cálculo (rota errada ou vazia) ou o movimento (rota certa, corpo que não segue). São bugs de causas completamente diferentes e o desenho separa os dois em segundos.

Fechando

Pathfinding no Godot 4 se resume a uma escolha e três nodes. A escolha: navmesh pra espaço contínuo, AStarGrid2D pra grid. Os nodes: NavigationRegion2D define onde dá pra andar, NavigationAgent2D calcula e acompanha o caminho, e o seu CharacterBody2D continua mandando no movimento.

O esqueleto de perseguição tem menos de vinte linhas, e é nele que eu sugiro começar: um player, um inimigo, uma região desenhada na mão. Quando isso funcionar, acrescente o bake automático, depois o avoidance com três ou quatro inimigos ao mesmo tempo. Cada etapa expõe um pedaço do sistema, e no fim você sabe exatamente qual peça ajustar quando a IA do seu jogo fizer algo estranho. Que vai fazer. IA de jogo sempre faz.