Scripting Lua

Pathfinding no Roblox: ligue a malha de navegação antes de escrever código

Quase todo bug de NPC que não anda direito é visível antes de qualquer script: o Studio desenha a malha de navegação e as setas de pulo. Quem liga isso primeiro depura com os olhos.

por Gustavo Cantino9 min de leituraIntermediário

Produzido com IA a partir das fontes citadas abaixo, e conferido por checagens automáticas contra essas mesmas fontes. Nenhuma pessoa leu este texto antes da publicação. Política Editorial

Placa tipográfica com a categoria Scripting Lua em destaque, o nível intermediário abaixo, e um campo de barras verticais verde-limão sobre fundo escuro.

NPC que anda contra a parede, que para no meio do caminho, que pula onde não deveria pular — a lista de sintomas de pathfinding quebrado é longa, e a reação padrão é abrir o script e encher de print.

Na maior parte das vezes, o problema é visível antes de qualquer código. O Studio desenha a malha de navegação na tela, e quem liga isso primeiro depura com os olhos.

O problema

A definição oficial já entrega a estrutura do assunto: pathfinding é mover um personagem ou objeto — o agente — por um caminho lógico em volta de obstáculos até um destino, opcionalmente evitando materiais perigosos ou regiões definidas.

Três partes, e cada uma é um lugar onde algo pode dar errado. O agente tem um corpo com tamanho. Os obstáculos definem por onde dá para passar. E os materiais e regiões definem por onde vale a pena passar, o que é diferente de poder.

A diferença entre isso e mover o NPC na direção do alvo é só uma: o que acontece quando existe uma parede. MoveTo na direção do alvo anda contra a parede para sempre; o caminho calculado dá a volta. Todo o resto da complexidade existe para sustentar essa única diferença.

E o motor não calcula o caminho olhando o seu cenário como você o vê. Ele usa uma malha de navegação, gerada a partir da geometria, que é a representação do que é chão andável. Quando o NPC se comporta de um jeito que não faz sentido, é quase sempre porque a malha discorda de você sobre o que é chão.

Passo a passo

1. Ligue a visualização antes de tudo. O Studio consegue desenhar a malha de navegação e os rótulos de modificador. As opções ficam no widget de Visualization Options, no canto superior direito do viewport 3D.

Com a malha ligada, as áreas coloridas mostram onde o personagem pode andar ou nadar. E as setinhas indicam áreas que o personagem vai tentar alcançar pulando.

Esses dois sinais respondem, de olho, as duas perguntas que mais consomem tempo: "por que ele não vai até ali?" — porque ali não é área colorida — e "por que ele pulou?" — porque há uma seta ali.

Este passo é primeiro de propósito. Depurar pathfinding sem a malha ligada é depurar um algoritmo cujo dado de entrada você não está vendo.

2. Crie o caminho com os parâmetros do agente. O pathfinding se implementa pelo PathfindingService e métodos como o CreatePath. O exemplo oficial cria o caminho com AgentCanClimb ligado e um custo para a água.

Os parâmetros do agente descrevem o corpo que vai andar: raio, altura, se pula, se escala. Eles não são enfeite — é com eles que o cálculo decide se o agente cabe numa passagem.

3. Use custos em vez de proibições. O Costs da criação do caminho é o mecanismo mais subutilizado. Ele não diz "não passe"; diz "passar aqui custa mais". Um custo alto na água faz o agente preferir o caminho seco quando existe um, e ainda assim atravessar quando não existe.

É a diferença entre um NPC que dá a volta no lago e um NPC que desiste porque o único caminho passava por ele.

4. Reaja ao bloqueio. Caminho calculado é uma foto do mundo naquele instante. Se uma porta fecha, se um jogador põe um bloco, se outro NPC para no meio — o caminho continua válido no papel e errado no mundo.

5. Para testar com o personagem do jogador, como faz o exemplo da documentação, é preciso pôr DevComputerMovementMode e DevTouchMovementMode em Scriptable, e o código vai num LocalScript dentro de StarterCharacterScripts. Sem isso, o controle do jogador e o script disputam o mesmo personagem.

Esse detalhe merece atenção porque ele explica um resultado confuso. Sem pôr os dois modos em Scriptable, o script de pathfinding roda, calcula tudo certo, manda o personagem andar — e o sistema de controle padrão manda ele parar no quadro seguinte. O personagem treme no lugar, e não há nada de errado com o caminho.

Vale também entender por que o exemplo oficial usa o personagem do jogador em vez de um NPC, o que parece uma escolha estranha para uma página sobre mover personagens. O motivo é didático: com o jogador, dá para ver de dentro o caminho sendo percorrido e sentir quando o movimento fica errado. Para código de produção, o mesmo padrão vale para NPC, do lado do servidor, e sem mexer em nenhum modo de movimento.

Artefato

O script abaixo é um NPC que persegue um alvo, e ele existe para mostrar as três coisas que quase todo exemplo simples de pathfinding deixa de fora.

A primeira: recalcular quando o caminho é bloqueado. O evento existe, e sem ele o NPC caminha confiante para uma parede que apareceu depois.

A segunda: recalcular quando o alvo andou. Perseguir um jogador com um caminho calculado há dez segundos é perseguir onde ele estava.

A terceira, e a que mais salva: desistir de um ponto que demora demais. Um NPC preso num canto, esperando um MoveToFinished que nunca vem, trava a rotina inteira e nunca mais anda. Com prazo, ele desiste, recalcula, e o jogador nunca vê o problema.

Erro comum

O erro mais comum é assumir que a malha de navegação é o cenário. Ela não é — é uma derivação dele, e há coisas que a fazem discordar do que você vê.

O sintoma clássico: o NPC se recusa a entrar numa área que claramente tem chão. Com a visualização ligada, o motivo aparece em um segundo — aquela área não está colorida. Pode ser geometria fina demais, pode ser uma parte sem colisão, pode ser um teto baixo que não dá a altura do agente. Sem a visualização, isso vira meia hora de hipóteses.

O segundo erro é sobre o tamanho do agente. Os parâmetros padrão descrevem um personagem padrão. Um NPC maior que isso recebe caminhos que passam por vãos onde ele não cabe — e então ele trava no vão, fisicamente, enquanto o algoritmo insiste que aquele é o caminho. O sintoma é um NPC empurrando uma quina.

O terceiro é o mais sutil e está no coração do exemplo oficial: decidir que um ponto foi alcançado é mais difícil do que parece. O exemplo da documentação trata o personagem no ar como ponto ainda não alcançado, constrói um plano do próximo ponto em direção ao atual, achata a normal desse plano em relação ao eixo Y quando não está escalando, e calcula um limiar a partir da velocidade do personagem em relação ao plano — considerando o ponto alcançado quando a distância fica abaixo desse limiar à frente do plano.

Isso é muito mais elaborado que "a distância é menor que dois studs", e a elaboração tem motivo. Um personagem rápido percorre mais que o raio de tolerância entre um quadro e outro, passa direto do ponto sem nunca estar perto o bastante, e fica oscilando de um lado para o outro. O limiar proporcional à velocidade resolve isso.

A lição prática, mesmo para quem não vai copiar aquele código: se o seu NPC fica tremendo em volta de um ponto, o problema não é o caminho. É o critério de chegada.

O quarto erro é recalcular demais. Cada cálculo tem custo, e um NPC que recalcula todo quadro consome o servidor sem andar melhor. Recalcular por evento — bloqueio, alvo que se moveu além de um limite, prazo estourado — é o meio-termo que funciona.

Próximo passo

O recurso que rende mais depois do básico são os modificadores de pathfinding, que é a segunda opção do widget de visualização. Eles são o que permite dizer "esta região é perigosa" ou "prefira este corredor" sem mexer na geometria — e os rótulos aparecem na tela, o que torna a configuração conferível.

Combinados com o Costs, eles transformam o pathfinding de um algoritmo que acha o caminho mais curto num sistema que acha o caminho que faz sentido no seu jogo: o guarda que patrulha pelo corredor iluminado, o bicho que evita a água, o NPC que não passa pela sala do chefe.

Antes disso, porém, vale o exercício de cinco minutos: abra o seu mapa, ligue a malha de navegação, e ande pelo cenário olhando. Quase todo mundo encontra pelo menos um lugar onde a malha discorda do que achava — e esse lugar costuma ser exatamente onde os NPCs vinham se comportando mal.

Fontes

Como corrigimos erros

Artefato pronto pra usar

NPC que persegue um alvo e recalcula quando o caminho é bloqueado (ServerScriptService)
lua
-- ServerScriptService: NPC que segue um alvo usando PathfindingService.
--
-- A diferença entre isto e um MoveTo na direção do alvo é o que
-- acontece quando existe uma parede: MoveTo anda contra ela para
-- sempre, o caminho calculado dá a volta.
--
-- Três coisas que este script faz e que a maioria esquece:
--   1. RECALCULA quando o caminho é bloqueado no meio
--   2. RECALCULA quando o alvo se move demais desde o cálculo
--   3. DESISTE do ponto se demorar tempo demais para alcançá-lo
--      (sem isso, um NPC preso num canto fica preso para sempre)

local PathfindingService = game:GetService("PathfindingService")

local npc = script.Parent
local humanoid = npc:WaitForChild("Humanoid")
local raiz = npc:WaitForChild("HumanoidRootPart")

local RECALCULAR_SE_ALVO_ANDAR = 8 -- studs
local PRAZO_POR_PONTO = 8 -- segundos
local PAUSA = 0.1

-- Os parâmetros do agente descrevem o CORPO que vai andar. Errar o
-- tamanho aqui produz caminho que atravessa vão onde o NPC não cabe.
local caminho = PathfindingService:CreatePath({
	AgentRadius = 2,
	AgentHeight = 5,
	AgentCanJump = true,
	AgentCanClimb = false,
	Costs = {
		Water = 20, -- atravessa, mas só se não houver caminho seco
		Perigo = math.huge, -- PathfindingModifier chamado "Perigo"
	},
})

local bloqueado = false
caminho.Blocked:Connect(function()
	bloqueado = true
end)

local function alvoAtual()
	local alvo = workspace:FindFirstChild("Alvo")
	return alvo and alvo:IsA("BasePart") and alvo.Position or nil
end

local function irAte(destino)
	local ok = pcall(function()
		caminho:ComputeAsync(raiz.Position, destino)
	end)
	if not ok or caminho.Status ~= Enum.PathStatus.Success then
		return false
	end

	local pontos = caminho:GetWaypoints()
	bloqueado = false

	-- começa no 2: o primeiro ponto é onde o NPC já está
	for i = 2, #pontos do
		if bloqueado then
			return false
		end

		local destinoNovo = alvoAtual()
		if destinoNovo and (destinoNovo - destino).Magnitude > RECALCULAR_SE_ALVO_ANDAR then
			return false -- o alvo andou: vale mais recalcular do que insistir
		end

		local ponto = pontos[i]
		if ponto.Action == Enum.PathWaypointAction.Jump then
			humanoid.Jump = true
		end

		humanoid:MoveTo(ponto.Position)

		-- MoveToFinished com prazo: sem ele, NPC presso num canto trava
		-- a rotina inteira e nunca mais anda.
		local chegou = humanoid.MoveToFinished:Wait()
		if not chegou then
			return false
		end
	end

	return true
end

task.spawn(function()
	while npc.Parent and humanoid.Health > 0 do
		local destino = alvoAtual()
		if destino then
			irAte(destino)
		end
		task.wait(PAUSA)
	end
end)
Neste artigo

Continue por aqui