How To Use Roblox Pathfinding Service 2.0

To my beloved old readers of this post

Please read this post of this topic to understand why I had decided to revamp this whole tutorial.
How To Use Roblox Pathfinding Service 2.0 - #6 by ItzMeZeus_IGotHacked

PATHFINDING SERSVICE TUTORIAL 2.0 IS FINALLY HERE!

Ladies and gentlemen welcome back to another Roblox scripting tutorial where in today’s tutorial we are going to talk about how you can use Pathfinding service better in Roblox!

Let’s see where we left off last time…

image

Oh.

It’s been more than a year since this topic was mentioned on here… :skull:

December 20th

Well, it’s been a year since I said that and everyone was wondering where is it…

Well here it is! I was just being a lazy couch potato and can’t think of new ideas on how to make this more interesting, until some new additions to the pathfinding service, such as the new Pathfinding Modifiers.

So buckle your seatbelts up everyone. because today we are going to learn:

If this is your first time learning Pathfinding service, you can still read this tutorial as I’ve planned this tutorial to rework on the old one since the old script from the old tutorial is inefficient and buggy… heh. Without any further ado, let’s get rolling!

Getting started

This section will cover the nitty-gritty of utilising PathfindingService to its maximum. At the end of this section, you will be able to script a humanoid that can walk to a certain position and smartly create a new path to the position again if it is blocked by another part when traversing through the path.

  1. Get a working R6 or R15 dummy model in your game. You can do this by using the Rig Builder plugin provided by Roblox Studio default.

image

image

im gonna use r15 in this case

  1. Most important step, unanchor every single BaseParts in the dummy model. This is to ensure that the model can walk without any anchored parts connecting it to prevent it from walking. You can simply click the model in the explorer tab and unanchor the model.

  2. Insert a server script in the model and declare a few variables such as referring Pathfinding service itself, the model, humanoid instance and HumanoidRootPart.

local PathfindingService = game:GetService("PathfindingService")

local model = script.Parent
local humanoid = model.Humanoid
local humanoidRootPart = model.HumanoidRootPart
  1. Now, we will use PathfindingService:CreatePath() function. As its name suggests, it basically creates a Path instance which we are going to use. Don’t mind the agentParameters argument in the function. We will cover that later. We are also going to define some constants which is useful for our functions soon. We will also declare a variable named reachedConnection which will be useful soon.
local MAX_RETRIES = 5
local RETRY_COOLDOWN = 5

local model = script.Parent
local humanoid = model.Humanoid
local humanoidRootPart = model.HumanoidRootPart

local path = PathfindingService:CreatePath()
local reachedConnection

– REVAMP STARTING HERE –

  1. Create a function named walkTo(). This function will be the brain of the operation of this whole thing. Add two parameters, targetPosition and yieldable (determines whether or not to yield the script until the humanoid has reached the destination.) After creating the function, call it with the respective arguments. I have created a base part named EndGoal under workspace.
local function walkTo(targetPosition, yieldable)
	
end

walkTo(workspace.EndGoal.Position, true)
  1. Now let’s dive into the main part! To create or compute the path between two points, we have to use path:ComputeAsync() on the path instance we have created through PathfindingService:CreatePath(). The first argument of the function is the starting point (I use the root part’s Position) and the second argument is an ending point (targetPosition parameter), both in terms of Vector3. This function makes network calls, in other words, this function can fail from time to time due to network errors (or just backend issues that you can’t control), so in order to counter this, we will use pcall() along with a retry system. If the function has retried a lot of times and it still fails, then we will not do anything but warn the error message in the output.
local function walkTo(targetPosition, yieldable)
	local RETRY_NUM = 0 
	local success, errorMessage

	repeat
		RETRY_NUM += 1 -- add one retry
		success, errorMessage = pcall(path.ComputeAsync, path, humanoidRootPart.Position, targetPosition)
		if not success then -- if it fails, warn the message
			warn("Pathfind compute path error: "..errorMessage)
			task.wait(RETRY_COOLDOWN)
		end
	until success == true or RETRY_NUM > MAX_RETRIES
	
	if success then -- if computing the path has no issues
		
	else -- if retry chance is maxed out
		warn("Pathfind compute retry maxed out, error: "..errorMessage)
		return
	end

end
About pcall()

Notice that I did not use the pcall function like so:

pcall(function()
    ...
end)

This is because it creates another function to do another function, which is unnecessary work. So I just typed out like that to save some spaces. Also notice that I did not use : when indexing the ComputeAsync. : is not suitable to be used here so we use ., and then the second argument of that pcall is the path instance itself because

path.ComputeAsync(path, ...)

is the same thing as

path:ComputeAsync(...)
  1. If the function was successfully called with no issues, then we must check if the function was able to find a path between the two points. If it can compute a path, then Path.Status will be Enum.PathStatus.Success. Hence, we can get the path’s waypoints, a table consisting of all points the humanoid must traverse through to get to the end goal, and cycle through each waypoint. We will create a variable which tells the humanoid to move to the corresponding waypoints. We will then use humanoid:MoveTo() to tell the humanoid to walk to that waypoint’s position, which can be accessed by getting the waypoint’s position property shown in the code below. If the waypoint requires the humanoid to jump, we will set humanoid.Jump to true.
    if success then
		if path.Status == Enum.PathStatus.Success then
			local waypoints = path:GetWaypoints()
			local currentWaypointIndex = 2 -- not 1, because 1 is the waypoint of the starting position.

			humanoid:MoveTo(waypoints[currentWaypointIndex].Position) -- move to the nth waypoint
			if waypoints[currentWaypointIndex].Action == Enum.PathWaypointAction.Jump then -- if it requires the humanoid to jump
				humanoid.Jump = true
			end
			
		else -- if the path can't be computed between two points, do nothing!
			return 
		end
	else -- this only runs IF the function has problems computing the path in its backend, NOT if a path can't be created between two points.
		warn("Pathfind compute retry maxed out, error: "..errorMessage)
		return
	end
  1. Awesome! But we only told the humanoid to move one waypoint, so how do we cycle through the whole waypoints? If you think of using for loops, you are right! BUT, for loops are not suitable in this case. I will tell you why later. Instead, we will use an event called humanoid.MoveToFinished. This basically gets fired every time the humanoid has reached a position called by humanoid:MoveTo(). In this case, this event gets fired every time it has reached a waypoint. When the event fires, we want to check if it has reached the waypoint in time (8 seconds) and has yet to reach the end goal, then we will tell the humanoid to move to the next waypoint.
    if path.Status == Enum.PathStatus.Success then
			local waypoints = path:GetWaypoints()
			local currentWaypointIndex = 2
			
			if not reachedConnection then
				reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
					if reached and currentWaypointIndex < #waypoints then
						currentWaypointIndex += 1
						
						humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
						if waypoints[currentWaypointIndex].Action == Enum.PathWaypointAction.Jump then
							humanoid.Jump = true
						end
                    end
				end)
			end
			
			humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
			if waypoints[currentWaypointIndex].Action == Enum.PathWaypointAction.Jump then
				humanoid.Jump = true
			end
			
		else
			return 

And we are done! For now. If your script has no issues and run the game, your humanoid would start walking to its assigned end goal.

Works smoothly, but there are some issues you have probably noticed besides the video if you pay attention closely.

Firstly in the video, the humanoid model walks to the end goal without playing any idle or running animation. This is because we don’t have a script that handles the animation of the script. To solve this issue, we can script a-

WE DO NOT WANT TO SCRIPT A STUPID ANIMATION SCRIPT

Okay, that’s fine! We have another method which is as shortcut. What we can do is we can “steal” the animation script inside of our character’s player model. All you need to do is run the game with your character, find your character model in Workspace, find a script called Animate, copy it, stop the game and paste it in workspace. You should notice there are also other things parented underneath it, all you need to do is delete the BindableEvent named PlayEmote, and then transfer the whole script into a server script since local scripts don’t work and you are done.

"too long im too lazy"

Sure, I have uploaded the scripts required to handle the animations. All you need to do is just select the script according to your rig type, place it under your humanoid model and let it do all the magic.

https://create.roblox.com/marketplace/asset/13450938683/Animate-R15
https://create.roblox.com/marketplace/asset/13450937287/Animate-R6

Onto the next issue, notice that our connection variable reachedConnection never disconnects. This can cause unnecessary memory leaks when something is no longer needed. Memory leaks can cause your game to use up more memory which is unnecessary and jamming up the performance. So what do we do?

That’s right! We have to disconnect the event when the path has been cycled. To do this, we can add an else statement inside the connected function where we tell the script to disconnect the event.

				reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
					if reached and currentWaypointIndex < #waypoints then
						currentWaypointIndex += 1
						
						humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
						if waypoints[currentWaypointIndex].Action == Enum.PathWaypointAction.Jump then
							humanoid.Jump = true
						end
					else
						reachedConnection:Disconnect()
						reachedConnection = nil -- you need to manually set this to nil! because calling disconnect function does not make the variable to be nil.
					end
				end)

Memory leaks is a wide and challenging topic to understand and overcome (at least for me), if you are interested in learning more, you can check out this awesome tutorial.

Furthermore, you will notice that our dummy model is unable to handle paths that are blocked, as shown in the video below:

This might not be an issue if your NPC is only moving from one location to another location for only one time and the path is inaccessible by other things, but, for the sake of this tutorial, we will combat all normal and common kinds of issues you have to deal with in PathfindingService.

So how do we approach to this issue? Luckily, there’s an event called Path.Blocked for every Path object created through the service. This event, you guessed it, basically fires every time the path is being blocked by a physical part, so all we can do is, every time this event gets fired, we will tell the bot to stop cycling through its currently assigned path, compute a new one with the same arguments, and then cycle that newly created path.

But here’s the problem, the event does not know whether the waypoint of the path being blocked has been cycled through by the bot. Thankfully, the connected function of the event provides an argument we can use, that states the number of waypoint that is currently being blocked. For example, if there is a part blocking between the 5th and 6th waypoint, the event will fire and give the number 6 with extra dip, a number 7 as the argument in the connected function. To take advantage of this, we can check whether the current waypoint index is smaller than this number, and if it is, we will tell the bot to stop cycling through the current path and follow the newly computed one. Otherwise, we will do nothing as that waypoint has been cycled through.

  1. Create a new variable called pathBlockedConnection under the variable reachedConnection.
local path = PathfindingService:CreatePath()
local reachedConnection
local pathBlockedConnection
  1. In the function right after setting up the reachedConnection connection, set pathBlockedConnection to the Path.Blocked event and connect it to a function. Make sure you create a parameter that represents the blocked waypoint index. Go ahead and set up the function with the applied description of it I just have just given.
			pathBlockedConnection = path.Blocked:Connect(function(waypointNumber)
				if waypointNumber > currentWaypointIndex then -- blocked path is ahead of the bot
					reachedConnection:Disconnect() -- disconnect these events to prevent memory leaks
					pathBlockedConnection:Disconnect()
					reachedConnection = nil
					pathBlockedConnection = nil
					walkTo(workspace.EndGoal.Position, true) -- compute and cycle new path
				end
			end)

Since pathBlockedConnection also contains an event like how reachedConnection has as well, you have to disconnect it and set it to nil when it is no longer used, so apply this logic also when reachedConnection is unnecessary.

					if reached and currentWaypointIndex < #waypoints then
						currentWaypointIndex += 1
						
						humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
						if waypoints[currentWaypointIndex].Action == Enum.PathWaypointAction.Jump then
							humanoid.Jump = true
						end
					else
						reachedConnection:Disconnect()
						pathBlockedConnection:Disconnect()
						reachedConnection = nil
						pathBlockedConnection = nil
					end

Give it a test and it should be working!

One thing that I have totally forgotten is the yieldable parameter in our function. We are going to need to implement a function for that.

  1. At the top of the script where the constant variables lives, creating a new constant variable named YIELDING, this will contain a boolean value that tells us if the function will yield. This must be in the main scope of the script and not the local scope as re-computations of new paths when the old path is blocked needs to refer it.
-- CONSTANTS --
local MAX_RETRIES = 5
local RETRY_COOLDOWN = 5
local YIELDING = false
  1. In the main function after the two setups of the connections, we will use a repeat until loop to yield the main thread when the function needs to yield, until YIELDING becomes true.
			humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
			if waypoints[currentWaypointIndex].Action == Enum.PathWaypointAction.Jump then
				humanoid.Jump = true
			end

			if yieldable then
				YIELDING = true
				repeat 
					task.wait()
				until YIELDING == false
			end
		else
  1. We have to set YIELDING to false at some part in our script so that it can stop yielding and move onto the next parts of the script, so we have to do that when the path has been fully cycled through.
					else
						reachedConnection:Disconnect()
						pathBlockedConnection:Disconnect()
						reachedConnection = nil
						pathBlockedConnection = nil
						YIELDING = false
					end

And that’s actually it for the basic usage of Pathfinding service. Here’s the full code.

Full Code
-- SERVICES -- 
local PathfindingService = game:GetService("PathfindingService")

-- CONSTANTS --
local MAX_RETRIES = 5
local RETRY_COOLDOWN = 5
local YIELDING = false

local model = script.Parent
local humanoid = model.Humanoid
local humanoidRootPart = model.HumanoidRootPart

local path = PathfindingService:CreatePath()
local reachedConnection
local pathBlockedConnection

local function walkTo(targetPosition, yieldable)
	local RETRY_NUM = 0
	local success, errorMessage

	repeat
		RETRY_NUM += 1
		success, errorMessage = pcall(path.ComputeAsync, path, humanoidRootPart.Position, targetPosition)
		if not success then
			warn("Pathfind compute path error: "..errorMessage)
			task.wait(RETRY_COOLDOWN)
		end
	until success == true or RETRY_NUM > MAX_RETRIES

	if success then
		if path.Status == Enum.PathStatus.Success then
			local waypoints = path:GetWaypoints()
			local currentWaypointIndex = 2

			if not reachedConnection then
				reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
					if reached and currentWaypointIndex < #waypoints then
						currentWaypointIndex += 1

						humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
						if waypoints[currentWaypointIndex].Action == Enum.PathWaypointAction.Jump then
							humanoid.Jump = true
						end
					else
						reachedConnection:Disconnect()
						pathBlockedConnection:Disconnect()
						reachedConnection = nil
						pathBlockedConnection = nil
						YIELDING = false
					end
				end)
			end

			pathBlockedConnection = path.Blocked:Connect(function(waypointNumber)
				if waypointNumber > currentWaypointIndex then
					reachedConnection:Disconnect()
					pathBlockedConnection:Disconnect()
					reachedConnection = nil
					pathBlockedConnection = nil
					walkTo(workspace.EndGoal.Position, true)
				end
			end)

			humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
			if waypoints[currentWaypointIndex].Action == Enum.PathWaypointAction.Jump then
				humanoid.Jump = true
			end

			if yieldable then
				YIELDING = true
				repeat 
					task.wait()
				until YIELDING == false
			end
		else
			return 
		end
	else
		warn("Pathfind compute retry maxed out, error: "..errorMessage)
		return
	end

end

walkTo(workspace.EndGoal.Position, true)

Agent Parameters and Pathfinding Modifiers

Through PathfindingService, we are also exposed to some features that allow us to customize our path to make it look more “smarter”. At its most basic form, Pathfinding service only computes a path with the shortest distance as much as possible. It ignores whether or not if the path that is computed is suited for your expectations and preferences.

But consider this:
image

If we were to use our script and make our character model to walk to the red part, it would walk in a straight line like so.
image

But what if we wanted our character to move along the blue lines?
image

This is where PathfindingModifiers come in! They are modifiers that allows PathfindingService to compute paths that suits our preferences. Not only that, we can also adjust some other properties that affects the final path as well.

To understand what they are, meet the AgentParameters dictionary. If you look at the developer hub, you’ll find information about it, but I will explain all of its properties simply here:

  • AgentRadius key determines the radius of our character. This is useful if we want to keep up a minimum separation distance between our character and its surrounding obstacles. Default value is 2, which is the default radius value of all rigs in Roblox. Anything higher than the default value will make the character have a minimum separation distance between obstacles with this formula: Separation Distance = Value - 2 studs

Default value = 2
image

AgentRadius = 3
image

  • AgentHeight key is self explanatory. The default value of 5 is the default height for all rigs in Roblox.

  • AgentCanJump key is also self explanatory. Determines whether or not our character can jump. Default value is true.

  • AgentCanClimb key determines whether or not the humanoid can climb (only using Roblox’s truss part, you have to program your own climbing logic and use PathfindingLinks if you are not using the truss part!) . Default value is false.

  • WaypointSpacing key determines the minimum amount of spacing between waypoints in a straight line(waypoints around a corner does not count). Default value is 4. If set to math.huge, it will create the least amount of waypoints whilst still providing the shortest path as much as possible. NOTE that changing this value does not respect the reached parameter/argument mentioned in the Humanoid.MoveToFinished connection. For instance, if you make each waypoints too far away from each other in a path, the value will return false even when your humanoid reaches the waypoint in more than 8 seconds.

WaypointSpacing = 8

WaypointSpacing = math.huge

i am unsure if this will increase performance soo lol

  • Costs key contains a dictionary for our PathfindingModifiers.

Feel free to mess around with the other keys, since they are easy to learn. This part of the tutorial will mainly focus on the PathfindingModifiers object in the Costs key.

When I first learnt about this, I was puzzled. Thankfully, @Hexcede managed to brighten me up!

So what do these things do? These things are also known as “multipliers”, meaning that if I set a higher number in this multiplier, it will make the path “harder” to traverse/walk through it. For instance, imagine a path that has a distance of 100 studs before reaching a goal. If I set a cost of multiplier of 5, this will make it so that this path is 5x harder to traverse. It sounds confusing, but the point here is that it stores a multiplier value that decides on how hard is to get to the goal through this goal.

You might be asking what is this multiplier value multiplying with? Well, we don’t have a clear answer but according to @Hexcede, costs are somewhat directly proportional to distance traveled. This means, the higher the cost, the harder it is for the humanoid to traverse that path. For example, travelling on 30-studs-long path with a cost value of 10 will cost around 300 to travel on, on the other hand a 100-studs-long path with a cost value of 2 will cost 200 to travel on. In conclusion, PathfindingService would prefer the 100-studs-long path because it costs less than 100 by the 30-studs-long path. This allow us to make our humanoids walk on longer paths with a low cost value over short paths with a high cost of values. Imagine a situation where you wanna force a humanoid to walk on the longer path when it has to choose which path to go, each with different lengths in studs. With the Costs parameter, we can make it so that the shorter path costs more than the longer path to travel, which will make pathfinding service to choose the longer path instead.

How is this useful you may ask? Well, just like the problem I have shown earlier, we can make it so that pathfinding service will choose to walk on the Cyan paths by lowering the cost to travel on that part. How do we do that? We can either make it so that the cost of traversing to the goal through the corroded metal part is higher than the cost of the Cyan parts or lower the cost of traversing on the Cyan parts. The way on how we lower the cost is by simply giving it a multiplier value of less than 1, because any number that is multiplied with less than 1 will output a lower number than its original. For instance, 500 x 0.5 = 250.

The addition of this is also as simple as how we implement our method to counter blocked paths. It just needs one variable and that’s it! At the beginning of the script, we create a variable named AgentParameters which contains a dictionary to customize our computed path. Inside it, we will add the Costs key which contains another dictionary to store all the costs to traverse on certain parts/materials. In our case, the Cyan part has a metal material, so we would have to name the key as Metal exactly, then its value will be the multiplier cost. I am going to set this lower than 1 so that pathfinding service would likely choose that material to be appeared in our final path more often.

local Path:Path
local AgentParameters = {
	WaypointSpacing = 4,
	Costs = {
		Metal = 0.1 -- if your material is other than Metal, name that material as the key exactly.
        -- set it to lower than 1 to make the service more likely to include that material in the path
	}
}
Path = PathfindingService:CreatePath(AgentParameters) -- the function receives an optional parameter which contains our AgentParameter dictionary in order to customize our path.

In the Costs dictionary, each key inside it can either be the name of a material, terrain material of a unique ID for a PathfindingModifier object which I will demonstrate later.

And that’s actually it! No catches, no strings no nothing. Let’s compare it with a path without the costs.

Without the dictionary (all costs are the same)

With the dictionary (Metal material cost is 0.1)

Of course, as I’ve said it is not limited to just BasePart materials, it can also be a terrain material or the PathfindingModifier object!

Terrain material
Costs:

PathfindingModifier(can be parented under a BasePart)
image

image
the big corroded metal part contains the pathfinding modifier object
image

Use the Label property as the name of the key.

local AgentParameters = {
	WaypointSpacing = 4,
	Costs = {
		AvoidThis = math.huge -- this will make it so that this part will never be included in the path even though it is the only way to get to the goal
	}
}

The PathfindingModifier object is useful if you want to define a certain region in your game as either traversable or not. To do this, you can define the region using a part, make sure its CanCollide property is false and make it transparent. To define the region as non-traversable, you can set its cost to a higher value according to the Label property of its PathfindingModifier object. Otherwise, check the PassThrough property and the service will mark the region as traversable. You do not have to set its cost in this case. This is extremely useful if you are making a bot to walk through a hinged door in your game.

There is also a PathfindingLink object, which is useful if you wanna trigger a custom event through attachments when your humanoid object is heading to a certain waypoint in the path. This is also useful if you want to make the humanoid traverse to waypoints that are impossible to get to without a special function that makes it possible to traverse. You can check out the developer hub as it has already explains it well, but I will just do one example.

Let’s say I want this humanoid to jump over the gap to get to the red block:

If we use PathfindingService to compute the path, it fails because it is unable to find a clear path for the humanoid to traverse through. BUT, if we use PathfindingLink, this is possible!

  1. First, add two attachments and parent them under 2 parts. In this case, I will put one attachment each in the 2 big parts in the photo I have just shown.

  2. Then, position both attachments to your desired location. As long as it is logical, it will work. In my case, I have to place it close but not touching to each other like so:
    image

This really depends on where you want your humanoid to utilise these special waypoints, so you have to experiment this a lot to find your sweet spot.

  1. Now, add a PathfindingLink object. Give it a name. I’ll call mine JumpGap
    image

  2. In the properties panel, assign Attachment0 and Attachment1 to those attachments. The order does not matter .Name the label as the name of your PathfindingLink object.


    forgot to name my Label

Also just in case if you are wondering, IsBidirectional is the reason why the order of the attachments does not matter. If it is false, then Attachment0 is the starting point and Attachment1 is the ending point.

  1. Back inside the script, let’s create a AgentParameters dictionary, where inside the Cost dictionary, we will add our label with its respective value. I’ll give it a 2 so that it prefers walking instead of jumping assuming the gap is connected with a part.
-- CONSTANTS --
local MAX_RETRIES = 5
local RETRY_COOLDOWN = 5
local YIELDING = false
local AGENT_PARAMETERS = {
	AgentCanClimb = true, -- this does not matter. i just want my humanoid to jump.
	Costs = {
		JumpGap = 2
	}
}
  1. To make this easier for you to add and update new PathfindingLink objects you may add in the future, we will create a dictionary which stores a key equivalent to a label’s name, and its respective value which is a function that stimulates the logic of that label. In our case, we want the humanoid to jump when it has reached that special waypoint (created by the first attachment), and then move to its connected waypoint (the second attachment).
-- SPECIAL WAYPOINTS FUNCTIONS --
local SPECIAL_WAYPOINTS = {
	JumpGap = function(model, waypoints, currentWaypointIndex) -- every other logic functions may need to use these arguments for their own logic.
		local humanoid = model:FindFirstChildWhichIsA("Humanoid")
		
		if humanoid then
			humanoid.Jump = true
			humanoid:MoveTo(waypoints[currentWaypointIndex + 1].Position)
		end
	end,
}
-- remember, key = label, value = its logic function.
  1. In the reachedConnection variable which stores the humanoid.MoveToFinished event with its connected function, after we add one to currentWaypointIndex, we will need to check if this next waypoint’s label (PathWaypoint.Label) can be found in the SPECIAL_WAYPOINTS table, and if such value exists, we will call that function, simple as that! Otherwise, we just tell it to move to the next waypoint.
					if reached and currentWaypointIndex < #waypoints then
						currentWaypointIndex += 1
						
						if SPECIAL_WAYPOINTS[waypoints[currentWaypointIndex].Label] ~= nil then
							SPECIAL_WAYPOINTS[waypoints[currentWaypointIndex].Label](model, waypoints, currentWaypointIndex)
						else
							humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
						end

And you are basically done!

This pretty much concludes how you can modify paths for your own liking using some special features offered by PathfindingService`.

Bot AI Chasing

I’m sure all of you have at least play a game where you have to escape from a threat before it gets you, take the game Piggy for example. In the game, a bot would spawn and pathfind its way to chase the closest player near to itself and when it touched the player, the player dies.

How does this work using pathfinding script? If you’ve dissected a bot AI chasing model, you’ll notice that most of them contains a function that gets the closest player’s character’s torso, and then pathfind it by only making the humanoid walking to either the second or third waypoint of the path. Why is that?

Based on my experience, I am sure it is something to do with whatever the bot is trying to get to. The script that I have taught you guys just now isn’t suitable to do such things. Here are a number of reasons:

  • The script itself generally is only used for bots that is going to pathfind a location that’s fixed. If you are going to make a pathfind script for a bot that’s going to constantly chase a player that its position is dynamically changed every few seconds, it’s better if we just don’t make the bot walk through the whole series of the path, instead we want it to only walk to either the second or third waypoint of the path. With this, the bot doesn’t have to wait until it finishes looping through the whole path and start going to another position after that.
  • The script can be severely deoptimized if not used right. The main reason why my previous pathfinding script in my last Pathfinding Service usage tutorial sucks and deoptimized is because of how frequently it is called per second. Previously, I would use a repetitive loop that spawns around 60 threads per second using RunService.Heartbeat event. The result? The script ended up calling that same function around 60 times per second and that is not good. To add the salt on the wound, there is a for loop in the function which would be ran around 60 times per second or the same rate as how many times the function was called per second. This ended up making our bot look buggy and stuttering. So how do we solve this? Simple, with the logic I’ve explained in the previous reason, we will need a loop that waits for the humanoid to stop walking to the waypoint before executing the same function over and over again. while task.wait() do loop is already good enough as it doesn’t spawns multiple threads per second and it yields if any code inside it has a yielding function.
  • The events (for handling blocked path and waypoint reached) connected inside the script would just add more performance drop in our script, but without it how can we solve such problems? Well, the first reason I’ve stated is already enough to counter this problem. Since the position of the player’s character model is constantly changing, we would compute a new path every time the while loop has done executing the function (when the yield is finished), and when we compute a new path, we would have a new series of waypoints with different positions. By computing a new path everytime our bot has reached to the second/third waypoint of the previous path, the computation of the path would avoid any obstacles and go around it to make our bot get to us without any issues.

Now that I’ve explained the reasons why, it’s time to create a new script. The flow of the script is simple. Everytime the while loop runs, it will call a function which tells the script to detect any closest player to the bot itself. If it managed to find one, we will tell the script to compute the path to it, and make our bot to only loop/walk through the second or third waypoint of the path before ending the function. Then this whole sequence would be repeated until our bot touches the player and makes the player die. Simple enough.

This part of the tutorial will be out soon.

I hope this tutorial benefits you. If there is any questions, feel free to leave a comment down…

96 Likes

This is absolutely amazing! Can’t wait for AI chasing :grin:

8 Likes

Hi. Great tutorial btw. Can you please add a small update to this regarding PathfindingModifiers.
From extensive testing with them, when you use a Part with a PathfindingModifier, you need to ensure that the CanQuery property is Enabled on the Part, otherwise Pathfinding will ignore the modifier.

Nowhere is this mentioned in any of the Roblox documentation, but after running round in circles, I have found this to be cause of lots of issues with my Pathfinding.

4 Likes

Thanks for the tutorial thing but Im having a problem… When the character walks from waypoint to waypoint, there’s like half a second where the NPC doesnt move… I don’t really know how to descibre it lol (You can see it when Testing with play, not run). I think it’s related to the “for i = RetriesLeft bla bla” thing loop.

Parallel luau for ai tutorial when?

Hello!

You are probably wondering why I am replying to this old tutorial of mine after such a long time.

The reason is simple.

I WAS BEING LAZY!!!

Not only that, I had to focus on my studies in my own school to ensure I was in a perfect state in terms of my academic studies. Then, my previous laptop’s charger killed itself after a power issue at my home. After some indeterminate amount of time, I got a new laptop, and then just started to play some games on it.

So you can basically say I was being lazy and I took a long hiatus from developing. But now here I am, back to business and realise my tutorial’s scripts are BROKEN. Hence, I have to make a full rework of this tutorial so that any future readers can learn from this.

I have learnt a couple of scripting tips and new features of PathfindingService, hence, if you are an old reader of my tutorial (which I indebtedly thank you for reading this), you can tell a huge difference compared to my old tutorial.

That’s all from me! Expect this tutorial to be added with more exciting features that you can do with PathfindingService. In the meantime, have fun scripting.

9 Likes

Hey, sorry for reviving this, but you said we shouldn’t use a for loop, why is that? I don’t believe you ever told us.

amazing post definetly gonna make a module out of this so i can reuse it for future games!