Skip to content

Latest commit

 

History

History
610 lines (466 loc) · 26 KB

pathfinding.md

File metadata and controls

610 lines (466 loc) · 26 KB
title description
Character Pathfinding
Pathfinding is the process of moving a character along a logical path to reach a destination.

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

Navigation mesh showing in Studio

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.

Navigation labels showing on navigation mesh

To enable navigation visualization:

  1. Open FileStudio Settings.
  2. In the Studio tab, within the Visualization section, enable Show Navigation Mesh and, optionally, Show Navigation Labels.

Creating Paths

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

local PathfindingService = game:GetService("PathfindingService")

local path = PathfindingService:CreatePath()

Class.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 `Class.TrussPart|TrussParts` during pathfinding is allowed. boolean `false`
**WaypointSpacing** Spacing between intermediate waypoints in path. If set to `Library.math.huge`, there will be no intermediate waypoints. number `4`
**Costs** Table of materials or defined `Class.PathfindingModifier|PathfindingModifiers` and their cost for traversal. Useful for making the agent prefer certain materials/regions over others. See [modifiers](#pathfinding-modifiers) for details. table `nil`
local PathfindingService = game:GetService("PathfindingService")

local path = PathfindingService:CreatePath({
	AgentRadius = 3,
	AgentHeight = 6,
	AgentCanJump = false,
	Costs = {
		Water = 20
	}
})

Note that the agent can climb Class.TrussPart|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.

Path going up a climbable TrussPart ladder

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:

  1. Copy the code into a Class.LocalScript within Class.StarterCharacterScripts.
  2. Edit line 11 to a Datatype.Vector3 destination that the player character can reach.
  3. Proceed through the following sections to learn about path computation and character movement.
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 Class.PathfindingService:CreatePath()|CreatePath(), it must be computed by calling Class.Path:ComputeAsync() with a Datatype.Vector3 for both the starting point and destination.

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

Path start/end marked on series of islands and bridges

Getting Waypoints

Once the Class.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 Class.Path:GetWaypoints() function.

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

Waypoints indicated across computed path

Waypoints indicated across computed path

Path Movement

Each waypoint consists of both a position (Datatype.Vector3) and action (Enum.PathWaypointAction|PathWaypointAction). To move a character containing a Class.Humanoid, like a typical Roblox character, the easiest way is to call Class.Humanoid:MoveTo() from waypoint to waypoint, using the Class.Humanoid.MoveToFinished|MoveToFinished event to detect when the character reaches each waypoint.

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 Class.Path.Blocked event and re-compute the path around whatever blocked it.

Paths may also become blocked somewhere **behind** the agent, such as a pile of rubble falling on a path as the agent runs away, but that doesn't mean the agent should stop moving. The conditional statement on line 31 makes sure that the path is re-computed only if the blocked waypoint is **ahead** of the current waypoint.
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
Currently, `Class.Model|Models` containing a `Class.Humanoid` instance, including typical player characters, will **not** be considered for path [computation](#computing-the-path) or path blockage, although the agent may still be blocked by those models physically.

Pathfinding Modifiers

By default, Class.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.

Two paths indicated with the shorter path not necessarily more logical

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 Class.Terrain and Class.BasePart materials, you can include a Costs table within Class.PathfindingService:CreatePath()|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 Library.math.huge.

Keys in the Costs table should be string names representing Enum.Material names, for example Water for Enum.Material.Water.

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 Class.PathfindingModifier object to a part.

  1. Create an Class.BasePart.Anchored|Anchored part around the dangerous region and set its Class.BasePart.CanCollide|CanCollide property to false.

    Anchored part defining a region to apply a pathfinding modifier to
  2. Insert a Class.PathfindingModifier instance onto the part, locate its Class.PathfindingModifier.Label|Label property, and assign a meaningful name like DangerZone.

    PathfindingModifier instance with Label property set to DangerZone
  3. Include a Costs table within Class.PathfindingService:CreatePath()|CreatePath() containing a matching key and associated numeric value. A modifier can be defined as non-traversable by setting its value to Library.math.huge.

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

  1. Create an Class.BasePart.Anchored|Anchored part around the object and set its Class.BasePart.CanCollide|CanCollide property to false.

    Anchored part defining a region to apply a pathfinding modifier to
  2. Insert a Class.PathfindingModifier instance onto the part and enable its Class.PathfindingModifier.PassThrough|PassThrough property.

    PathfindingModifier instance with PassThrough property enabled

    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.

    Zombie NPC path passing through the previously blocking 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 Class.PathfindingLink object.

Using the island example from above, you can make the agent use a boat instead of walking across all of the bridges.

PathfindingLink showing how an agent can use a boat instead of walking across all of the bridges

To create a Class.PathfindingLink using this example:

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

    Attachments created for pathfinding link's start and end
  2. Create a Class.PathfindingLink object in the workspace, then assign its Attachment0 and Attachment1 properties to the starting and ending attachments respectively.

    Attachment0/Attachment1 properties of a PathfindingLink PathfindingLink visualized in the 3D world
  3. Assign a meaningful name like UseBoat to its Class.PathfindingLink.Label|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.

    Label property specified for PathfindingLink
  4. Include a Costs table within Class.PathfindingService:CreatePath()|CreatePath() containing both a Water key and a custom key matching the Class.PathfindingLink.Label|Label property name. Assign the custom key a lower value than Water.

    local PathfindingService = game:GetService("PathfindingService")
    local Players = game:GetService("Players")
    local RunService = game:GetService("RunService")
    
    local path = PathfindingService:CreatePath({
    	Costs = {
    		Water = 20,
    		UseBoat = 1
    	}
    })
  5. In the event which fires when a waypoint is reached, add a custom check for the Class.PathfindingLink.Label|Label modifier name and take a different action than Class.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.

    local 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.LocalPlayer
    local character = player.Character
    local humanoid = character:WaitForChild("Humanoid")
    
    local TEST_DESTINATION = Vector3.new(228.9, 17.8, 292.5)
    
    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
    
    					-- Use boat if waypoint label is "UseBoat"; otherwise move to next waypoint
    					if waypoints[nextWaypointIndex].Label == "UseBoat" then
    						useBoat()
    					else
    						humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
    					end
    				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
    
    function useBoat()
    	local boat = workspace.BoatModel
    
    	humanoid.Seated:Connect(function()
    		-- Start boat moving if agent is seated
    		if humanoid.Sit then
    			task.wait(1)
    			boat.CylindricalConstraint.Velocity = 5
    		end
    		-- Detect constraint position in relation to island
    		local boatPositionConnection
    		boatPositionConnection = RunService.PostSimulation:Connect(function()
    			-- Stop boat when next to island
    			if boat.CylindricalConstraint.CurrentPosition >= 94 then
    				boatPositionConnection:Disconnect()
    				boat.CylindricalConstraint.Velocity = 0
    				task.wait(1)
    				-- Unseat agent and continue to destination
    				humanoid.Sit = false
    				humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
    			end
    		end)
    	end)
    end
    
    followPath(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 Class.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 Class.Script|Scripts since the server has full view of the world at all times, but Class.LocalScript|LocalScripts and Class.ModuleScript|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 Class.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 Class.Workspace.PersistentLoaded|PersistentLoaded event and safely access the model for creating waypoints after the event fires.