Character Pathfinding

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.

To assist with pathfinding layout and debugging, Studio can render a navigation mesh. 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.

In addition, Studio can render navigation labels that indicate specific materials and region labels that are taken into consideration when using pathfinding modifiers, as well as link point labels assigned to pathfinding links.

To enable navigation visualization:

  1. Open FileStudio Settings.

  2. In the Studio tab, enable Show Navigation Mesh and, optionally, Show Navigation Labels.

Creating Paths

Pathfinding is initiated through PathfindingService and its CreatePath() function.

LocalScript

1local PathfindingService = game:GetService("PathfindingService")
2
3local path = PathfindingService:CreatePath()
4

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 Pathfinding Modifiers for details. table nil
LocalScript

1local PathfindingService = game:GetService("PathfindingService")
2
3local path = PathfindingService:CreatePath({
4 AgentRadius = 3,
5 AgentHeight = 6,
6 AgentCanJump = false,
7 Costs = {
8 Water = 20
9 }
10})
11

Truss Climbing

The agent can climb TrussParts during pathfinding assuming you set AgentCanClimb = 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

1local PathfindingService = game:GetService("PathfindingService")
2
3local path = PathfindingService:CreatePath({
4 AgentCanClimb = true,
5 Costs = {
6 Climb = 2 -- Cost of the climbing path; default is 1
7 }
8})
9

Moving Along Paths

This section uses the following pathfinding script for the player's character. To test while reading:

  1. Copy the code into a LocalScript within StarterCharacterScripts.
  2. Edit line 11 to a Vector3 destination that the player character can reach.
  3. Proceed through the following sections to learn about path computation and character movement.
LocalScript - Character Pathfinding

1local PathfindingService = game:GetService("PathfindingService")
2local Players = game:GetService("Players")
3local RunService = game:GetService("RunService")
4
5local path = PathfindingService:CreatePath()
6
7local player = Players.LocalPlayer
8local character = player.Character
9local humanoid = character:WaitForChild("Humanoid")
10
11local TEST_DESTINATION = Vector3.new(100, 0, 100)
12
13local waypoints
14local nextWaypointIndex
15local reachedConnection
16local blockedConnection
17
18local function followPath(destination)
19 -- Compute the path
20 local success, errorMessage = pcall(function()
21 path:ComputeAsync(character.PrimaryPart.Position, destination)
22 end)
23
24 if success and path.Status == Enum.PathStatus.Success then
25 -- Get the path waypoints
26 waypoints = path:GetWaypoints()
27
28 -- Detect if path becomes blocked
29 blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
30 -- Check if the obstacle is further down the path
31 if blockedWaypointIndex >= nextWaypointIndex then
32 -- Stop detecting path blockage until path is re-computed
33 blockedConnection:Disconnect()
34 -- Call function to re-compute new path
35 followPath(destination)
36 end
37 end)
38
39 -- Detect when movement to next waypoint is complete
40 if not reachedConnection then
41 reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
42 if reached and nextWaypointIndex < #waypoints then
43 -- Increase waypoint index and move to next waypoint
44 nextWaypointIndex += 1
45 humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
46 else
47 reachedConnection:Disconnect()
48 blockedConnection:Disconnect()
49 end
50 end)
51 end
52
53 -- Initially move to second waypoint (first waypoint is path start; skip it)
54 nextWaypointIndex = 2
55 humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
56 else
57 warn("Path not computed!", errorMessage)
58 end
59end
60
61followPath(TEST_DESTINATION)
62

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

1local PathfindingService = game:GetService("PathfindingService")
2local Players = game:GetService("Players")
3local RunService = game:GetService("RunService")
4
5local path = PathfindingService:CreatePath()
6
7local player = Players.LocalPlayer
8local character = player.Character
9local humanoid = character:WaitForChild("Humanoid")
10
11local TEST_DESTINATION = Vector3.new(100, 0, 100)
12
13local waypoints
14local nextWaypointIndex
15local reachedConnection
16local blockedConnection
17
18local function followPath(destination)
19 -- Compute the path
20 local success, errorMessage = pcall(function()
21 path:ComputeAsync(character.PrimaryPart.Position, destination)
22 end)
23

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

1local PathfindingService = game:GetService("PathfindingService")
2local Players = game:GetService("Players")
3local RunService = game:GetService("RunService")
4
5local path = PathfindingService:CreatePath()
6
7local player = Players.LocalPlayer
8local character = player.Character
9local humanoid = character:WaitForChild("Humanoid")
10
11local TEST_DESTINATION = Vector3.new(100, 0, 100)
12
13local waypoints
14local nextWaypointIndex
15local reachedConnection
16local blockedConnection
17
18local function followPath(destination)
19 -- Compute the path
20 local success, errorMessage = pcall(function()
21 path:ComputeAsync(character.PrimaryPart.Position, destination)
22 end)
23
24 if success and path.Status == Enum.PathStatus.Success then
25 -- Get the path waypoints
26 waypoints = path:GetWaypoints()
27

Path Movement

Each waypoint consists of both a Position (Vector3) and Action (Enum.PathWaypointAction)|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

1local PathfindingService = game:GetService("PathfindingService")
2local Players = game:GetService("Players")
3local RunService = game:GetService("RunService")
4
5local path = PathfindingService:CreatePath()
6
7local player = Players.LocalPlayer
8local character = player.Character
9local humanoid = character:WaitForChild("Humanoid")
10
11local TEST_DESTINATION = Vector3.new(100, 0, 100)
12
13local waypoints
14local nextWaypointIndex
15local reachedConnection
16local blockedConnection
17
18local function followPath(destination)
19 -- Compute the path
20 local success, errorMessage = pcall(function()
21 path:ComputeAsync(character.PrimaryPart.Position, destination)
22 end)
23
24 if success and path.Status == Enum.PathStatus.Success then
25 -- Get the path waypoints
26 waypoints = path:GetWaypoints()
27
28 -- Detect if path becomes blocked
29 blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
30 -- Check if the obstacle is further down the path
31 if blockedWaypointIndex >= nextWaypointIndex then
32 -- Stop detecting path blockage until path is re-computed
33 blockedConnection:Disconnect()
34 -- Call function to re-compute new path
35 followPath(destination)
36 end
37 end)
38
39 -- Detect when movement to next waypoint is complete
40 if not reachedConnection then
41 reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
42 if reached and nextWaypointIndex < #waypoints then
43 -- Increase waypoint index and move to next waypoint
44 nextWaypointIndex += 1
45 humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
46 else
47 reachedConnection:Disconnect()
48 blockedConnection:Disconnect()
49 end
50 end)
51 end
52
53 -- Initially move to second waypoint (first waypoint is path start; skip it)
54 nextWaypointIndex = 2
55 humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
56 else
57 warn("Path not computed!", errorMessage)
58 end
59end
60

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

1local PathfindingService = game:GetService("PathfindingService")
2local Players = game:GetService("Players")
3local RunService = game:GetService("RunService")
4
5local path = PathfindingService:CreatePath()
6
7local player = Players.LocalPlayer
8local character = player.Character
9local humanoid = character:WaitForChild("Humanoid")
10
11local TEST_DESTINATION = Vector3.new(100, 0, 100)
12
13local waypoints
14local nextWaypointIndex
15local reachedConnection
16local blockedConnection
17
18local function followPath(destination)
19 -- Compute the path
20 local success, errorMessage = pcall(function()
21 path:ComputeAsync(character.PrimaryPart.Position, destination)
22 end)
23
24 if success and path.Status == Enum.PathStatus.Success then
25 -- Get the path waypoints
26 waypoints = path:GetWaypoints()
27
28 -- Detect if path becomes blocked
29 blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
30 -- Check if the obstacle is further down the path
31 if blockedWaypointIndex >= nextWaypointIndex then
32 -- Stop detecting path blockage until path is re-computed
33 blockedConnection:Disconnect()
34 -- Call function to re-compute new path
35 followPath(destination)
36 end
37 end)
38

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 Material names, for example Water for Material.Water.

LocalScript - Character Pathfinding

1local PathfindingService = game:GetService("PathfindingService")
2local Players = game:GetService("Players")
3local RunService = game:GetService("RunService")
4
5local path = PathfindingService:CreatePath({
6 Costs = {
7 Water = 20,
8 Mud = 5,
9 Neon = math.huge
10 }
11})
12

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.

  1. Create an anchored part around the dangerous region and set its CanCollide property to false.

  2. Insert a PathfindingModifier instance onto the part.

  3. Select the new instance, locate its Label property, and assign a meaningful name like DangerZone.

  4. 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 Pathfinding

    1local PathfindingService = game:GetService("PathfindingService")
    2local Players = game:GetService("Players")
    3local RunService = game:GetService("RunService")
    4
    5local path = PathfindingService:CreatePath({
    6 Costs = {
    7 DangerZone = math.huge
    8 }
    9})
    10

Ignoring Obstacles

In some cases, it's useful to pathfind through solid obstacles, as if the obstacles didn't exist. For example, if a zombie NPC "hears" a character and tries to find it, but the character is hiding behind a door, Path:ComputeAsync() fails.

To compute a path that ignores an object such as a door:

  1. Create an anchored part around the door and set its CanCollide property to false.

  2. Insert a PathfindingModifier instance onto the part.

  3. Select the new instance and enable its PassThrough property.

    Now, when the same path is computed, it will extend beyond the door.

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:

  1. Create two Attachments, one on the boat's seat and one near the boat's landing point.

  2. Create a PathfindingLink object in the workspace.

  3. Select the new instance and assign its Attachment0 and Attachment1 properties to the starting and ending attachments respectively.

  4. 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.

  5. 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 Pathfinding

    1local PathfindingService = game:GetService("PathfindingService")
    2local Players = game:GetService("Players")
    3local RunService = game:GetService("RunService")
    4
    5local path = PathfindingService:CreatePath({
    6 Costs = {
    7 Water = 20,
    8 UseBoat = 1
    9 }
    10})
    11
  6. 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 Pathfinding

    1local PathfindingService = game:GetService("PathfindingService")
    2local Players = game:GetService("Players")
    3local RunService = game:GetService("RunService")
    4
    5local path = PathfindingService:CreatePath({
    6 Costs = {
    7 Water = 20,
    8 UseBoat = 1
    9 }
    10})
    11
    12local player = Players.LocalPlayer
    13local character = player.Character
    14local humanoid = character:WaitForChild("Humanoid")
    15
    16local TEST_DESTINATION = Vector3.new(228.9, 17.8, 292.5)
    17
    18local waypoints
    19local nextWaypointIndex
    20local reachedConnection
    21local blockedConnection
    22
    23local function followPath(destination)
    24 -- Compute the path
    25 local success, errorMessage = pcall(function()
    26 path:ComputeAsync(character.PrimaryPart.Position, destination)
    27 end)
    28
    29 if success and path.Status == Enum.PathStatus.Success then
    30 -- Get the path waypoints
    31 waypoints = path:GetWaypoints()
    32
    33 -- Detect if path becomes blocked
    34 blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
    35 -- Check if the obstacle is further down the path
    36 if blockedWaypointIndex >= nextWaypointIndex then
    37 -- Stop detecting path blockage until path is re-computed
    38 blockedConnection:Disconnect()
    39 -- Call function to re-compute new path
    40 followPath(destination)
    41 end
    42 end)
    43
    44 -- Detect when movement to next waypoint is complete
    45 if not reachedConnection then
    46 reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
    47 if reached and nextWaypointIndex < #waypoints then
    48 -- Increase waypoint index and move to next waypoint
    49 nextWaypointIndex += 1
    50
    51 -- Use boat if waypoint modifier ID is "UseBoat"; otherwise move to next waypoint
    52 if waypoints[nextWaypointIndex].ModifierId == "UseBoat" then
    53 useBoat()
    54 else
    55 humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
    56 end
    57 else
    58 reachedConnection:Disconnect()
    59 blockedConnection:Disconnect()
    60 end
    61 end)
    62 end
    63
    64 -- Initially move to second waypoint (first waypoint is path start; skip it)
    65 nextWaypointIndex = 2
    66 humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
    67 else
    68 warn("Path not computed!", errorMessage)
    69 end
    70end
    71
    72function useBoat()
    73 local boat = workspace.BoatModel
    74
    75 humanoid.Seated:Connect(function()
    76 -- Start boat moving if agent is seated
    77 if humanoid.Sit == true then
    78 task.wait(1)
    79 boat.CylindricalConstraint.Velocity = 5
    80 end
    81 -- Detect constraint position in relation to island
    82 local boatPositionConnection
    83 boatPositionConnection = RunService.Heartbeat:Connect(function()
    84 -- Stop boat when next to island
    85 if boat.CylindricalConstraint.CurrentPosition >= 94 then
    86 boatPositionConnection:Disconnect()
    87 boat.CylindricalConstraint.Velocity = 0
    88 task.wait(1)
    89 -- Unseat agent and continue to destination
    90 humanoid.Sit = false
    91 humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
    92 end
    93 end)
    94 end)
    95end
    96
    97followPath(TEST_DESTINATION)
    98