NPC AI Pathfinding

Learn how to make AI move with PathfindingService

by EXpodo1234ALT

Author Avatar

Introduction

If you didnt know theres a service called PathfindingService which obviously meant for pathfinding for custom AI for NPC's or actual players, but im going to focus on NPCs. PathfindingService has one main method, :CreatePath(). The arguments of it is a dictionary with the keys AgentHeight and AgentRadius, though I do not know what AgentHeight does (you can clearly see it makes a difference when you lower or increase it) since the wiki didn't provide a sufficient amount of information

about the new pathfinding (barely anything of the API actually, but I

know what AgentRadius is from observations which i'll explain later. If

you're a beginner scripter or do not know what I am saying, don't take

this tutorial THE DIFFICULTY IS HARD, since I expect you to know what

methods and algorithms im saying without explaining that much.

Steps

To create this system, we can use a while loop and use the wait() method

to make it yield from a random (not really) amount of time between 2

numbers. Firstly we're going to make a variable defined as the

:CreatePath() method which returns the Path object with specific methods

in them to create our system. So in our dictionary keys AgentHeight and

AgentRadius is all going to be defined as 1.

--server, note you should put this in the character

local humanoid = script.Parent.Humanoid
local torso = script.Parent.HumanoidRootPart

local ps = game:GetService("PathfindingService")

while true do
    local path = ps:CreatePath({AgentHeight = 1, AgentRadius = 1})

    wait(math.random(2,3.5))
end

After that, we're going to use the :ComputeAsync() which will make us a

path with specific arguments, the starting point which is obviously our

character's root (HumanoidRootPart) and the ending position (destination)

which we will create right now. To create it we're going to make a offset

from the characters root's position and set the x,z axes as a random cord.

local humanoid = script.Parent.Humanoid
local torso = script.Parent.HumanoidRootPart

local ps = game:GetService("PathfindingService")

while true do

    local pos = torso.CFrame.Position + Vector3.new(math.random(-50,50),0,math.random(-50,50)) -- 100x100 square radius

    local path = ps:CreatePath({AgentHeight = 1, AgentRadius = 1})
    
local comp = path:ComputeAsync(torso.CFrame.Position, pos)

    wait(math.random(2,3.5))  
end

Right now we got the basics done right now, but we dont have anything

to do with it, But before we get starting with the main block we need

to check if we can create a path to our destination by checking

path.Status returns Enum.PathStatus.Success. If theres no path to getting

to the point then it'll return Enum.PathStatus.NoPath. After if there is a

path, then we can call :GetWaypoints() which returns the positions the

character needs to move to to get to our point of the Path object

:CreatePath() returns. It returns a array with dictionaries inside of it

with the keys the position and the action which i'll explain later. Also I

might say now what the AgentRadius does is how far the point will be from

the character, so if it was 1 then it'll be 1 stud ex 100; 100 studs away,

though i'd suggest using 1 because it'll make the character a lot easier

to detect when to jump than 100 or so. Then to move to those points we

can call the :MoveTo() method and wait for when the :MoveTo() finishes by listening

for MoveToFinished:Wait().

local humanoid = script.Parent.Humanoid
local torso = script.Parent.HumanoidRootPart
local ps = game:GetService("PathfindingService")

while true do
    local pos = torso.CFrame.Position + Vector3.new(math.random(-50,50),0,math.random(-50,50))
    local path = ps:CreatePath({AgentHeight = 1, AgentRadius = 1})
    local comp = path:ComputeAsync(torso.CFrame.Position, pos)

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

        for _,point in pairs(waypoints) do
            humanoid:MoveTo(point.Position)

            -- this is just a demonstration of where the point is, you can remove it
            local part = Instance.new("Part")

            part.CanCollide = false
            part.Locked = true
            part.Anchored = true
            part.Material = Enum.Material.Neon
            part.BrickColor = BrickColor.Red()
            part.Size = Vector3.new(1,0.5,1)
            part.CFrame = CFrame.new(point.Position)
            part.Transparency = 0.25
            part.Parent = workspace

            humanoid.MoveToFinished:Wait()

            part:Destroy()
        end
    end

    wait(math.random(2,3.5))
end

Now the NPC moves around, but what if they need to jump over something

or on something which is why point.Action exists. It'll have a value of

Enum.PathWaypointAction.Jump or Enum.PathWaypointAction.Walk which we

can check then call Humanoid:ChangeState(Enum.HumanoidStateType.Jumping)

to make the character jump. Though what if the character sits, it'll just

stay there forver, so in which we can listen for the Seated event of the

humanoid and check if the 1st parameter is true then call the

Humanoid:ChangeState() method. But we have another problem, what if an

object blocks the character's path while traveling there. We can use the

Blocked event of the path object and listen for it, then just call

:MoveTo() to the character's own positon to cancel the NPC from walking

towards the blockade.

local humanoid = script.Parent.Humanoid
local torso = script.Parent.HumanoidRootPart

local ps = game:GetService("PathfindingService")


math.randomseed(tick()) -- create a seed

while true do
    local pos = torso.CFrame.Position + Vector3.new(math.random(-50,50),0,math.random(-50,50))
    local path = ps:CreatePath({AgentHeight = 1, AgentRadius = 1})
    local comp = path:ComputeAsync(torso.CFrame.Position, pos)

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

        humanoid.Seated:Connect(function(seated)
            if seated == true then
                print("seated")
                humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
            end
        end)

        path.Blocked:Connect(function()
            print("blocked!")
            humanoid:MoveTo(torso.CFrame.Position)
        end)

        for _,point in pairs(waypoints) do
            humanoid:MoveTo(point.Position)

            local part = Instance.new("Part")

            part.CanCollide = false
            part.Locked = true
            part.Anchored = true
            part.Material = Enum.Material.Neon
            part.BrickColor = BrickColor.Red()
            part.Size = Vector3.new(1,0.5,1)
            part.CFrame = CFrame.new(point.Position)
            part.Transparency = 0.25
            part.Parent = workspace

            if point.Action == Enum.PathWaypointAction.Jump then 
                print("jumping")
                humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
            end

            humanoid.MoveToFinished:Wait()

            part:Destroy() 
        end
    end

    wait(math.random(2,3.5))
end

Conclusion

Since we've doing creating the entire script, you could try making an

obstacle course and see if the NPC can get out of it. Note that if your

using multiple NPCs with this type of system instead of placing a script

in each of them, you could place a script inside ServerScriptService as

a command centre. Then you could place all the AI inside a folder in

workspace and modify this script so its compatible with a generic for

loop ex:

local ps = game:GetService("PathfindingService")

math.randomseed(tick())

for _,npc in pairs(workspace.NPCS:GetChildren()) do
    spawn(function() -- spawn a new thread
        while true do
            local humanoid = npc.Humanoid
            local torso = npc.HumanoidRootPart
            local pos = torso.CFrame.Position + Vector3.new(math.random(-50,50),0,math.random(-50,50))
            local path = ps:CreatePath({AgentHeight = 1, AgentRadius = 1})
            local comp = path:ComputeAsync(torso.CFrame.Position, pos)

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

                humanoid.Seated:Connect(function(seated)
                    if seated == true then
                        print("seated")
                        humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
                    end
                end)

                path.Blocked:Connect(function()
                    print("blocked!")
                    humanoid:MoveTo(torso.CFrame.Position)
                end)

                for _,point in pairs(waypoints) do
                    humanoid:MoveTo(point.Position)

                    local part = Instance.new("Part")

                    part.CanCollide = false
                    part.Locked = true
                    part.Anchored = true
                    part.Material = Enum.Material.Neon
                    part.BrickColor = BrickColor.Red()
                    part.Size = Vector3.new(1,0.5,1)
                    part.CFrame = CFrame.new(point.Position)
                    part.Transparency = 0.25
                    part.Parent = workspace

                    if point.Action == Enum.PathWaypointAction.Jump then 
                        print("jumping")
                        humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
                    end

                    humanoid.MoveToFinished:Wait()

                    part:Destroy() 
                end
            end

            wait(math.random(2,3.5))
        end
    end)
end

If you want to see areas that the NPC can go, just go to files>settings>

studio>visual which is way at the bottom and it should show some things.

the purple areas is where it could go, the non coloured areas is where

it cant go and the arrows is where the NPC needs to jump.

Animations

If you could see, the NPC doesn't have any idle, jumping nor walking

animation. So what you could do if you didn't know, is play solo in studio

and copy the Animate LocalScript inside your character to the clipboard

and quit play solo. Secondly paste it in the explorer and add a server script

in the NPC (LocalScripts can not run in workspace with the exception of the player's character)

and copy the contents and also the instances inside of the

Animate script in the server script. Finally you can remove lines 510 to

524 since the server doesn't know which player "LocalPlayer" is and NPCs

cant chat anyways.