Voltar para o Blog
Quest Log

Pathfinding 3D no Godot 4: NavigationAgent3D na Prática

Cenário 3D visto de cima com um caminho de navegação brilhante desviando de obstáculos até o personagem do jogador

Aprenda pathfinding 3D no Godot 4 com NavigationAgent3D: bake da NavigationMesh, perseguição em _physics_process, avoidance e debug do navmesh.

Fazer um inimigo perseguir o jogador num mapa 3D é o momento em que muito projeto trava: em linha reta funciona, mas basta uma parede no meio pra o bicho ficar esfregando a cara no obstáculo. A solução é pathfinding 3D no Godot, e a boa notícia é que o Godot 4 entrega o pacote completo com dois nós: NavigationRegion3D pro mapa e NavigationAgent3D pro inimigo. Eu já cobri o caso 2D em detalhe no post de pathfinding 2D no Godot, incluindo a teoria do A* que roda por baixo, então aqui não vou repetir o básico do algoritmo: o foco é o fluxo 3D de ponta a ponta, com as pegadinhas que só aparecem quando você sai do plano.

O plano de trabalho é direto: preparar o mapa com uma NavigationMesh, montar o inimigo com CharacterBody3D e NavigationAgent3D, escrever o loop de perseguição em _physics_process, e depois refinar com parada no destino, avoidance entre inimigos e debug visual. Tudo em GDScript tipado, Godot 4.x, sem nenhuma API inventada.

Preparando o mapa: NavigationRegion3D e o bake da NavigationMesh

Pathfinding não acontece em cima da geometria do mapa. Acontece em cima de uma malha simplificada chamada NavigationMesh (o famoso navmesh), que representa só as superfícies onde um agente pode pisar. O primeiro passo é gerar essa malha.

A estrutura de cena que funciona sem dor de cabeça:

Fase (Node3D)
└── NavigationRegion3D
    ├── Chao (MeshInstance3D + StaticBody3D)
    ├── Paredes (MeshInstance3D + StaticBody3D)
    └── Obstaculos (MeshInstance3D + StaticBody3D)

O NavigationRegion3D precisa ser pai (ou ancestral) da geometria que entra no cálculo. Selecione o nó, crie um recurso NavigationMesh novo no Inspector e clique no botão Bake NavigationMesh que aparece na barra do editor. O Godot varre a geometria filha e gera a malha: no viewport ela aparece como uma camada azulada por cima do chão. Tudo que estiver coberto de azul é área navegável; o resto não existe pro pathfinding.

Antes de sair do bake, quatro parâmetros do recurso NavigationMesh merecem sua atenção, porque são eles que decidem se o navmesh representa o seu jogo de verdade:

  • cell_size: a resolução da grade de voxels usada no bake. Menor = navmesh mais fiel a detalhes pequenos, bake mais lento. O padrão (0.25) atende a maioria dos mapas; só desça se portas e rampas estreitas estiverem saindo deformadas.
  • agent_radius: a largura do seu agente. O bake encolhe a área navegável por esse raio, garantindo que o centro do agente nunca chegue perto o bastante da parede pra atravessar o corpo. Se o seu inimigo tem 0.4 de raio de colisão, use algo próximo disso aqui.
  • agent_height: a altura do agente. Passagens com teto mais baixo que isso ficam fora do navmesh. É o que impede o caminho de passar por baixo de uma mesa que o inimigo não cabe.
  • agent_max_climb: o degrau máximo que o agente sobe. Escadas e meios-fios abaixo desse valor viram área contínua; acima disso, viram barreira.

Regra prática: esses valores devem espelhar o CollisionShape3D do inimigo. Se o navmesh diz que cabe e a colisão diz que não cabe, o inimigo vai ficar preso tentando seguir um caminho impossível.

Mudou a geometria do mapa? Refaça o bake. O navmesh não se atualiza sozinho no editor.

Pathfinding 3D no Godot na prática: o inimigo com NavigationAgent3D

Com o mapa pronto, o inimigo é um CharacterBody3D comum com um nó filho a mais:

Inimigo (CharacterBody3D)
├── CollisionShape3D
├── MeshInstance3D
└── NavigationAgent3D

O NavigationAgent3D não move nada sozinho. Ele é um consultor: você diz onde quer chegar (target_position), ele pergunta ao NavigationServer3D qual o caminho e te devolve um ponto de cada vez (get_next_path_position()). Mover o corpo continua sendo trabalho seu, com velocity e move_and_slide(), exatamente como você já faz desde o primeiro jogo 3D no Godot.

O script completo de perseguição:

extends CharacterBody3D

@export var velocidade: float = 4.0
@onready var agent: NavigationAgent3D = $NavigationAgent3D
@onready var jogador: Node3D = get_tree().get_first_node_in_group("jogador")

func _ready() -> void:
    await get_tree().physics_frame
    agent.target_position = jogador.global_position

func _physics_process(delta: float) -> void:
    if agent.is_navigation_finished():
        velocity = Vector3.ZERO
        return
    var proximo_ponto: Vector3 = agent.get_next_path_position()
    var direcao: Vector3 = global_position.direction_to(proximo_ponto)
    velocity = direcao * velocidade
    move_and_slide()

Três pontos desse código merecem explicação, porque cada um deles é uma pergunta recorrente de fórum.

O await get_tree().physics_frame no _ready não é enfeite. O mapa de navegação sincroniza com o NavigationServer3D no primeiro frame de física do jogo. Se você setar target_position direto no _ready, a consulta roda contra um mapa que ainda não existe, o agente não acha caminho nenhum e você perde uma hora achando que o bake está errado. Esperar um frame de física resolve.

get_next_path_position() devolve o próximo ponto, não o destino. O caminho calculado é uma sequência de pontos contornando os obstáculos. A cada frame você pede o próximo, anda na direção dele, e o agente avança o cursor internamente quando você chega perto o suficiente. Você nunca manipula a lista de pontos na mão.

A direção sai de direction_to, que já devolve o vetor normalizado. Multiplicou pela velocidade, jogou no velocity, chamou move_and_slide(). Se o seu jogo tem gravidade, some velocity.y à parte: calcule a direção no plano e preserve a componente vertical antes do move_and_slide().

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

Atualizando o alvo num Timer, não todo frame

O script acima seta o alvo uma vez só, então o inimigo persegue a posição onde o jogador estava no início. Pra perseguição de verdade, o alvo precisa ser atualizado. A tentação é fazer isso todo frame dentro do _physics_process, mas cada escrita em target_position dispara um recálculo de caminho, e com dez inimigos fazendo isso 60 vezes por segundo você paga caro por precisão que ninguém percebe.

Adicione um Timer como filho do inimigo (wait_time entre 0.2 e 0.5, autostart ligado) e conecte o timeout:

func _on_timer_timeout() -> void:
    agent.target_position = jogador.global_position

O jogador não se teleporta: em 0.3 segundo ele andou meio metro, e o caminho recalculado corrige a rota sem ninguém notar o atraso. É o mesmo padrão que uso no 2D, e continua sendo a otimização de melhor custo-benefício do pathfinding.

Parando no destino: navigation_finished e target_reached

O is_navigation_finished() no topo do _physics_process já impede o comportamento clássico de o inimigo chegar no alvo e ficar vibrando em cima dele, tentando alcançar um ponto que já alcançou. O agente considera a navegação encerrada quando entra no raio de target_desired_distance do destino.

Se você prefere reagir por evento em vez de checar todo frame, o NavigationAgent3D emite dois sinais úteis: target_reached, quando chega ao destino, e navigation_finished, quando o caminho termina (inclusive quando o destino é inalcançável e o agente para no ponto navegável mais próximo). É o gancho natural pra trocar de estado: chegou perto do jogador, sai de "perseguir" e entra em "atacar". Essa máquina de estados é assunto do post de inteligência artificial de inimigos no Godot; o pathfinding que montamos aqui é exatamente a peça de locomoção que aquele sistema de decisão comanda.

Ajuste fino que evita frustração: se o inimigo para longe demais do jogador, diminua target_desired_distance; se ele fica empurrando o jogador, aumente. E path_desired_distance controla o quão perto ele precisa chegar de cada ponto intermediário antes de mirar no próximo; valores muito baixos deixam o movimento robótico em curvas.

Avoidance: quando vários inimigos disputam o mesmo corredor

Com um inimigo, o script acima está completo. Com cinco perseguindo o mesmo jogador, todos calculam caminhos quase idênticos e viram um trenzinho se empurrando na porta. Pra isso existe o sistema de avoidance do NavigationAgent3D: cada agente enxerga os vizinhos e desvia a própria velocidade pra não colidir.

O fluxo muda num detalhe importante: em vez de aplicar a velocidade direto, você a entrega ao agente com set_velocity() e aplica só a versão segura que volta pelo sinal velocity_computed:

extends CharacterBody3D

@export var velocidade: float = 4.0
@onready var agent: NavigationAgent3D = $NavigationAgent3D
@onready var jogador: Node3D = get_tree().get_first_node_in_group("jogador")

func _ready() -> void:
    agent.avoidance_enabled = true
    agent.velocity_computed.connect(_on_velocity_computed)
    await get_tree().physics_frame
    agent.target_position = jogador.global_position

func _physics_process(delta: float) -> void:
    if agent.is_navigation_finished():
        velocity = Vector3.ZERO
        return
    var proximo_ponto: Vector3 = agent.get_next_path_position()
    var direcao: Vector3 = global_position.direction_to(proximo_ponto)
    agent.set_velocity(direcao * velocidade)

func _on_velocity_computed(safe_velocity: Vector3) -> void:
    velocity = safe_velocity
    move_and_slide()

O move_and_slide() migra pro callback: quem decide a velocidade final agora é o servidor de navegação, que considera todos os agentes com avoidance ligado e devolve um safe_velocity ajustado. No Inspector do agente, radius define o espaço pessoal de cada um e max_speed limita a correção.

Quando vale a pena? Quando você tem grupos de agentes se espremendo pelos mesmos gargalos. Pra um chefe solitário ou dois inimigos em salas separadas, avoidance é processamento gasto à toa: deixe avoidance_enabled em false e siga com a versão simples. E anote: avoidance desvia de outros agentes, não de paredes. Quem desvia de parede é o navmesh.

Debug: enxergando o que o agente enxerga

Quando o inimigo faz rota estranha ou empaca, não chute: olhe o navmesh. No editor, ative Debug > Visible Navigation no menu de debug e rode o jogo: a área navegável aparece desenhada em runtime, e nove entre dez bugs de pathfinding ficam óbvios na hora.

Os diagnósticos mais comuns:

  • Buraco no navmesh onde deveria ter chão: quase sempre agent_radius grande demais no bake. Uma porta de 1 metro com agent_radius de 0.6 simplesmente desaparece da malha, porque o centro do agente não passa. Reduza o raio ou alargue a passagem e refaça o bake.
  • Ilhas desconectadas: duas plataformas navegáveis sem ligação entre si. O agente para na beirada da ilha dele. Verifique se rampas e escadas entraram no bake e se o agent_max_climb cobre os degraus.
  • Caminho atravessando um obstáculo novo: o navmesh é estático depois do bake. Caixa que o jogador empurrou, porta que fechou, parede destruída: nada disso atualiza a malha sozinho. Pra obstáculos que se movem em runtime, o Godot oferece o NavigationObstacle3D, que recorta a área de avoidance ao redor do objeto; pra mudanças grandes de layout, o caminho é rebake em runtime ou regiões separadas que você ativa e desativa.

Também vale ligar o debug do próprio agente: a propriedade debug_enabled do NavigationAgent3D desenha o caminho atual do inimigo em jogo, ótimo pra ver exatamente qual rota ele escolheu.

Checklist final

O fluxo inteiro, de trás pra frente: NavigationRegion3D com a geometria como filha e NavigationMesh baked com cell_size, agent_radius, agent_height e agent_max_climb coerentes com o inimigo. CharacterBody3D com NavigationAgent3D filho. No _ready, await get_tree().physics_frame antes do primeiro target_position. Alvo atualizado por Timer, não por frame. No _physics_process, is_navigation_finished() como guarda, get_next_path_position() pra direção, move_and_slide() pra mover. Avoidance só quando há multidão, com set_velocity() e o sinal velocity_computed. E Visible Navigation ligado sempre que a rota parecer errada.

Com isso o seu inimigo contorna paredes, sobe rampas e chega no jogador por qualquer mapa que você fizer o bake. O próximo passo natural é dar cérebro pra esse corpo que já sabe andar: estados de patrulha, detecção e ataque por cima dessa locomoção. O pathfinding é a perna; a decisão de pra onde ir é o resto da IA.

Perguntas frequentes

Preciso programar A* pra fazer um inimigo perseguir o jogador em 3D no Godot?

Não. O Godot 4 já traz o algoritmo de busca de caminho pronto no NavigationServer3D. Você faz o bake de uma NavigationMesh no NavigationRegion3D, adiciona um NavigationAgent3D no inimigo e consome o caminho ponto a ponto com get_next_path_position(). Só vale implementar A* na mão em casos muito específicos, como grids customizados.

Por que o meu NavigationAgent3D não encontra caminho no primeiro frame?

Porque o mapa de navegação sincroniza com o NavigationServer3D no primeiro frame de física. Se você setar target_position dentro do _ready sem esperar, a consulta roda contra um mapa vazio. Use await get_tree().physics_frame no _ready antes de definir o primeiro alvo.

Preciso setar o target_position todo frame pra perseguir o jogador?

Não, e é melhor não fazer isso. Cada mudança de target_position dispara um recálculo do caminho. Atualize o alvo num Timer a cada 0.2 ou 0.5 segundos: o jogador não se teleporta, então o caminho continua válido entre as atualizações e você economiza CPU com vários inimigos na cena.

Pra que serve o avoidance_enabled do NavigationAgent3D?

Pra evitar que agentes se atropelem. Com avoidance ligado, você manda a velocidade desejada com set_velocity(), o NavigationServer3D calcula uma velocidade segura considerando os outros agentes e devolve pelo sinal velocity_computed. Vale a pena quando vários inimigos perseguem o mesmo alvo; pra um inimigo sozinho é custo à toa.

Por que o navmesh tem buracos perto de paredes e portas?

Quase sempre é o agent_radius da NavigationMesh. O bake encolhe a área navegável pela metade da largura do agente, então corredores e portas estreitas demais somem do navmesh. Reduza o agent_radius pro tamanho real do inimigo ou alargue a passagem no level design, e refaça o bake.