Hard Written By EXpodo1234ALT

NPC AI Pathfinding Created on: 01-01-2019

Learn how to make AI move with PathfindingService

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.

Hard Written By EXpodo1234ALT

See EXpodo1234ALT's profile on Roblox

Discussion