Pathfinding is the process of moving a character along a logical path to reach a destination, avoiding obstacles and (optionally) hazardous materials or defined regions.
Navigation Visualization
To assist with pathfinding layout and debugging, Studio can render a navigation mesh and modifier labels. To enable them, toggle on Navigation mesh and Pathfinding modifiers from the Visualization Options widget in the upper‑right corner of the 3D viewport.
With Navigation mesh enabled, colored areas show where a character might walk or swim, while non-colored areas are blocked. The small arrows indicate areas that a character will attempt to reach by jumping, assuming you set AgentCanJump to true when creating the path.
With Pathfinding modifiers enabled, text labels indicate specific materials and regions that are taken into consideration when using pathfinding modifiers.
Known Limitations
Pathfinding features specific limitations to ensure efficient processing and optimal performance.
Vertical Placement Limit
Pathfinding calculations consider only parts within certain vertical boundaries:
- Lower Boundary — Parts with a bottom Y coordinate less than -65,536 studs are ignored.
- Upper Boundary — Parts with a top Y coordinate exceeding 65,536 studs are ignored.
- Vertical Span — The vertical distance from the lowest part's bottom Y coordinate to the highest part's top Y coordinate must not exceed 65,536 studs; otherwise, the pathfinding system will ignore those parts during the pathfinding computation.
Search Distance Limitation
The direct line-of-sight distance for pathfinding from the start to the finish point must not exceed 3,000 studs. Exceeding this distance will result in a NoPath status.
Creating Paths
Pathfinding is initiated through PathfindingService and its CreatePath() function.
LocalScript
local PathfindingService = game:GetService("PathfindingService")local path = PathfindingService:CreatePath()
CreatePath() accepts an optional table of parameters which fine tune how the character (agent) moves along the path.
Key | Description | Type | Default |
---|---|---|---|
AgentRadius | Agent radius, in studs. Useful for determining the minimum separation from obstacles. | integer | 2 |
AgentHeight | Agent height, in studs. Empty space smaller than this value, like the space under stairs, will be marked as non-traversable. | integer | 5 |
AgentCanJump | Determines whether jumping during pathfinding is allowed. | boolean | true |
AgentCanClimb | Determines whether climbing TrussParts during pathfinding is allowed. | boolean | false |
WaypointSpacing | Spacing between intermediate waypoints in path. If set to math.huge, there will be no intermediate waypoints. | number | 4 |
Costs | Table of materials or defined PathfindingModifiers and their cost for traversal. Useful for making the agent prefer certain materials/regions over others. See modifiers for details. | table | nil |
LocalScript
local PathfindingService = game:GetService("PathfindingService")local path = PathfindingService:CreatePath({AgentRadius = 3,AgentHeight = 6,AgentCanJump = false,Costs = {Water = 20}})
Note that the agent can climb TrussParts during pathfinding assuming you set AgentCanClimb to true when creating the path and nothing blocks the agent from the truss climbing path. A climbable path has the Climb label and the cost for a climbable path is 1 by default.
LocalScript - Truss Climbing Path
local PathfindingService = game:GetService("PathfindingService")local path = PathfindingService:CreatePath({AgentCanClimb = true,Costs = {Climb = 2 -- Cost of the climbing path; default is 1}})
Moving Along Paths
This section uses the following pathfinding script for the player's character. To test while reading:
- Copy the code into a LocalScript within StarterCharacterScripts.
- Edit line 11 to a Vector3 destination that the player character can reach.
- Proceed through the following sections to learn about path computation and character movement.
LocalScript - Character Pathfinding
local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local path = PathfindingService:CreatePath()
local player = Players.LocalPlayer
local character = player.Character
local humanoid = character:WaitForChild("Humanoid")
local TEST_DESTINATION = Vector3.new(100, 0, 100)
local waypoints
local nextWaypointIndex
local reachedConnection
local blockedConnection
local function followPath(destination)
-- Compute the path
local success, errorMessage = pcall(function()
path:ComputeAsync(character.PrimaryPart.Position, destination)
end)
if success and path.Status == Enum.PathStatus.Success then
-- Get the path waypoints
waypoints = path:GetWaypoints()
-- Detect if path becomes blocked
blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
-- Check if the obstacle is further down the path
if blockedWaypointIndex >= nextWaypointIndex then
-- Stop detecting path blockage until path is re-computed
blockedConnection:Disconnect()
-- Call function to re-compute new path
followPath(destination)
end
end)
-- Detect when movement to next waypoint is complete
if not reachedConnection then
reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
if reached and nextWaypointIndex < #waypoints then
-- Increase waypoint index and move to next waypoint
nextWaypointIndex += 1
humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
else
reachedConnection:Disconnect()
blockedConnection:Disconnect()
end
end)
end
-- Initially move to second waypoint (first waypoint is path start; skip it)
nextWaypointIndex = 2
humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
else
warn("Path not computed!", errorMessage)
end
end
followPath(TEST_DESTINATION)
Computing the Path
After you've created a valid path with CreatePath(), it must be computed by calling Path:ComputeAsync() with a Vector3 for both the starting point and destination.
LocalScript - Character Pathfinding
local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local path = PathfindingService:CreatePath()
local player = Players.LocalPlayer
local character = player.Character
local humanoid = character:WaitForChild("Humanoid")
local TEST_DESTINATION = Vector3.new(100, 0, 100)
local waypoints
local nextWaypointIndex
local reachedConnection
local blockedConnection
local function followPath(destination)
-- Compute the path
local success, errorMessage = pcall(function()
path:ComputeAsync(character.PrimaryPart.Position, destination)
end)
end
Getting Waypoints
Once the Path is computed, it will contain a series of waypoints that trace the path from start to end. These points can be gathered with the Path:GetWaypoints() function.
LocalScript - Character Pathfinding
local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local path = PathfindingService:CreatePath()
local player = Players.LocalPlayer
local character = player.Character
local humanoid = character:WaitForChild("Humanoid")
local TEST_DESTINATION = Vector3.new(100, 0, 100)
local waypoints
local nextWaypointIndex
local reachedConnection
local blockedConnection
local function followPath(destination)
-- Compute the path
local success, errorMessage = pcall(function()
path:ComputeAsync(character.PrimaryPart.Position, destination)
end)
if success and path.Status == Enum.PathStatus.Success then
-- Get the path waypoints
waypoints = path:GetWaypoints()
end
end
Path Movement
Each waypoint consists of both a position (Vector3) and action (PathWaypointAction). To move a character containing a Humanoid, like a typical Roblox character, the easiest way is to call Humanoid:MoveTo() from waypoint to waypoint, using the MoveToFinished event to detect when the character reaches each waypoint.
LocalScript - Character Pathfinding
local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local path = PathfindingService:CreatePath()
local player = Players.LocalPlayer
local character = player.Character
local humanoid = character:WaitForChild("Humanoid")
local TEST_DESTINATION = Vector3.new(100, 0, 100)
local waypoints
local nextWaypointIndex
local reachedConnection
local blockedConnection
local function followPath(destination)
-- Compute the path
local success, errorMessage = pcall(function()
path:ComputeAsync(character.PrimaryPart.Position, destination)
end)
if success and path.Status == Enum.PathStatus.Success then
-- Get the path waypoints
waypoints = path:GetWaypoints()
-- Detect if path becomes blocked
blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
-- Check if the obstacle is further down the path
if blockedWaypointIndex >= nextWaypointIndex then
-- Stop detecting path blockage until path is re-computed
blockedConnection:Disconnect()
-- Call function to re-compute new path
followPath(destination)
end
end)
-- Detect when movement to next waypoint is complete
if not reachedConnection then
reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
if reached and nextWaypointIndex < #waypoints then
-- Increase waypoint index and move to next waypoint
nextWaypointIndex += 1
humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
else
reachedConnection:Disconnect()
blockedConnection:Disconnect()
end
end)
end
-- Initially move to second waypoint (first waypoint is path start; skip it)
nextWaypointIndex = 2
humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
else
warn("Path not computed!", errorMessage)
end
end
Handling Blocked Paths
Many Roblox worlds are dynamic; parts might move or fall and floors may collapse. This can block a computed path and prevent the character from reaching its destination. To handle this, you can connect the Path.Blocked event and re-compute the path around whatever blocked it.
LocalScript - Character Pathfinding
local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local path = PathfindingService:CreatePath()
local player = Players.LocalPlayer
local character = player.Character
local humanoid = character:WaitForChild("Humanoid")
local TEST_DESTINATION = Vector3.new(100, 0, 100)
local waypoints
local nextWaypointIndex
local reachedConnection
local blockedConnection
local function followPath(destination)
-- Compute the path
local success, errorMessage = pcall(function()
path:ComputeAsync(character.PrimaryPart.Position, destination)
end)
if success and path.Status == Enum.PathStatus.Success then
-- Get the path waypoints
waypoints = path:GetWaypoints()
-- Detect if path becomes blocked
blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
-- Check if the obstacle is further down the path
if blockedWaypointIndex >= nextWaypointIndex then
-- Stop detecting path blockage until path is re-computed
blockedConnection:Disconnect()
-- Call function to re-compute new path
followPath(destination)
end
end)
end
end
Pathfinding Modifiers
By default, Path:ComputeAsync() returns the shortest path between the starting point and destination, with the exception that it attempts to avoid jumps. This looks unnatural in some situations — for instance, a path may go through water rather than over a nearby bridge simply because the path through water is geometrically shorter.
To optimize pathfinding even further, you can implement pathfinding modifiers to compute smarter paths across various materials, around defined regions, or through obstacles.
Setting Material Costs
When working with Terrain and BasePart materials, you can include a Costs table within CreatePath() to make certain materials more traversable than others. All materials have a default cost of 1 and any material can be defined as non-traversable by setting its value to math.huge.
Keys in the Costs table should be string names representing Enum.Material names, for example Water for Enum.Material.Water.
LocalScript - Character Pathfinding
local PathfindingService = game:GetService("PathfindingService")local Players = game:GetService("Players")local RunService = game:GetService("RunService")local path = PathfindingService:CreatePath({Costs = {Water = 20,Mud = 5,Neon = math.huge}})
Working With Regions
In some cases, material preference is not enough. For example, you might want characters to avoid a defined region, regardless of the materials underfoot. This can be achieved by adding a PathfindingModifier object to a part.
Create an Anchored part around the dangerous region and set its CanCollide property to false.
Insert a PathfindingModifier instance onto the part, locate its Label property, and assign a meaningful name like DangerZone.
Include a Costs table within CreatePath() containing a matching key and associated numeric value. A modifier can be defined as non-traversable by setting its value to math.huge.
LocalScript - Character Pathfindinglocal PathfindingService = game:GetService("PathfindingService")local Players = game:GetService("Players")local RunService = game:GetService("RunService")local path = PathfindingService:CreatePath({Costs = {DangerZone = math.huge}})
Ignoring Obstacles
In some cases, it's useful to pathfind through solid obstacles as if they didn't exist. This lets you compute a path through specific physical blockers, versus the computation failing outright.
Create an Anchored part around the object and set its CanCollide property to false.
Insert a PathfindingModifier instance onto the part and enable its PassThrough property.
Now, when a path is computed from the zombie NPC to the player character, the path extends beyond the door and you can prompt the zombie to traverse it. Even if the zombie is unable to open the door, it reacts as if it "hears" the character behind the door.
Pathfinding Links
Sometimes it's necessary to find a path across a space that cannot be normally traversed, such as across a chasm, and perform a custom action to reach the next waypoint. This can be achieved through the PathfindingLink object.
Using the island example from above, you can make the agent use a boat instead of walking across all of the bridges.
To create a PathfindingLink using this example:
- optionalTo assist with visualization and debugging, toggle on Pathfinding links from the Visualization Options widget in the upper‑right corner of the 3D viewport.
Create two Attachments, one on the boat's seat and one near the boat's landing point.
Create a PathfindingLink object in the workspace, then assign its Attachment0 and Attachment1 properties to the starting and ending attachments respectively.
Assign a meaningful name like UseBoat to its Label property. This name is used as a flag in the pathfinding script to trigger a custom action when the agent reaches the starting link point.
Include a Costs table within CreatePath() containing both a Water key and a custom key matching the Label property name. Assign the custom key a lower value than Water.
LocalScript - Character Pathfindinglocal PathfindingService = game:GetService("PathfindingService")local Players = game:GetService("Players")local RunService = game:GetService("RunService")local path = PathfindingService:CreatePath({Costs = {Water = 20,UseBoat = 1}})In the event which fires when a waypoint is reached, add a custom check for the Label modifier name and take a different action than Humanoid:MoveTo() — in this case, calling a function to seat the agent in the boat, move the boat across the water, and continue the agent's path upon arrival at the destination island.
LocalScript - Character Pathfindinglocal PathfindingService = game:GetService("PathfindingService")local Players = game:GetService("Players")local RunService = game:GetService("RunService")local path = PathfindingService:CreatePath({Costs = {Water = 20,UseBoat = 1}})local player = Players.LocalPlayerlocal character = player.Characterlocal humanoid = character:WaitForChild("Humanoid")local TEST_DESTINATION = Vector3.new(228.9, 17.8, 292.5)local waypointslocal nextWaypointIndexlocal reachedConnectionlocal blockedConnectionlocal function followPath(destination)-- Compute the pathlocal success, errorMessage = pcall(function()path:ComputeAsync(character.PrimaryPart.Position, destination)end)if success and path.Status == Enum.PathStatus.Success then-- Get the path waypointswaypoints = path:GetWaypoints()-- Detect if path becomes blockedblockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)-- Check if the obstacle is further down the pathif blockedWaypointIndex >= nextWaypointIndex then-- Stop detecting path blockage until path is re-computedblockedConnection:Disconnect()-- Call function to re-compute new pathfollowPath(destination)endend)-- Detect when movement to next waypoint is completeif not reachedConnection thenreachedConnection = humanoid.MoveToFinished:Connect(function(reached)if reached and nextWaypointIndex < #waypoints then-- Increase waypoint index and move to next waypointnextWaypointIndex += 1-- Use boat if waypoint label is "UseBoat"; otherwise move to next waypointif waypoints[nextWaypointIndex].Label == "UseBoat" thenuseBoat()elsehumanoid:MoveTo(waypoints[nextWaypointIndex].Position)endelsereachedConnection:Disconnect()blockedConnection:Disconnect()endend)end-- Initially move to second waypoint (first waypoint is path start; skip it)nextWaypointIndex = 2humanoid:MoveTo(waypoints[nextWaypointIndex].Position)elsewarn("Path not computed!", errorMessage)endendfunction useBoat()local boat = workspace.BoatModelhumanoid.Seated:Connect(function()-- Start boat moving if agent is seatedif humanoid.Sit thentask.wait(1)boat.CylindricalConstraint.Velocity = 5end-- Detect constraint position in relation to islandlocal boatPositionConnectionboatPositionConnection = RunService.PostSimulation:Connect(function()-- Stop boat when next to islandif boat.CylindricalConstraint.CurrentPosition >= 94 thenboatPositionConnection:Disconnect()boat.CylindricalConstraint.Velocity = 0task.wait(1)-- Unseat agent and continue to destinationhumanoid.Sit = falsehumanoid:MoveTo(waypoints[nextWaypointIndex].Position)endend)end)endfollowPath(TEST_DESTINATION)
Streaming Compatibility
In-experience instance streaming is a powerful feature that dynamically loads and unloads 3D content as a player's character moves around the world. As they explore the 3D space, new subsets of the space stream to their device and some of the existing subsets might stream out.
Consider the following best practices for using PathfindingService in streaming-enabled experiences:
Streaming can block or unblock a given path as a character moves along it. For example, while a character runs through a forest, a tree might stream in somewhere ahead of them and obstruct the path. To make pathfinding work seamlessly with streaming, it's highly recommended that you use the Handling Blocked Paths technique and re-compute the path when necessary.
A common approach in pathfinding is to use the coordinates of existing objects for computation, such as setting a path destination to the position of an existing TreasureChest model in the world. This approach is fully compatible with server-side Scripts since the server has full view of the world at all times, but LocalScripts and ModuleScripts that run on the client may fail if they attempt to compute a path to an object that's not streamed in.
To address this issue, consider setting the destination to the position of a BasePart within a persistent model. Persistent models load soon after the player joins and they never stream out, so a client-side script can connect to the PersistentLoaded event and safely access the model for creating waypoints after the event fires.