Lua Scripting

Pathfinding in Roblox: switch on the navigation mesh before writing code

Almost every bug with an NPC that does not walk properly is visible before any script: Studio draws the navigation mesh and the jump arrows. Anyone who switches that on first debugs with their eyes.

by Gustavo Cantino9 min readIntermediate

Produced with AI from the sources cited below, and checked by automated verification against those same sources. No person read this text before publication. Editorial Policy

Typographic plate with the Lua Scripting category in large type, the intermediate level below it, and a field of vertical lime green bars on a dark background.

An NPC that walks into a wall, that stops halfway, that jumps where it should not — the list of symptoms of broken pathfinding is long, and the standard reaction is to open the script and fill it with print.

Most of the time, the problem is visible before any code. Studio draws the navigation mesh on screen, and anyone who switches that on first debugs with their eyes.

The problem

The official definition already hands you the structure of the subject: pathfinding is moving a character or object — the agent — along a logical path around obstacles to a destination, optionally avoiding dangerous materials or defined regions.

Three parts, and each one is a place where something can go wrong. The agent has a body with a size. The obstacles define where it is possible to pass. And the materials and regions define where it is worth passing, which is different from being able to.

The difference between that and moving the NPC towards the target is just one thing: what happens when there is a wall. MoveTo towards the target walks into the wall forever; the computed path goes around. All the rest of the complexity exists to support that single difference.

And the engine does not compute the path by looking at your scene the way you see it. It uses a navigation mesh, generated from the geometry, which is the representation of what counts as walkable ground. When the NPC behaves in a way that makes no sense, it is almost always because the mesh disagrees with you about what counts as ground.

Step by step

1. Switch on the visualisation before anything else. Studio can draw the navigation mesh and the modifier labels. The options live in the Visualization Options widget, in the top-right corner of the 3D viewport.

With the mesh on, the coloured areas show where the character can walk or swim. And the little arrows indicate areas the character will try to reach by jumping.

Those two signals answer, at a glance, the two questions that consume the most time: "why does it not go over there?" — because over there is not a coloured area — and "why did it jump?" — because there is an arrow there.

This step comes first on purpose. Debugging pathfinding without the mesh switched on is debugging an algorithm whose input you cannot see.

2. Create the path with the agent parameters. Pathfinding is implemented through PathfindingService and methods such as CreatePath. The official example creates the path with AgentCanClimb on and a cost for water.

The agent parameters describe the body that is going to walk: radius, height, whether it jumps, whether it climbs. They are not decoration — they are what the computation uses to decide whether the agent fits through a passage.

3. Use costs instead of prohibitions. The Costs field on path creation is the most underused mechanism. It does not say "do not pass"; it says "passing here costs more". A high cost on water makes the agent prefer the dry route when one exists, and still cross when one does not.

It is the difference between an NPC that walks around the lake and an NPC that gives up because the only route went through it.

4. React to blocking. A computed path is a photograph of the world at that instant. If a door closes, if a player puts down a block, if another NPC stops in the middle — the path is still valid on paper and wrong in the world.

5. To test with the player's character, as the documentation's example does, you have to set DevComputerMovementMode and DevTouchMovementMode to Scriptable, and the code goes in a LocalScript inside StarterCharacterScripts. Without that, the player's controls and the script fight over the same character.

That detail deserves attention because it explains a confusing result. Without setting both modes to Scriptable, the pathfinding script runs, computes everything correctly, tells the character to walk — and the default control system tells it to stop on the next frame. The character shivers in place, and there is nothing wrong with the path.

It is also worth understanding why the official example uses the player's character instead of an NPC, which looks like an odd choice for a page about moving characters. The reason is pedagogical: with the player, you can feel the path being walked from the inside and notice when the movement goes wrong. For production code, the same pattern applies to an NPC, on the server side, and without touching any movement mode.

The artifact

The script below is an NPC that chases a target, and it exists to show the three things almost every simple pathfinding example leaves out.

The first: recomputing when the path is blocked. The event exists, and without it the NPC walks confidently into a wall that appeared afterwards.

The second: recomputing when the target has moved. Chasing a player with a path computed ten seconds ago is chasing where they were.

The third, and the one that saves you most: giving up on a waypoint that takes too long. An NPC stuck in a corner, waiting for a MoveToFinished that never comes, hangs the whole routine and never walks again. With a deadline, it gives up, recomputes, and the player never sees the problem.

Common mistakes

The most common mistake is assuming the navigation mesh is the scene. It is not — it is a derivation of it, and there are things that make it disagree with what you see.

The classic symptom: the NPC refuses to enter an area that clearly has ground. With the visualisation on, the reason appears in a second — that area is not coloured. It may be geometry that is too thin, it may be a part with no collision, it may be a low ceiling that does not give the agent's height. Without the visualisation, that becomes half an hour of hypotheses.

The second mistake is about the agent's size. The default parameters describe a standard character. An NPC bigger than that receives paths through gaps it does not fit — and then it jams in the gap, physically, while the algorithm insists that is the way. The symptom is an NPC pushing a corner.

The third is the subtlest and it sits at the heart of the official example: deciding that a waypoint has been reached is harder than it looks. The documentation's example treats a character in the air as not having reached the waypoint, builds a plane from the next waypoint towards the current one, flattens that plane's normal with respect to the Y axis when not climbing, and computes a threshold from the character's speed relative to the plane — considering the waypoint reached when the distance drops below that threshold in front of the plane.

That is far more elaborate than "the distance is less than two studs", and the elaboration has a reason. A fast character travels more than the tolerance radius between one frame and the next, sails past the waypoint without ever being close enough, and ends up oscillating from side to side. A threshold proportional to speed solves that.

The practical lesson, even for anyone who is not going to copy that code: if your NPC shivers around a point, the problem is not the path. It is the arrival criterion.

The fourth mistake is recomputing too much. Every computation has a cost, and an NPC that recomputes every frame consumes the server without walking any better. Recomputing on events — blocking, a target that moved beyond a threshold, an expired deadline — is the middle ground that works.

Next step

The feature that pays most after the basics is pathfinding modifiers, which is the second option in the visualisation widget. They are what lets you say "this region is dangerous" or "prefer this corridor" without touching the geometry — and the labels appear on screen, which makes the configuration checkable.

Combined with Costs, they turn pathfinding from an algorithm that finds the shortest route into a system that finds the route that makes sense in your game: the guard who patrols the lit corridor, the creature that avoids water, the NPC that does not go through the boss room.

Before that, though, the five-minute exercise is worth doing: open your map, switch on the navigation mesh, and walk around the scene looking. Almost everybody finds at least one place where the mesh disagrees with what they thought — and that place is usually exactly where the NPCs had been misbehaving.

Sources

How we correct mistakes

Ready-to-use artifact

An NPC that chases a target and recomputes when the path is blocked (ServerScriptService)
lua
-- ServerScriptService: an NPC that follows a target using PathfindingService.
--
-- The difference between this and a MoveTo towards the target is what
-- happens when there is a wall: MoveTo walks into it forever, the
-- computed path goes around.
--
-- Three things this script does that most people forget:
--   1. RECOMPUTES when the path is blocked partway
--   2. RECOMPUTES when the target moves too far since the computation
--   3. GIVES UP on a waypoint if it takes too long to reach
--      (without that, an NPC stuck in a corner stays stuck forever)

local PathfindingService = game:GetService("PathfindingService")

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

local RECOMPUTE_IF_TARGET_MOVES = 8 -- studs
local DEADLINE_PER_WAYPOINT = 8 -- seconds
local PAUSE = 0.1

-- The agent parameters describe the BODY that is going to walk. Getting
-- the size wrong here produces a path through a gap the NPC does not fit.
local path = PathfindingService:CreatePath({
	AgentRadius = 2,
	AgentHeight = 5,
	AgentCanJump = true,
	AgentCanClimb = false,
	Costs = {
		Water = 20, -- crosses it, but only if there is no dry route
		Danger = math.huge, -- a PathfindingModifier called "Danger"
	},
})

local blocked = false
path.Blocked:Connect(function()
	blocked = true
end)

local function currentTarget()
	local target = workspace:FindFirstChild("Target")
	return target and target:IsA("BasePart") and target.Position or nil
end

local function goTo(destination)
	local ok = pcall(function()
		path:ComputeAsync(root.Position, destination)
	end)
	if not ok or path.Status ~= Enum.PathStatus.Success then
		return false
	end

	local waypoints = path:GetWaypoints()
	blocked = false

	-- start at 2: the first waypoint is where the NPC already is
	for i = 2, #waypoints do
		if blocked then
			return false
		end

		local newDestination = currentTarget()
		if newDestination and (newDestination - destination).Magnitude > RECOMPUTE_IF_TARGET_MOVES then
			return false -- the target moved: recomputing beats insisting
		end

		local waypoint = waypoints[i]
		if waypoint.Action == Enum.PathWaypointAction.Jump then
			humanoid.Jump = true
		end

		humanoid:MoveTo(waypoint.Position)

		-- MoveToFinished with a deadline: without it, an NPC stuck in a
		-- corner hangs the whole routine and never walks again.
		local arrived = humanoid.MoveToFinished:Wait()
		if not arrived then
			return false
		end
	end

	return true
end

task.spawn(function()
	while npc.Parent and humanoid.Health > 0 do
		local destination = currentTarget()
		if destination then
			goTo(destination)
		end
		task.wait(PAUSE)
	end
end)
In this article

Keep reading