A bit advanced AI pathfinding

Here you will learn how to make nextbots using a cool AI PathfindingService

by Archer_Reality

Author Avatar

Introduction

Hello there! My name is Archer_Reality and here I will teach you how to create a cool pathfinding for a game like evade, that there are bots that are trying to chase you. I will teach you how that works!

Firstly, try inserting a ModuleScript inside ReplicatedStorage, like this:

image|100x100

then, try naming the module as "AIBotPathfinding", so we can give our module a different name! Ok, now if we open, the module should be like this:

##Setting the module

local module = {}

return module

try changing it to

local botModule = {}

return botModule

yeah, I guess this is better, isn't it? Its good so we won't confuse ourselves.

Now, lets create some variables to the module:

local pathfind = {}
local pathfindingService = game:GetService("PathfindingService")
local players = game:GetService("Players")
local botDataFolder = nil
local file = nil

return pathfind

##Creating some bot settings

Very well! Now, we should add some settings to the bot. We are going to create a function that create some settings for the bot, for example, how much damage, or the speed when the bot sees a player or when he doesn't

local pathfindingService = game:GetService("PathfindingService")
local players = game:GetService("Players")
local botDataFolder = nil
local file = nil

function botModule:SetBotFunctionsAsync(bot, d, w, s)
	local self = {
		[1] = d;
		[2] = w;
		[3] = s;
	}
	local names = {
		"Damage"; "WalkSpeed";"SprintSpeed"
	}
	dataFolder = Instance.new('Folder')
	dataFolder.Name = 'BotData'
	dataFolder.Parent = bot
	for i, v in ipairs(self) do
		local value = Instance.new('NumberValue', bot)
		value.Name = names[i]
		value.Value = self[i]
		value.Parent = dataFolder
	end
	file = dataFolder:GetChildren()
	return self
end


return pathfind

So what this block of code does: In the parameters, are the main settings: damage, normal walk speed and the speed when it sees a player, respectively. Then, it creates a table with those values, and another table for the names.

Then, it creates a folder inside the bot with the settings (just to make it clear). Then, it creates a NumberValue that will be inside the folder and will store the settings data.

Since we will have to use the folder later, we took that file nil value, and transformed into the children of the folder, which are Damage, WalkSpeed and SprintSpeed. And, of course, the function will return the table.

##Creating the path

Perfect! Now the next block of code will be the path creation, which is also pretty simple:

local function setPathAsync(self, pos)
	local path = pathfindingService:CreatePath({
		AgentHeight = 6;
		AgentRadius = 3;
		AgentCanJump = true;
		AgentCanClimb = false;
		
		Costs = {
			Water = 10;
			DangerZone = math.huge
		}
	})
	local s = pcall(function()
		path:ComputeAsync(self.HumanoidRootPart.Position, pos.Position)
	end)
	return path
end

So, the function will create a path using the PathfindingService. After that, between the path brackets, it is setting the options. The costs table will make the bot choose the path which has less costs. For example: To walk in concrete will be 10 bucks and in asphalt will be 50 bucks. What are you choosing? The concrete, of course, because it costs less than the asphalt, right?

Next, it is creating a safier function for the coding of the path. So the first parameter of path:ComputeAsync() is from where it will start. Of couse, it will be in the bot's humanoidRootPart. But the bot is set inside the parameter, so when you use this function, make sure the first parameter will be the bot.

And the second parameter, is for the destination of the bot, that is the player's humanoidRootPart, an also, please make sure the second parameter is just for the player's humanoidRootPart, or their head, etc. And we will also return the path.

Well done! You made one of the most essencial parts of the script!

##Finding nearest player

Now, moving to the next part, which is finding the closest player:

local function getPlayer(self)
	local nearestTarget
	local MaxDistance = math.huge

	for index, player in pairs(players:GetPlayers())do
		if player.Character then
			local target = player.Character
			local distance = (self.HumanoidRootPart.Position - target.HumanoidRootPart.Position).Magnitude

			if distance < MaxDistance then
				nearestTarget = target
				MaxDistance = distance
			end
		end
	end
	return nearestTarget
end

So, here is the script. The first parameter is also the bot, that is what bot you want to chase the nearest player. Dont confuse yourself with ANYTHING ELSE, or else the script will go wrong.

So, the math.huge in max distance is just to make sure that the bot will not stay idled.

This function will check every player's humanoidRootPart's position, to see what is the closest to the bot, using the variable "distance".

Then, the script will set the nil variables. The nearestTarget will be the closest player and the maxdistance will turn the closest player's distance, so the bot won't hunt more that 1 player at the same time. And it is very essencial to return the nearest player.

##Finding the player as an external function (You can jump, but if you do, make sure to copy that part in the whole module.)

The next block of code is basically the same thing, but for external uses. So for example, you insert the script inside the bot that will set the module working, you should probably use it:

function botModule:GetClientAsync(self)
	local nearestTarget
	local MaxDistance = math.huge

	for index, player in pairs(players:GetPlayers())do
		if player.Character then
			local target = player.Character
			local distance = (self.HumanoidRootPart.Position - target.HumanoidRootPart.Position).Magnitude

			if distance < MaxDistance then
				nearestTarget = target
				MaxDistance = distance
			end
		end
	end
	return nearestTarget
end

Good job! If you are here, then you REALLY want to learn this, right? Nice!

##Damaging distances and damaging quantity

Now, the next block of code is the damaging function:

local function dmgPlr(self, player, humanoid)
	local distance = (self.HumanoidRootPart.Position - player.HumanoidRootPart.Position).Magnitude
	if distance > 4 then
		humanoid:MoveTo(player.HumanoidRootPart.Position)
	else
		humanoid:TakeDamage(file[1].Value)
	end
end

So here, the first parameter is the bot that will damage the player and the second parameter is the player that will be damaged.

The distance will be the subtraction of the bot's position and the player's position. We wont use the self.Touched:Connect(), because it will not be so good. Instead, we will check the distance if it's greater than 4. If it is, then the bot will move to the player's position. If it isn't, then the bot will damage the player.

Remeber that we created the folder with the bot's settings? So, we are going to use it right now! The index number 1 is the damage number, but it is calling from the folder, not from the table.

##Main module function: Where everything is in one function

You are doing good, keep it up! Now, its the last block of code, which is also the longest, the hardest and the most important: Setting everything up together.

function botModule:SetAIPathfindAsync(self, pos, player)
	local humanoid = self:FindFirstChildOfClass('Humanoid')
	self.PrimaryPart:SetNetworkOwner(nil)
	local path = setPathAsync(self, pos)
	local plr = getPlayer(self)
	if path.Status == Enum.PathStatus.Success then
		for i, waypoint in ipairs(path:GetWaypoints()) do
			if plr and #path:GetWaypoints() > 0 then
				dmgPlr(self, player, humanoid)
				humanoid.WalkSpeed = file[3].Value
				break
			elseif not plr then
				local i = file[2].Value
				humanoid.WalkSpeed = i.Value
				humanoid:MoveTo(waypoint.Position)
				local reachedConnection
				if not reachedConnection then
				reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
					error('Path not completed')
					end)
					local nextWaypoint = 2
					local points = path:GetWaypoints()
					humanoid:MoveTo(points[nextWaypoint].Position)
				end
				error('No identified player')
				elseif #path:GetWaypoints() <= 0 then error('Waypoints does not exist')
			else
				humanoid:MoveTo(plr.Head.Position)
				error('Path does not exist')
				return false
			end
		end 
	end
end

So we created an external function, that has three parameters: the bot, the position and the player. Firstly, we are going to find the bot's humanoid, which is crucial to this module. Then we are setting the network owner.

After that, we are creating a new path for the bot to follow, with the second parameter of it being the same second parameter if this external function. The plr is the closest player.

Now we have the status of the path. If the path status is successful, then we will call every waypoint of the path, and if its not, the bot will try to follow the closest player's head. Then we are going to check if the closest player exists and if the "last" waypoint is greater than zero. If the waypoint is zero or less is not, then will message an error. But if it is, then we will call the damage player function. Remembering, the damage player function's third parameter is for the bot's humanoid, not the player's humanoid.

If the closest player exists, then the bot will trigger it's sprint speed, in other words, he found the closest player. But if the player does not exists, he will trigger his normal speed, and move to his next waypoint.

Then we have the reachedConnection, which it is nil for now. Now, this might be confusing, but if the reachedConnection doesnt exists, then it will set to the bot's humanoid:MoveToFinished, that will trigger another waypoint.

The "points" value is every waypoint of the path. Now the bot's humanoid will move to the next waypoint.

##Creating the bot script

That is pretty much the module, now we are going to move into the EASIEST PART of this tutorial: Creating the nextbot.

Create your nextbot, customize as much as you want. Done it? Nice! Now insert a normal script inside the bot and write down:

local aiBotModule = require(game:GetService("ReplicatedStorage"):WaitForChild("AIBotPathfinding"))
local self = script.Parent

aiBotModule:SetBotFunctionsAsync(self, 100, 20, 40)

while wait(.25) do
	local character = aiBotModule:GetClientAsync(self)
	if character then
		local rP = character:FindFirstChild("HumanoidRootPart")
		if rP then
			aiBotModule:SetAIPathfindAsync(self, rP.Position, character)
		end
	end
end

So this script is calling the module, then is setting his parent (bot) as "self".

Then, he is finding the closest player character using the module's external function. If the character exists, then he is calling the humanoidRootPart of the character. If the humanoidRootPart exists, then he will create a path. The first parameter is the bot, the second is the position the bot will go anf the third paramater is the player's character. The third parameter is just so the bot can damage the player. All of this insside a loop that returns to the beginning every 0.25 seconds. Thats it!

##Whole module

Now this is the whole module:

local botModule = {}
local pathfindingService = game:GetService('PathfindingService')
local players = game:GetService('Players')
local dataFolder = nil
local file = nil

-- setting the bot's functions (default the damage, normal speed and the sprint speed (when the bot finds a player))
-- inf you want to create more values to the table, add it and change it in the bot's script. All the values are going to be stored inside a file named "BotData" inside a bot
function botModule:SetBotFunctionsAsync(bot, d, w, s)
	local self = {
		[1] = d;
		[2] = w;
		[3] = s;
	}
	local names = {
		"Damage"; "WalkSpeed";"SprintSpeed"
	}
	dataFolder = Instance.new('Folder')
	dataFolder.Name = 'BotData'
	dataFolder.Parent = bot
	for i, v in ipairs(self) do
		local value = Instance.new('NumberValue', bot)
		value.Name = names[i]
		value.Value = self[i]
		value.Parent = dataFolder
	end
	file = dataFolder:GetChildren()
	return self
end

-- creating a path
local function setPathAsync(self, pos)
	local path = pathfindingService:CreatePath({
		AgentHeight = 6;
		AgentRadius = 3;
		AgentCanJump = true;
		AgentCanClimb = false;
		
		Costs = {
			Water = 10;
			DangerZone = math.huge
		}
	})
	local s = pcall(function()
		path:ComputeAsync(self.HumanoidRootPart.Position, pos.Position)
	end)
	return path
end

-- finding nearest player
local function getPlayer(self)
	local nearestTarget
	local MaxDistance = math.huge

	for index, player in pairs(players:GetPlayers())do
		if player.Character then
			local target = player.Character
			local distance = (self.HumanoidRootPart.Position - target.HumanoidRootPart.Position).Magnitude

			if distance < MaxDistance then
				nearestTarget = target
				MaxDistance = distance
			end
		end
	end
	return nearestTarget
end

-- finding nearest player (different because you can use it to detect the player in the bot's script)
function botModule:GetClientAsync(self)
	local nearestTarget
	local MaxDistance = math.huge

	for index, player in pairs(players:GetPlayers())do
		if player.Character then
			local target = player.Character
			local distance = (self.HumanoidRootPart.Position - target.HumanoidRootPart.Position).Magnitude

			if distance < MaxDistance then
				nearestTarget = target
				MaxDistance = distance
			end
		end
	end
	return nearestTarget
end

-- function to damage the player
local function dmgPlr(self, player, humanoid)
	local distance = (self.HumanoidRootPart.Position - player.HumanoidRootPart.Position).Magnitude
	if distance > 4 then
		humanoid:MoveTo(player.HumanoidRootPart.Position)
	else
		humanoid:TakeDamage(file[1].Value)
	end
end

-- setting the main function so the bot script will directly call it from here
function botModule:SetAIPathfindAsync(self, pos, player)
	local humanoid = self:FindFirstChildOfClass('Humanoid')
	self.PrimaryPart:SetNetworkOwner(nil)
	local path = setPathAsync(self, pos)
	local plr = getPlayer(self)
	if path.Status == Enum.PathStatus.Success then
		for i, waypoint in ipairs(path:GetWaypoints()) do
			if plr and #path:GetWaypoints() > 0 then
				dmgPlr(self, player, humanoid)
				humanoid.WalkSpeed = file[3].Value
				break
			elseif not plr then
				local i = file[2].Value
				humanoid.WalkSpeed = i.Value
				humanoid:MoveTo(waypoint.Position)
				local reachedConnection
				if not reachedConnection then
				reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
					error('Path not completed')
					end)
					local nextWaypoint = 2
					local points = path:GetWaypoints()
					humanoid:MoveTo(points[nextWaypoint].Position)
				end
				error('No identified player')
				elseif #path:GetWaypoints() <= 0 then error('Waypoints does not exist')
			else
				humanoid:MoveTo(plr.Head.Position)
				error('Path does not exist')
				return false
			end
		end 
	end
end

return botModule

Now your script is ready for your game! Thanks for seeing this tutorial, and I hope we meet again in another one! Goodbye!

View in-game to comment, award, and more!