Pathfinding

Moving a character from one point to another is not always a simple matter. Often there is not a clear path between the starting point and destination. Simply moving a character along such a path is a sure fire way of getting it stuck. In cases where a character (be it the player or a NPC) need to move to a specific point, the PathfindingService can compute a clear path that can be followed.

Path[edit]

The key object added by the PathFindingService is the Path object. A Path holds a series of points between a given start and end location and can also check if that path becomes invalid at any point, giving the game a chance to recompute the path.

Creating a path[edit]

A path is made with ComputeRawPathAsync. This function requires three things, a Vector3 for the start of the path, a Vector3 for the end of the path, and the maximum distance the path is allowed to be.

Evaluating a path[edit]

When a Path is created with the PathfindingService, a status is returned along with the path. This status is stored in the path under Status, and will be one of the values in PathStatus. The different statuses and their meaning are as follows:

Success Path found successfully.
ClosestNoPath Path doesn't exist, returns path to closest point.
ClosestOutOfRange Goal is out of MaxDistance range, returns path to closest point you can reach within MaxDistance.
FailStartNotEmpty Failed to compute path; the starting point is not empty.
FailFinishNotEmpty Failed to compute path; the finish point is not empty.

The status can be used to determine if you want to recompute the path with different parameters (such as a new end point), or would like the character to choose a different behavior.

Using a path[edit]

A Path object contains a table of points that can be accessed with GetPointCoordinates. This table stores the series of Vector3's that can be followed in order to complete the given path. Keep in mind the PathfindingService does not create any kind of motion, it just shows which points to move to on the path. To create movement, consider using a movement functions like MoveTo, moving the object with CFrames, or even create physical motion with a force via BodyVelocity or BodyForce.

Rechecking Path[edit]

ROBLOX games are often dynamic, and things might get in the way of a path after it has been calculated. If this happens, the path should be recalculated so the object moving along the path can continue. That said, recalculating a path is an expensive operation, and should be seldom done.

A good way to check if a path needs to be recalculated is with CheckOcclusionAsync. This function will check to see if any point on the path (starting with a point you specify) is obstructed. If so, the function will return the first index of the path where the obstruction occurs. At this point you can recompute the path from the object's current position, or can compute a path from the last obscured point.

EmptyCutoff[edit]

You may notice that pathfinding can have difficulty with thin floors. Thin floors are sometimes considered empty because they don't take up much space. This is due to the way pathfinding calculates empty space in the game world. To find a path, the service divides the world into a grid of 4x4x4 cubes called voxels. It then checks on how empty the voxels are to determine what an available path is. By default, if a voxel is less than 16% filled then it is considered empty.

To solve the issue of thinner floors, there are two solutions you can employ. Firstly, you can simply make the floors thicker (which will fill up more of the space so the voxels are considered full). The other solution is to change the EmptyCutoff to a lower value. This has the advantage of preserving the look of your level (if you need thin floors), but it does mean the paths in your level may change.

Example[edit]

Click to move is a very common feature in games, but can be very troublesome when simply using MoveTo. Using the PathfindingService, we can calculate a series of points to MoveTo so that the character does not get stuck.

You can see this place in action here.

Select
-- In LocalScript in StarterPack:
local player = game.Players.LocalPlayer
local mouse = player:GetMouse()
 
local followingPath = false
mouse.Button1Down:connect(function()
	if not followingPath then
		followingPath = true
 
		-- Ask the server to calculate a path for us
		local points = game.ReplicatedStorage.RemoteFunction:InvokeServer(mouse.Hit.p)
 
		for _, point in ipairs(points) do
			print("moving to: ", point)
			player.Character.Humanoid:MoveTo(point)
			-- Don't try moving to the next point until we are close enough to the current point in the path
			repeat
				distance = (point - player.Character.Torso.Position).magnitude
				wait()
			until distance < 3
		end
		followingPath = false
	end
end)
Select
-- In Script in ServerScriptService
 
local pathfindingService = game:GetService("PathfindingService")
local pointModel = Instance.new("Model")
pointModel.Name = "Points"
pointModel.Parent = game.Workspace
 
-- Setup remote function
local remoteFunction = Instance.new("RemoteFunction")
remoteFunction.Parent = game.ReplicatedStorage
 
function visualizePath(path)
	-- clear old path visualization
	pointModel:ClearAllChildren()
 
	-- calculate new visualization	
	local points = path:GetPointCoordinates()
	for _, point in ipairs(points) do
		local part = Instance.new("Part")
		part.FormFactor = Enum.FormFactor.Custom
		part.Size = Vector3.new(1,1,1)
		part.Position = point
		part.Anchored = true
		part.CanCollide = false
		part.Parent = pointModel
	end
end
 
game.ReplicatedStorage.RemoteFunction.OnServerInvoke = function(player, target)
	local start = player.Character.Torso.Position
	local path = pathfindingService:ComputeRawPathAsync(start, target, 500)
 
	visualizePath(path)
	-- Check to see if the path was computed correctly
	if path.Status == Enum.PathStatus.FailStartNotEmpty or path.Status == Enum.PathStatus.FailFinishNotEmpty then
		print("Compute failed")
		return {}
	end
 
	return path:GetPointCoordinates()		
end