Pathfinding NPC Advanced [PART 1]

In this part of this tutorial, I'll show you how to pathfind using PathfindingService, move a NPC and prevent to block the NPC.

by LuckyScripters

Author Avatar

Introduction

Pathfinding is a system implemented by roblox to find the fastest path to a destination. We use the PathfindingService service. When you use that you'll get a table of waypoints where NPC will move to reach the destination.

Setup

For this tutorial, you'll need to create a destination and obstacles. You'll also need a NPC.

Here is an example

image|100x100, 150%

Here is the link of the NPC I used

https://www.roblox.com/library/3991217109/Simple-Animated-R6-NPC

PS: If you want the navigation mesh with arrows and parts walkable with Pathfinding colored purple, you need to go to File > Studio Settings > Studio (Tab) and you'll see a box named Show Navigation Mesh at the bottom in the category Visualization.

Adding a script

Well now you need to create a script inside the NPC. Name it whatever you like.

image|100x100

A new window appeared with a small code. Remove this the code inside the script.

Initializing variables

First we have to initialize variables. We'll have to define the service, the destination, the NPC, the NPC's Humanoid and NPC's PrimaryPart. We'll also define three others variables which are Waypoints for the table of waypoints, IsBlocked to check if the current path has a waypoint blocked, if it has, then we restart the pathfinding and CurrentWaypoint for the current waypoint where the NPC has to move, this will also be useful when the path has a blocked point.

local PathfindingService = game:GetService("PathfindingService")

local NPC = script.Parent
local Torso = NPC:WaitForChild("Torso")
local Humanoid = NPC:WaitForChild("Humanoid")

local Destination = workspace:WaitForChild("Destination") 
-- Replace with your own destination part.

local Waypoints = {}
local IsBlocked = false
local CurrentWaypoint = 2

Creating the Path

To create a path you need to use the function CreatePath() in PathfindingService which has an argument AgentParameters. This argument is a table containing the settings of the path. In these settings you have in the order:

local Path = PathfindingService:CreatePath({
	AgentRadius = Torso.Size.X, -- The size of his torso. Who cares about arms, because they can go through walls?
	AgentHeight = NPC:GetExtentsSize().Y, -- This is the height of his body.
	AgentCanJump = true, -- Can he jump? Yes he can.
	AgentanClimb = true, -- Can he climb 1 stud high trussparts? Of course he can.
	WaypointSpacing = 4, -- The 4 is the distance between each waypoint.
	Costs = {
 -- I'll talk about this in the PART 2 of this tutorial.
		Grass = 0, -- I love grass, my life will be better if I can touch grass.
		Water = math.huge -- I don't like water. I don't want to go through it.
	}
})

Computing the Path

Now we have to connect the path from the Start to the Destination using ComputeAsync() in Path (Async mean that this function will yield the current thread), the Start will be the current position of the Torso of the NPC and the Destination will be the destination that we set before. We'll create a new function and use protected call (pcall) to prevent for getting errors during computation.

local function ConnectPath()
	local Success, Error = pcall(Path.ComputeAsync, Path, Torso.Position, Destination.Position)
	
	if Success then
		return true
	end
	return false
end

This function will return true if the computation has been a success or false is the computation has failed.

Blocked Path

We to create an another function that will send you the nearest blocked waypoint which is CheckOcclusionAsync in Path. This function has one argument Start. This argument is the begining of the scan of each waypoint. It starts from the waypoint indicated by the Start argument and finishes to the last waypoint in the table Waypoints. This function contain also Async and this function yield for a long time, that can makes lag for our script. So we'll create an another thread later to prevent for yielding main thread (Script). We'll also use protected call.

local function GetNearestBlockedWaypoint()
	local Success, NearestBlockedWaypoint = pcall(Path.CheckOcclusionAsync, Path, CurrentWaypoint) -- The scan will start from the CurrentWaypoint, because we don't care about old waypoints reached by NPC.

	if Success and NearestBlockedWaypoint ~= -1 then
		return NearestBlockedWaypoint
	end
	return -1
end

-1 mean that there are no waypoints blocked and the current path is safe.

Main Function

Now we'll make the main function where we'll make the NPC move, jump if it's needed and re-compute path if it's blocked.


local function MoveWithPathfinding()
	local IsConnected = ConnectPath()
	
	if IsConnected and Path.Status == Enum.PathStatus.Success then -- Path.Status is an Enum. If it's Success, then the path is connected and we can gets waypoints if it's not Success that mean the path is connected but no ways to go to the destination.

		Waypoints = Path:GetWaypoints() -- Getting waypoints.
		IsBlocked = false -- We have to set to false everytime we call this function.
		CurrentWaypoint = 2 -- Why 2 and not 1? 1 is the NPC's current position. We don't want him to go back to his starting point.
		
		if Waypoints[CurrentWaypoint] then -- Check if the current waypoint exists in the table
			if Waypoints[CurrentWaypoint].Action == Enum.PathWaypointAction.Jump then -- Check if the NPC has to jump to reach the current waypoint. You can remove that if you set "AgentCanJump" to false.
				print("Jump is needed to reach this waypoint.")
				Humanoid.Jump = true
			end
			Humanoid:MoveTo(Waypoints[CurrentWaypoint].Position) -- Moving to the current waypoint.
		end
		if IsBlocked then -- Check if it's blocked
			print("A waypoint has been blocked, re-computing path.")
			MoveWithPathfinding() -- If it's blocked, we re-compute the path.
		end
	else
		print("Failed to connect path.")
	end
end

Move Finished

We'll use Humanoid.MoveToFinished to check when the NPC reached the waypoint. This event contain one parameter Reached. When this is fired that mean that the NPC has reached the current waypoint or not. If he reached then we have to increment the CurrentWaypoint and check if the CurrentWaypoint isn't the last waypoint of the table Waypoints. We also have to check if a waypoint isn't blocked. If he didn't reach, we have to teleport him to the CurrentWaypoint.

Humanoid.MoveToFinished:Connect(function(Reached)
	if Reached then -- CurrentWaypoint reached?
		if CurrentWaypoint < table.maxn(Waypoints) then -- table.maxn(Waypoints) is the number of waypoints in the table. We have to check if it wasn't the last waypoint of the table Waypoint. If it was then he reached the destination.
			CurrentWaypoint = CurrentWaypoint + 1 -- Going to the next waypoint
			if IsBlocked then -- Check if it's blocked
				print("A waypoint has been blocked, re-computing path.")
				MoveWithPathfinding() -- If it's blocked, we re-compute the path.
			end
			if Waypoints[CurrentWaypoint] then -- Check if the current waypoint exists in the table
				if Waypoints[CurrentWaypoint].Action == Enum.PathWaypointAction.Jump then -- Check if the NPC has to jump to reach the current waypoint. You can remove that if you set "AgentCanJump" to false.
					print("Jump is needed to reach this waypoint.")
					Humanoid.Jump = true
				end
				Humanoid:MoveTo(Waypoints[CurrentWaypoint].Position) -- Moving to the current waypoint.
			end
		else
			print("Destination reached!")
		end
	else -- I failed to reach the CurrentWaypoint.
		print("Failed to reach the CurrentWaypoint.")
		NPC:PivotTo(CFrame.new(Waypoints[CurrentWaypoint].Position + Vector3.new(0, NPC:GetExtentsSize().Y / 2, 0))) -- Teleporting to the CurrentWaypoint.
		MoveWithPathfinding()
	end
end)

GetExtentSize() is a function that returns the size of the smallest bounding box that contains all of the BaseParts in the Model.

Creating Blocked Thread

To make a new thread we'll use corountine.wrap and setup blocked path. We'll check if the BlockedWaypoint is near to the CurrentWaypoint in a loop.

coroutine.wrap(function()
	while true do
		if Waypoints[CurrentWaypoint] then
			local NearestBlockedWaypoint = GetNearestBlockedWaypoint() -- Getting the nearest waypoint blocked

			if NearestBlockedWaypoint ~= -1 then -- Check if the result isn't -1 (No waypoints blocked)
				if CurrentWaypoint <= NearestBlockedWaypoint and CurrentWaypoint + 2 >= NearestBlockedWaypoint then -- We have to check if the BlockedWaypoint is near to the CurrentWaypoint. 2 is the number of waypoints to skip.
					IsBlocked = true
				end
			end
		end
		task.wait()
	end
end)()

Initialization

This is the final part of the script. In this part we have to use SetNetworkOwner in the Torso and set to nil to prevent for getting lag and break pathfinding. We'll also call the MainFunction to start the pathfinding.

Torso:SetNetworkOwner(nil)

MoveWithPathfinding()

Final Result

Here is the final result. Yellow spheres are waypoints where the NPC has to move and red spheres are waypoints where the NPC has to jump.

image|100x100, 150%

End of Tutorial & Extras

Thank you for reading through the tutorial. I hope this tutorial has helped you understand how pathfinding works. Also I'll probaby make others parts of this tutorial.

I want to tell you that this is one method of pathfinding, because there are others pathfinding functions like PathfindingService:FindPathAsync(Start, Finish) which is very similar to the function I told you about but there are no settings like AgentParameters. There are also deprecated functions like PathfindingService:ComputeRawPathAsync(Start, Finish, MaxDistance) or PathfindingService:ComputeSmoothPathAsync(Start, Finish, MaxDistance). These two functions are similar but deprecated. There where returning other Enum.PathStatus items like:

Here is an example DO NOT COPY THIS, JUST READ IT.

Enum.PathStatus.ClosestNoPath, -- When something block the path from the Start to the Finish, the Finish will be the place where the path has been blocked.
Enum.PathStatus.ClosestOutOfRange, -- When the distance between the Start and Finish is greater than the MaxDistance defined in the function.
Enum.PathStatus.FailStartNotEmpty, -- When the Start point is blocked and it's impossible to compute path.
Enum.PathStatus.FailFinishNotEmpty, -- When the Finish point is blocked and it's impossible to compute path.

View in-game to comment, award, and more!