I recently helped a user make a OOP oriented Ai and decided to make a Community Tutorial on it. This tutorial should help you implement PathFindingService and how to turn it into a OOP module so that it can be used in different scripts and such. If the term OOP is foreign to you completely, feel free to skip the OOP part and just read how to use PathFindingService.
How to use PathfindingService
How to use PathfindingService
1. Start off with some Variables
First you're gonna need to start off with some stuff- The PathFindingService
- The NPC model, assumes the model has a humanoid in it
- The NPC’s Humanoid
- Reference to a part named “EndPart” inside “Workspace”
local PathService = game:GetService("PathfindingService")
local NPC = script.Parent
local Humanoid = NPC.Humanoid
local EndPart = workspace.EndPart
Shouldn’t require much explaining but I’ll explain it anyways. The PathService = game:GetService("PathfindingService")
is used to create a path in which the NPC will follow. The NPC
and Humanoid
are references to the NPC model and its Humanoid. And I already explained EndPart from above
2. Make Path that the NPC will follow
Now to actually make the Path
local Path = PathService:CreatePath()
Path:ComputeAsync(NPC.HumanoidRootPart.Position, EndPart.Position)
The local Path = PathService:CreatePath()
is pretty hard to explain unless I add some arguments. So the below code will just be used as an example.
local Path = PathService:CreatePath({ -- default values if table isn't provided
AgentRadius = 2,
AgentHeight = 5,
AgentCanJump = true,
AgentCanClimb = false, -- I think it can only climb truss ladders, if set to true
WaypointSpacing = 4,
Costs = {}
})
PathService:CreatePath()
is basically making a brain for the NPC. We can tell what the brain will do by putting the table of arguments in. Currently AgentCanJump
is true, meaning the NPC knows how to jump. However if we were to change it to AgentCanJump = false
then the NPC wouldn’t know how to jump. I won’t be explaining what the rest does cause the names are pretty self explanatory and because the ones that aren’t self explanatory are pretty hard to word. So I’ll just link to the Documentation, PathfindingService | Documentation - Roblox Creator Hub.
I should also mention the arguments are optional, so you don’t have to include the table or include one of the arguments in order for it to work.
Now onto Path:ComputeAsync()
, this is where we make the path for the NPC to follow. It takes 2 parameters, that being the Starting Position and End Position of someplace. Since we put NPC.HumanoidRootPart.Position
as the first parameter it’ll start at the Position of the NPC’s HumanoidRootPart. And since we put EndPart.Position
as the 2nd parameter, it’ll end at the position of EndPart.
3. Make the NPC actually move through the path
There are a few ways of doing this, you could
- Use a
for i
loop - Use a
for
loop, difference being that it doesn’t increment like the for i loop - Connect a function on Humanoid.MoveToFinished()
For simplicity sake, we’ll just use the 2nd one.
if Path.Status == Enum.PathStatus.Success then -- Checks if path is walkable
local Waypoints = Path:GetWaypoints()
Waypoints[1] = nil -- removes the first index cause it's the StarPos from SetPath(), which is usually the Current Position of the NPC
for _,PathPoint:PathWaypoint in Waypoints do
if PathPoint.Action == Enum.PathWaypointAction.Jump then -- checks if the the path point is suppose to be jumping
Humanoid.Jump = true -- Makes character jump
end
Humanoid:MoveTo(PathPoint.Position) -- moves the NPC to the waypoint
if not Humanoid.MoveToFinished:Wait() then -- When the NPC finishes moving to a position we told it to go, it'll either return true or false, true if it made it, false if 8 seconds has passed and it's still trying to walk to it
warn(NPC.Name,"got stuck somewhere")
break
end
end
end
The if Path.Status == Enum.PathStatus.Success then
checks if path is actually walkable. Not running the below code if it isn’t
The local Waypoints = Path:GetWaypoints()
as it says, gets the PathPoints, which is all the position the NPC will follow to get to it’s final destination. Basically you can’t always draw a straight line to the end goal, you’ll sometimes have to jump or make turn at a spot to finally reach the end goal, which is what the PathPoints are.
The for _,PathPoint:PathWaypoint in Waypoints do
loops through the Waypoints table, checking if PathPoint.Action == Enum.PathWaypointAction.Jump
, if it is true, makes the NPC Jump(Humanoid.Jump = true
).
The Humanoid:MoveTo(PathPoint.Position)
actually makes the NPC move to a PathPoint.
The if not Humanoid.MoveToFinished:Wait() then
checks if the NPC made it to the position we told it to follow.
- It will return true if it did reach the position we told it to go, doing nothing and continuing the loop.
- It will return false if it didn’t reach the position we told it to go after 8 seconds pass of it trying to walk to the position. Ending the loop and warning us the NPC didn’t make it to it’s end path
Video in Action:
Entire Script:
local PathService = game:GetService("PathfindingService")
local NPC = script.Parent
local Humanoid = NPC.Humanoid
local EndPart = workspace.EndPart
local Path = PathService:CreatePath()
Path:ComputeAsync(NPC.HumanoidRootPart.Position,EndPart.Position)
if Path.Status == Enum.PathStatus.Success then -- Checks if path is walkable
local Waypoints = Path:GetWaypoints()
Waypoints[1] = nil -- removes the first index cause it's the StarPos from SetPath(), which is usually the Current Position of the NPC
for _,PathPoint:PathWaypoint in Waypoints do
if PathPoint.Action == Enum.PathWaypointAction.Jump then -- checks if the the path point is suppose to be jumping
Humanoid.Jump = true -- Makes character jump
end
Humanoid:MoveTo(PathPoint.Position) -- moves the NPC to the waypoint
if not Humanoid.MoveToFinished:Wait() then -- When the NPC finishes moving to a position we told it to go, it'll either return true or false, true if it made it, false if 8 seconds has passed and it's still trying to walk to it
warn(NPC.Name,"got stuck somewhere")
break
end
end
end
Boom you now got a working PathFinding Ai. Also here’s a function to use instead of running the script how it is.
Better script because it uses function so it's reusable I guess
local PathService = game:GetService("PathfindingService")
local NPC = script.Parent
local Humanoid = NPC.Humanoid
local EndPart = workspace.EndPart
local Path = PathService:CreatePath()
function FollowPath(StartPos, EndPos)
Path:ComputeAsync(StartPos,EndPos)
if Path.Status == Enum.PathStatus.Success then
local Waypoints = Path:GetWaypoints()
Waypoints[1] = nil
for _,PathPoint:PathWaypoint in Waypoints do
if PathPoint.Action == Enum.PathWaypointAction.Jump then
Humanoid.Jump = true
end
Humanoid:MoveTo(PathPoint.Position)
if not Humanoid.MoveToFinished:Wait() then
warn(NPC.Name,"got stuck somewhere")
break
end
end
end
end
FollowPath(NPC.HumanoidRootPart.Position,EndPart.Position)
4. Visualizing the Path
To visualize the path, you’re gonna need 3 variables, a PathPointPart, a Visualize boolean, and a PathFolder set to nil
local PathPointPart = Instance.new("Part")
PathPointPart.Shape = Enum.PartType.Ball
PathPointPart.Size = Vector3.new(1,1,1)
PathPointPart.Color = Color3.fromRGB(255,255,255)
PathPointPart.Transparency = 0.6
PathPointPart.Material = Enum.Material.SmoothPlastic
PathPointPart.CanCollide, PathPointPart.CanTouch, PathPointPart.CanQuery = false, false, false
PathPointPart.Anchored = true
local Visualize = true
local PathFolder = nil
The PathPointPart
is just a part except I changed how it looks a bit after creating it. You can technically reference a regular part instead of making an entirely new instance and setting the properties for it but I can’t do that for you, so this is here instead.
Visualize
will be used to well visualize the path, you should generally keep it false and only true for debugging. Or if you just want to see where the NPC will go for funzies.
PathFolder
will be used reference a folder that we’ll make that has all the new PathPointParts parts we’ll create to visualize the Path the NPC will follow. Currently it’s set to nil because we haven’t made it yet.
Now add this code between Waypoints[1] = nil
and for _,PathPoint:PathWaypoint in Waypoints do
:
if Visualize then
PathFolder = Instance.new("Folder")
for i,PathPoint in Waypoints do
local newPart = PathPointPart:Clone()
newPart.Position = PathPoint.Position
if PathPoint.Action == Enum.PathWaypointAction.Jump then
newPart.Color = Color3.fromRGB(255, 170, 0)
end
newPart.Parent = PathFolder
newPart.Name = i
end
PathFolder.Parent = workspace
end
If you’re a basic coder you should understand what it does but if you don’t, I’ll just explain it in order of what it does instead specifically explaining each part.
First it checks if Visualize is true, remember you should generally keep if false to reduce server lag, if it is true, it sets PathFolder
as a new Folder instance. Then it loops through each PathPoint in Waypoints, cloning a new PathPointPart
, position the new part at the position of the PathPoint
coloring the new part orange if the PathPoint
requires you to jump, parenting the new part PathFolder
which we’ve set as a Folder, and finally naming the new part as the index of where it is found in the Waypoint table. After all of that happens for each Pathpoint in Waypoints, it finally parents the PathFolder
to workspace so it’s visible to you.
You can technically leave it at that so long as you delete the PathFolder
after the NPC finishes moving to it’s EndPosition, but it usually doesn’t look nice as the NPC moves through the PathPointPart with the parts still being there. To make it look better, you’re gonna need to replace this code:
for _,PathPoint:PathWaypoint in Waypoints do
if PathPoint.Action == Enum.PathWaypointAction.Jump then
Humanoid.Jump = true
end
Humanoid:MoveTo(PathPoint.Position)
if not Humanoid.MoveToFinished:Wait() then
warn(NPC.Name,"got stuck somewhere")
break
end
end
With this one:
for i,PathPoint:PathWaypoint in Waypoints do
if PathPoint.Action == Enum.PathWaypointAction.Jump then
Humanoid.Jump = true
end
Humanoid:MoveTo(PathPoint.Position)
if not Humanoid.MoveToFinished:Wait() then
warn(NPC.Name,"got stuck somewhere")
break
end
if Visualize then
PathFolder[tostring(i)]:Destroy()
end
end
What did we change? Well we changed for _,PathPoint
with for i,PathPoint
which is important so we can reference and delete the correct PathPointPart
.
We also added
if Visualize then
PathFolder[tostring(i)]:Destroy()
end
At the end which checks if Visualize is true and deletes the PathPointPart named the index of which it’s located in the Waypoint table. Not gonna explain this any further, just think it through.
Now for the FINAL thing to add for part 4, deleting the PathFolder after the NPC finishes moving. Place this below all the loopings
if PathFolder then
PathFolder:Destroy()
end
It’s self explanitory not gonna explain it.
Anyways here’s the FINAL FINAL Server Script Code:
local PathService = game:GetService("PathfindingService")
local PathPointPart = Instance.new("Part")
PathPointPart.Shape = Enum.PartType.Ball
PathPointPart.Size = Vector3.new(1,1,1)
PathPointPart.Color = Color3.fromRGB(255,255,255)
PathPointPart.Transparency = 0.6
PathPointPart.Material = Enum.Material.SmoothPlastic
PathPointPart.CanCollide, PathPointPart.CanTouch, PathPointPart.CanQuery = false, false, false
PathPointPart.Anchored = true
local NPC = script.Parent
local Humanoid = NPC.Humanoid
local EndPart = workspace.EndPart
local Visualize = true
local PathFolder = nil
local Path = PathService:CreatePath()
function FollowPath(StartPos, EndPos)
Path:ComputeAsync(StartPos,EndPos)
if Path.Status == Enum.PathStatus.Success then
local Waypoints = Path:GetWaypoints()
Waypoints[1] = nil
if Visualize then
PathFolder = Instance.new("Folder")
for i,PathPoint in Waypoints do
local newPart = PathPointPart:Clone()
newPart.Position = PathPoint.Position
if PathPoint.Action == Enum.PathWaypointAction.Jump then
newPart.Color = Color3.fromRGB(255, 170, 0)
end
newPart.Parent = PathFolder
newPart.Name = i
end
PathFolder.Parent = workspace
end
for i,PathPoint:PathWaypoint in Waypoints do
if PathPoint.Action == Enum.PathWaypointAction.Jump then
Humanoid.Jump = true
end
Humanoid:MoveTo(PathPoint.Position)
if not Humanoid.MoveToFinished:Wait() then
warn(NPC.Name,"got stuck somewhere")
break
end
if Visualize then
PathFolder[tostring(i)]:Destroy()
end
end
if PathFolder then
PathFolder:Destroy()
end
end
end
FollowPath(NPC.HumanoidRootPart.Position,EndPart.Position)
5. Things you should know before leaving
PathService:CreatePath()
is reusable. You can callPath:ComputeAsync()
on the same Path instead of creating a new Path and calling ComputeAsync() on the new path.PathService:CreatePath()
actually creates an instance that’s deletable. This is more for the OOP but it still applies here. But because it’s deletable, I assume it takes up memory. And so to avoid memory leaks, make sure to delete it when the NPC dies or gets destroyed. Example code to add below thePathService:CreatePath()
:
local Path = PathService:CreatePath()
NPC.Destroying:Connect(function()
Path:Destroy()
PathPointPart:Destroy() -- Make sure to delete this also as it takes up memory
if PathFolder then
PathFolder:Destroy() -- Make sure to delete this also as it takes up memory
end
end)
- Pathes have a built in “Blocked” feature →
Path.Blocked:Connect()
, in most cases though, it’s generally better to just generate a new path every .25 seconds or so and make the Ai switch to the new Path. While it can be done in a regular script, it gets complicated, especially when you’re trying to implement it in a regular module script to be used in other scripts. Thus the reason why you should consider switching to OOP Ai. - I also heavily recommend modifying or adding onto the script. My code works for my game, if it doesn’t work for yours, just change it to make it work.
Implementing OOP into PathfindingService
How to implement OOP Ai
This assumes you’ve read “How to use PathFindingService” or atleast know how to use PathFindingService. If none of those is true, go read “How to use PathfindingService”
THIS IS NOT A TUTORIAL ON OOP
OOP is quite complex. I’ll try my best to explain what’s happening, but it might be explained poorly or not explained at all. So if you’re a beginner coder you should probably not read this part of the tutorial until you learn what OOP is.
First what is OOP? Well it stands for Object Oriented Programming. To summarize it, it’s just a style of coding that makes it easier to make and get variables to be used in custom functions, basically something made to make organization easy. You can think of it like making a custom instance with custom properties, like how a “Part” has a position value, size value, and all those other properties. Except instead of calling it “Part” we’re gonna call it “CustomAi” and it has variables that references the NPC Character, the path it’s currently walking in if there’s any, and other variables that you want to include.
However why should you use OOP Ai? Well I hope you know copying and pasting code to be used in different scripts is a terrible idea, to fix that people use Module Scripts and have scripts require the Module Script. While yes this solves some problems, this also causes other problems to arise. If you read “How to use PathfindingService” you’d know we’d use 3 maybe 4 variables if you’re including the NPC’s model as one. These variables aren’t accessible to the modules script unless you put them in as arguments function(arg1, arg2, ar3)
. Doing this is usually a bad idea but it works. However if you were to add another argument, that’d mean you’d have to change every script that is requiring the Module Script, which makes using a Module Script practically useless. All in all it would just be better if the Module Script was able to create and have access to those starting variables. This is where OOP comes in. As stated before you can think of OOP as “a custom instance with custom properties” that allows those custom properties to be used in custom functions. Custom Custom Custom :3
1. Start of OOP
To begin we’re gonna need a Module Script named “AiModule” parented to ServerScriptService with the code below:
local PathService = game:GetService("PathfindingService")
local PathPointPart = Instance.new("Part")
PathPointPart.Shape = Enum.PartType.Ball
PathPointPart.Size = Vector3.new(1,1,1)
PathPointPart.Color = Color3.fromRGB(255,255,255)
PathPointPart.Transparency = 0.6
PathPointPart.Material = Enum.Material.SmoothPlastic
PathPointPart.CanCollide, PathPointPart.CanTouch, PathPointPart.CanQuery = false, false, false
PathPointPart.Anchored = true
local module = {}
module.__index = module
function module.new(NPC)
local newThing = setmetatable({}, module)
newThing.Character = NPC
newThing.Visualize = true
newThing.PathFolder = nil
newThing.CurrentPath = nil
return newThing
end
return module
Not explaining this part, all you need to know is that module.__index = module
is needed and that local newThing = setmetatable({}, module)
is needed with the return newThing
, and that function module.new()
will allow a regular script and the module script to acess the variables inside of it.
Below is an example code on how to use the AiModule, assumes Script is parented to NPC:
local AiModule = require(game:GetService("ServerScriptService").AiModule3)
local NPC = script.Parent
local AiNPC = AiModule.new(NPC)
print(AiNPC.Visualize) -- will print true, since Visualize = true
2. Setting the Path
The next piece you’re gonna need is a SetPath function
function module:SetPath(StartPos,EndPos,PathSetting:{})
local Path = PathService:CreatePath(PathSetting)
Path:ComputeAsync(StartPos,EndPos)
self.CurrentPath = Path
return Path -- Technically not needed, feel free to remove
end
If you’re new to OOP, self is basically referencing the values made in
function module.new()
.
How do you know you can use self is when the function uses a colon :
instead of .
.
Example: module:Eat()
, module.Eat()
3. ClearPath Function
Now before we go into making the FollowPath function, we need a way to destroy any uneeded instances that take up memory. This is so we don’t create any memory leaks.
function module:ClearPath()
if self.StepsFolder then
self.StepsFolder:Destroy()
end
if self.CurrentPath then
self.CurrentPath:Destroy()
end
end
All this function is doing is checking if the object exists then deleting it it does.
4. Make NPC follow Path
Now you just need a FollowPath function
function module:FollowPath()
local Path = self.CurrentPath
if Path and Path.Status == Enum.PathStatus.Success then
local Humanoid:Humanoid = self.Character.Humanoid
local Visualize = self.Visualize
local PathFolder = self.PathFolder
local Waypoints = Path:GetWaypoints()
Waypoints[1] = nil
if Visualize then
PathFolder = Instance.new("Folder")
for i,PathPoint in Waypoints do
local newPart = PathPointPart:Clone()
newPart.Position = PathPoint.Position
if PathPoint.Action == Enum.PathWaypointAction.Jump then
newPart.Color = Color3.fromRGB(255, 170, 0)
end
newPart.Parent = PathFolder
newPart.Name = i
end
PathFolder.Parent = workspace
end
for i,PathPoint:PathWaypoint in Waypoints do
if PathPoint.Action == Enum.PathWaypointAction.Jump then
Humanoid.Jump = true
end
Humanoid:MoveTo(PathPoint.Position)
if not Humanoid.MoveToFinished:Wait() then
warn(self.Character.Name,"got stuck somewhere")
break
end
if Visualize then
PathFolder[tostring(i)]:Destroy()
end
end
self:ClearPath()
end
end
This script is just a copy paste from the script I made in “How to use PathFindingService” it’s also explained in there. The only main difference is that the variables Path
, Humanoid
, Visualize
, and PathFolder
are referenced by self
.
One thing I should mention is the self:ClearPath()
near the end of the script. self
also allows you to also call any functions you made using function module:Test()
or
function module.Test()
. And it doesn’t matter in what order you made it in. For example, if you made function 1
below function 2
, function 1
normally wouldn’t be able to call function 2
in a regular script. However, using OOP (and I think regular modules can also do this) and the self
thing, you will be able to call function 2
from function 1
.
Entire Module Code:
local PathService = game:GetService("PathfindingService")
local PathPointPart = Instance.new("Part")
PathPointPart.Shape = Enum.PartType.Ball
PathPointPart.Size = Vector3.new(1,1,1)
PathPointPart.Color = Color3.fromRGB(255,255,255)
PathPointPart.Transparency = 0.6
PathPointPart.Material = Enum.Material.SmoothPlastic
PathPointPart.CanCollide, PathPointPart.CanTouch, PathPointPart.CanQuery = false, false, false
PathPointPart.Anchored = true
local module = {}
module.__index = module
function module.new(NPC:Model)
local newThing = setmetatable({}, module)
newThing.Character = NPC
newThing.Visualize = false
newThing.PathFolder = nil
newThing.CurrentPath = nil
return newThing
end
function module:ClearPath()
if self.StepsFolder then
self.StepsFolder:Destroy()
end
if self.CurrentPath then
self.CurrentPath:Destroy()
end
end
function module:SetPath(StartPos,EndPos,PathSetting:{})
local Path = PathService:CreatePath(PathSetting)
Path:ComputeAsync(StartPos,EndPos)
self.CurrentPath = Path
return Path -- Technically not needed, feel free to remove
end
function module:FollowPath()
local Path = self.CurrentPath
if Path and Path.Status == Enum.PathStatus.Success then
local Humanoid:Humanoid = self.Character.Humanoid
local Visualize = self.Visualize
local PathFolder = self.PathFolder
local Waypoints = Path:GetWaypoints()
Waypoints[1] = nil
if Visualize then
PathFolder = Instance.new("Folder")
for i,PathPoint in Waypoints do
local newPart = PathPointPart:Clone()
newPart.Position = PathPoint.Position
if PathPoint.Action == Enum.PathWaypointAction.Jump then
newPart.Color = Color3.fromRGB(255, 170, 0)
end
newPart.Parent = PathFolder
newPart.Name = i
end
PathFolder.Parent = workspace
end
for i,PathPoint:PathWaypoint in Waypoints do
if PathPoint.Action == Enum.PathWaypointAction.Jump then
Humanoid.Jump = true
end
Humanoid:MoveTo(PathPoint.Position)
if not Humanoid.MoveToFinished:Wait() then
warn(self.Character.Name,"got stuck somewhere")
break
end
if Visualize then
PathFolder[tostring(i)]:Destroy()
end
end
self:ClearPath()
end
end
return module
Example usage from a regular script:
local AiModule = require(game:GetService("ServerScriptService").AiModule)
local NPC = script.Parent
local AiNPC = AiModule.new(NPC)
AiNPC:SetPath(NPC.HumanoidRootPart.Position,workspace.EndPart.Position) -- Just make a part and name it "EndPart" and move it somewhere or something
AiNPC:FollowPath()
Video:
There, now you can easily implement PathFindingService to any NPC with a Humanoid.
I also heavily recommend modifying or adding onto the module code. My code works for my game, if it doesn’t work for yours, just change it to make it work. For example, one thing you can you could easily implement is a wander specific waypoints function
-- Waypoint is a folder or model containing parts at different places to walk to, Folder:GetChildren()
function module:WanderWaypoints(Waypoint)
local PickRandPoint = Waypoint[math.random(1,#Waypoint)]
self:SetPath(self.Character.HumanoidRootPart.Position,PickRandPoint.Position)
self:FollowPath()
end
If you have any questions feel free to ask, unless they’re related to OOP. Go ask someone else :d