Simple pathfinding ai

Welcome to my first ever tutorial on the devforum, I wanted to make a pathfinding tutorial for such a long time. This may not be the best pathfinding AI tutorial out there. I’m just showing you what i have learned in the past weeks for making a pathfinding AI, without any further ado, let’s begin the tutorial.

note: i suck at explaining scripts :/, also my English kinda sucks as well.

Keep in mind the code under me should be placed in a normal script inside an NPC.

Alright, Lets setup some quick variables

local npc = script.Parent -- the path to the NPC
local human = npc.Humanoid -- getting the humanoid of the npc

local PFS = game:GetService("PathfindingService")
local RUNSERVICE = game:GetService("RunService")

npc.PrimaryPart:SetNetworkOwner(nil)

Well, that was easy, wasn’t it. Lets begin the fun >:), (aka finding the closest target)

Let’s create a function with couple of variables in it.

local function findTarget()
	local players = game:GetService("Players"):GetPlayers()
	local nearesttarget
	local maxDistance = 5000 -- distance

end

Alright. Lets loop through the players and check if theres a character!

local function findTarget()
	local players = game:GetService("Players"):GetPlayers()
	local nearesttarget
	local maxDistance = 5000 -- distance

	for i,player in pairs(players) do
		if player.Character then
           --codeeeeeeeeeee
		end
	end
	-- return will go here later in the tutorial
end

EPIC

Okay. let’s do the rest!

local function findTarget()
	local players = game:GetService("Players"):GetPlayers()
	local nearesttarget
	local maxDistance = 5000 -- distance

	for i,player in pairs(players) do
		if player.Character then
			local target = player.Character
			local distance = (npc.HumanoidRootPart.Position - target:WaitForChild("HumanoidRootPart").Position).Magnitude

			if distance < maxDistance then
				nearesttarget = target
				maxDistance = distance
			end

		end
	end
	return nearesttarget
end

Alright, so what we are doing is getting the target and distance between the NPC, and the target(in this case its the character)

local target = player.Character
local distance = (npc.HumanoidRootPart.Position - target:WaitForChild("HumanoidRootPart").Position).Magnitude

Then checking if the distance, it’s less than the max distance. and setting the nearest target to the target, ANDDD setting the maxDistance to the distance! yay


if distance < maxDistance then
	nearesttarget = target
	maxDistance = distance
end

We almost forgot, lets add one last line of code to the findTarget function

return nearesttarget

All, together the function should look something like this:

local function findTarget()
	local players = game:GetService("Players"):GetPlayers()
	local nearesttarget
	local maxDistance = 5000 -- distance

	for i,player in pairs(players) do
		if player.Character then
			local target = player.Character
			local distance = (npc.HumanoidRootPart.Position - target:WaitForChild("HumanoidRootPart").Position).Magnitude

			if distance < maxDistance then
				nearesttarget = target
				maxDistance = distance
			end

		end
	end
	return nearesttarget
end

PHEWWW, We are done with that. Let’s begin by calculating the path

Start by creating an another function, inside that function there’s just going to be 3 lines of code

local function getPath(destination)
	local path = PFS:CreatePath()
	path:ComputeAsync(npc.HumanoidRootPart.Position, destination)
	return path
end

So what we are doing is creating a path using PathFindingService, and using the ComputeAsync(), to compute a path from a start position to an end position. And returning the path.

Thats over, Hmm, Alright then.

Lets create a 3rd function(wow, loads of functions today aye?), with 2 variables inside. One to get the path function, and the 2nd to get the target function.

local function pathFindTo(destination)
	local path = getPath(destination)
	local target = findTarget()
    --rest of the code
end

Okay, Now lets check if there’s a target and check if the target is not dead.

if target and target.Humanoid.Health > 0 then
		
end

Good. Lets start by creating waypoints. So the AI can walk to them.

for i,waypoint in pairs(path:GetWaypoints()) do
	if waypoint.Action == Enum.PathWaypointAction.Jump then
		human.Jump =true
	end

	human:MoveTo(waypoint.Position)
	human.MoveToFinished:Wait()
end

So currently, we are looping through the waypoints, using the path variable, and using GetWaypoints() to get them. Simple enough right?, and checking if the waypoint action, its a jump action. Then we are going to set the humanoid jump property to true. And the rest is simple.

All together the function should look something like this.

local function pathFindTo(destination)
	local path = getPath(destination)
	local target = findTarget()
	
	if target and target.Humanoid.Health > 0 then
		for i,waypoint in pairs(path:GetWaypoints()) do
			if waypoint.Action == Enum.PathWaypointAction.Jump then
				human.Jump =true
			end

			human:MoveTo(waypoint.Position)
			human.MoveToFinished:Wait()
		end
	end
end

We are almost done!!! Now let’s use RunService

RUNSERVICE.Heartbeat:Connect(function()
	--code
end)

The Heartbeat event fires every frame , after the physics simulation has completed. :+1:

Now lets check if theres a target if so, use the pathFindTo() function, and set the destination as the target.

RUNSERVICE.Heartbeat:Connect(function()
	local target = findTarget()
	
	if target then
		pathFindTo(target:WaitForChild("HumanoidRootPart").Position + (target:WaitForChild("HumanoidRootPart").Velocity.Unit * 7)) -- play around with the number 7 to your liking.
	end
end)

As you can see the script works very well, It still needs some improvements, But you can do that on your own :>

FULL SCRIPT

local npc = script.Parent
local human = npc.Humanoid

local PFS = game:GetService("PathfindingService")
local RUNSERVICE = game:GetService("RunService")

npc.PrimaryPart:SetNetworkOwner(nil)

local function findTarget()
	local players = game:GetService("Players"):GetPlayers()
	local nearesttarget
	local maxDistance = 5000 -- distance

	for i,player in pairs(players) do
		if player.Character then
			local target = player.Character
			local distance = (npc.HumanoidRootPart.Position - target:WaitForChild("HumanoidRootPart").Position).Magnitude

			if distance < maxDistance then
				nearesttarget = target
				maxDistance = distance
			end

		end
	end
	return nearesttarget
end

local function getPath(destination)
	local path = PFS:CreatePath()

	path:ComputeAsync(npc.HumanoidRootPart.Position, destination)

	return path
end

local function pathFindTo(destination)
	local path = getPath(destination)
	local target = findTarget()
	
	if target and target.Humanoid.Health > 0 then
		for i,waypoint in pairs(path:GetWaypoints()) do

			if waypoint.Action == Enum.PathWaypointAction.Jump then
				human.Jump =true
			end

			human:MoveTo(waypoint.Position)
			human.MoveToFinished:Wait()

		end
	end
end

RUNSERVICE.Heartbeat:Connect(function()
	local target = findTarget()
	
	if target then
		pathFindTo(target:WaitForChild("HumanoidRootPart").Position + (target:WaitForChild("HumanoidRootPart").Velocity.Unit * 7))
	end
end)

Q: Why does the AI twitch in the video?
A: Because, We are generating the path infront of the player by 7 studs. As i just said in the script, Play around with the number.

(target:WaitForChild("HumanoidRootPart").Velocity.Unit * 7)

https://developer.roblox.com/en-us/api-reference/class/RunService

https://developer.roblox.com/en-us/api-reference/event/RunService/Heartbeat

47 Likes

This is a very nice tutorial! I like the organization of the code and how you explained everything.

2 Likes

really like how it was explained, although the video isn’t playing (for me)

2 Likes

Using Heartbeat is never a good idea when it comes to pathfinding. Why? well because of the fact that it creates a seperate thread every single frame, it doesn’t actually yield or wait for the last calculated path to finish.

Here’s a video showcasing the problem. The red and green AI have the exact same code that you have in the tutorial, except the green one uses while true do instead of Heartbeat. I also made each AI visualize each waypoint.

Notice how the red AI (the one that uses Heartbeat) is jittering and creating an unnecessary amount of waypoints, while the green AI has absolutely no problems in following the player.

This is why you shouldn’t use RunService events (RenderStepped, Stepped, Heartbeat) when it comes to Pathfinding, mostly because you need it to yield in order for it to not make unnecessary waypoints and waste a ton of performance.

Here’s a screenshot of the script performance tab comparing the two scripts.

image

14 Likes

very interesting. in some of my pathfinding scripts ive always used while runservice.Heartbeat:Wait() do thanks for sharing
edit: when i actually test this in one of my pathfinding scripts, the npc seems to stop when it gets close to the player, then chases again. however when i use heartbeat it barely stops

In that case, you can add a raycast check inside pathFindTo(). If the AI can directly see the target/player, a repeat until loop will happen. Here’s some pseudocode of what I mean.

if target and target.Humanoid.Health > 0 then
	for i,waypoint in pairs(path:GetWaypoints()) do

		if waypoint.Action == Enum.PathWaypointAction.Jump then
			human.Jump = true
		end

		human:MoveTo(waypoint.Position)
		human.MoveToFinished:Wait()
		
		if CheckRaycast() == true then -- you're going to have to make your own checkraycast function
			repeat
				human:MoveTo(target.PrimaryPart.Position)
				task.wait()
			until CheckRaycast() == false
		end
	end
end
9 Likes

yeah when i tested it i was using different code then the tutorial however i will try it with the tutorial later

You should just put the ComputeAsync in the loop instead of CreatePath.