In the Articles/Moving NPCs Between Points|Moving NPCs Between Points
tutorial, you learned about direct straight-line character movement. In this article, we’ll explore how to move an NPC along a more complex path or around obstacles. This is known as pathfinding.
Setup
In Roblox, pathfinding is driven by the PathfindingService
, so your scripts must get the service before doing much else.
local PathfindingService = game:GetService("PathfindingService")
Creating a Path
Once you’ve included PathfindingService
in your script, you can create a new Path
with the PathfindingService/CreatePath|CreatePath()
method:
local PathfindingService = game:GetService("PathfindingService")
local path = PathfindingService:CreatePath(pathParams)
Path Parameters
As you can see, this method accepts one argument, pathParams
, a Lua table which lets you fine-tune the path for the size of the agent (the humanoid that will move along the path).
Here are the available parameters for the pathParams
table:
Key |
Value Type |
Default |
Description |
AgentRadius |
integer |
2 |
Humanoid radius. Used to determine the minimum separation from obstacles. |
AgentHeight |
integer |
5 |
Humanoid height. Empty space smaller than this value, like the space under stairs, will be marked as non-traversable. |
AgentCanJump |
boolean |
true |
Sets whether off-mesh links for jumping are allowed. |
Moving Along the Path
The zombie below has a few more brains than the zombie in the Articles/Moving NPCs Between Points|direct movement
tutorial, so it shouldn’t walk directly toward the pink flag and into the lava. Instead, let’s make it walk safely across the plank.
The following code gets the PathfindingService
, creates variables for the zombie and its Humanoid
, sets the destination point (pink flag), and creates the Path
object.
local PathfindingService = game:GetService("PathfindingService")
-- Variables for the zombie, its humanoid, and destination
local zombie = game.Workspace.Zombie
local humanoid = zombie.Humanoid
local destination = game.Workspace.PinkFlag
-- Create the path object
local path = PathfindingService:CreatePath()
In this example, we don't pass a table of custom parameters to PathfindingService/CreatePath|CreatePath()
because the agent (zombie) is a normal-sized character and the default radius/height values are fine.
Computing the Path
After you’ve created a valid Path
object using PathfindingService/CreatePath|CreatePath()
, you need to compute the path — this is an explicit step that does not happen automatically when the path is created!
To compute a path, call Path/ComputeAsync|ComputeAsync()
on the Path
object, providing a Vector3
for both the starting location and target destination.
local PathfindingService = game:GetService("PathfindingService")
-- Variables for the zombie, its humanoid, and destination
local zombie = game.Workspace.Zombie
local humanoid = zombie.Humanoid
local destination = game.Workspace.PinkFlag
-- Create the path object
local path = PathfindingService:CreatePath()
-- Compute the path
path:ComputeAsync(zombie.HumanoidRootPart.Position, destination.PrimaryPart.Position)
Getting Path Waypoints
Once the Path
is computed by Path/ComputeAsync|ComputeAsync()
, it will have a series of waypoints that, when followed, can lead a character along the path. These points are gathered with the Path/GetWaypoints|GetWaypoints()
function:
local PathfindingService = game:GetService("PathfindingService")
-- Variables for the zombie, its humanoid, and destination
local zombie = game.Workspace.Zombie
local humanoid = zombie.Humanoid
local destination = game.Workspace.PinkFlag
-- Create the path object
local path = PathfindingService:CreatePath()
-- Compute the path
path:ComputeAsync(zombie.HumanoidRootPart.Position, destination.PrimaryPart.Position)
-- Get the path waypoints
local waypoints = path:GetWaypoints()
Showing Waypoints
With the waypoints saved, we can show each one by creating a small part at its location:
-- Get the path waypoints
local waypoints = path:GetWaypoints()
-- Loop through waypoints
for _, waypoint in pairs(waypoints) do
local part = Instance.new("Part")
part.Shape = "Ball"
part.Material = "Neon"
part.Size = Vector3.new(0.6, 0.6, 0.6)
part.Position = waypoint.Position
part.Anchored = true
part.CanCollide = false
part.Parent = game.Workspace
end
As you can see, the path waypoints lead across the plank and over to the pink flag.
Path Movement
The path looks good, so let’s make the zombie walk along it. The easiest way is to call Humanoid/MoveTo|MoveTo()
from waypoint to waypoint. In this script, we can simply add two commands to the same waypoint loop:
-- Loop through waypoints
for _, waypoint in pairs(waypoints) do
local part = Instance.new("Part")
part.Shape = "Ball"
part.Material = "Neon"
part.Size = Vector3.new(0.6, 0.6, 0.6)
part.Position = waypoint.Position
part.Anchored = true
part.CanCollide = false
part.Parent = game.Workspace
-- Move zombie to the next waypoint
humanoid:MoveTo(waypoint.Position)
-- Wait until zombie has reached the waypoint before continuing
humanoid.MoveToFinished:Wait()
end
Handling Blocked Paths
Many Roblox worlds are dynamic — parts might move or fall, floors collapse, etc. This can block a computed path and prevent the NPC from reaching its destination.
To handle this, you can connect the Path/Blocked|Blocked
event to the Path
object and re-compute the path around whatever blocked it. Consider this pathfinding script:
local PathfindingService = game:GetService("PathfindingService")
-- Variables for the zombie, its humanoid, and destination
local zombie = game.Workspace.Zombie
local humanoid = zombie.Humanoid
local destination = game.Workspace.PinkFlag
-- Create the path object
local path = PathfindingService:CreatePath()
-- Variables to store waypoints table and zombie's current waypoint
local waypoints
local currentWaypointIndex
local function followPath(destinationObject)
-- Compute and check the path
path:ComputeAsync(zombie.HumanoidRootPart.Position, destinationObject.PrimaryPart.Position)
-- Empty waypoints table after each new path computation
waypoints = {}
if path.Status == Enum.PathStatus.Success then
-- Get the path waypoints and start zombie walking
waypoints = path:GetWaypoints()
-- Move to first waypoint
currentWaypointIndex = 1
humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
else
-- Error (path not found); stop humanoid
humanoid:MoveTo(zombie.HumanoidRootPart.Position)
end
end
local function onWaypointReached(reached)
if reached and currentWaypointIndex < #waypoints then
currentWaypointIndex = currentWaypointIndex + 1
humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
end
end
local function onPathBlocked(blockedWaypointIndex)
-- Check if the obstacle is further down the path
if blockedWaypointIndex > currentWaypointIndex then
-- Call function to re-compute the path
followPath(destination)
end
end
-- Connect 'Blocked' event to the 'onPathBlocked' function
path.Blocked:Connect(onPathBlocked)
-- Connect 'MoveToFinished' event to the 'onWaypointReached' function
humanoid.MoveToFinished:Connect(onWaypointReached)
followPath(destination)
Several things have been added or changed, so let’s walk through the code:
- The first section is similar to before: get the
PathfindingService
, set some variables, and create the Path
object. The main additions are the waypoints
and currentWaypointIndex
variables. waypoints
will store the computed waypoints and currentWaypointIndex
will track the index number of each waypoint the zombie reaches, starting at 1 and increasing as it walks along the path.
local PathfindingService = game:GetService("PathfindingService")
-- Variables for the zombie, its humanoid, and destination
local zombie = game.Workspace.Zombie
local humanoid = zombie.Humanoid
local destination = game.Workspace.PinkFlag
-- Create the path object
local path = PathfindingService:CreatePath()
-- Variables to store waypoints table and zombie's current waypoint
local waypoints
local currentWaypointIndex
- The next function,
followPath()
, contains many of the same commands we used earlier, plus a little error checking on line 21. If Path/ComputeAsync|path:ComputeAsync()
is successful, we get the waypoints and store them in the waypoints
variable. Next, we set the currentWaypointIndex
counter to 1 and move the zombie to the first waypoint.
local function followPath(destinationObject)
-- Compute and check the path
path:ComputeAsync(zombie.HumanoidRootPart.Position, destinationObject.PrimaryPart.Position)
-- Empty waypoints table after each new path computation
waypoints = {}
if path.Status == Enum.PathStatus.Success then
-- Get the path waypoints and start zombie walking
waypoints = path:GetWaypoints()
-- Move to first waypoint
currentWaypointIndex = 1
humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
else
-- Error (path not found); stop humanoid
humanoid:MoveTo(zombie.HumanoidRootPart.Position)
end
end
- In a dynamic place where paths may be blocked, it’s problematic to loop through all waypoints in a
pairs()
loop. If anything blocks the path, that loop can be tricky to stop/break. In this script, the onWaypointReached()
function moves the zombie onward only after the next waypoint is reached.
local function onWaypointReached(reached)
if reached and currentWaypointIndex < #waypoints then
currentWaypointIndex = currentWaypointIndex + 1
humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
end
end
- The final function,
onPathBlocked()
, is connected to the Path/Blocked|Blocked
event on line 49. If the path is ever blocked, this function will fire and blockedWaypointIndex
will be the waypoint index number that was blocked.
local function onPathBlocked(blockedWaypointIndex)
-- Check if the obstacle is further down the path
if blockedWaypointIndex > currentWaypointIndex then
-- Call function to re-compute the path
followPath(destination)
end
end
-- Connect 'Blocked' event to the 'onPathBlocked' function
path.Blocked:Connect(onPathBlocked)
On line 42, we check if the blocked waypoint index is greater than the current waypoint index. Remember that the path may become blocked somewhere behind the zombie, but that doesn't mean it should stop. This check makes sure that the path is re-computed only if the blocked waypoint is ahead of the zombie.
Be aware that the Path/Blocked|Blocked
event can fire on a path anytime during the path's lifetime. It may also fire multiple times if moving obstacles are involved, like a boulder rolling across the path. Because of this, you may want to Instance/Destroy|destroy
the path or un-register the Path/Blocked|Blocked
connection until a new path is computed.
As you can see, PathfindingService
lets you create NPCs that are much smarter than the average zombie. Along with custom agent parameters and the Path/Blocked|Blocked
event, you can make any NPC reach a destination, even in a dynamic and changing place.