Need help with improvement on pathfinding code

  • What does the code do and what are you not satisfied with?
    This module deals with handling of npc characters in my game. It uses pathfinding service to create paths to move to locations (almost always to players) and has a check sight function to detect direct sight. The main problem with it is that the pathfinding is dog water.

NPC Module

local PFS = game:GetService("PathfindingService")

local MAX_RETRIES = 5
local RETRY_TIME = 1 --in sec

local RAYDIST = 1000

local NPC = {}
NPC.__index = NPC

type self = {
	NPC:Instance,
	HRP:BasePart,
	Humanoid: Humanoid,
	
	Path:Path,
	IsPathing:boolean,
	
	CanAttack:boolean,
	Damage:number,
	Range:number,
	Cooldown:number,
	Friendlies:Instance,
	Animations:{AnimationTrack}
}

export type NPC = typeof(setmetatable({} :: self, NPC))

function NPC.new(npc:Instance, pathArgs:{}): NPC
	local self = setmetatable({} :: self, NPC)
	
	self.NPC = npc
	self.HRP = npc:FindFirstChild("HumanoidRootPart")
	self.Humanoid = npc:FindFirstChildOfClass("Humanoid")
	
	self.Path = PFS:CreatePath(pathArgs)
	self.IsPathing = false
	
	self.CanAttack = true
	self.Damage = 20
	self.Range = 5
	self.Cooldown = 1
	self.Friendlies = nil
	self.Animations = {}
	
	for i, v in ipairs(npc:GetChildren()) do
		if v:IsA("Animation") then
			if v.AnimationId == nil then continue; end
			local track = self.Humanoid:FindFirstChildOfClass("Animator"):LoadAnimation(v)

			self.Animations[v.Name] = track
		elseif v:IsA("BasePart") then
			v:SetNetworkOwner(nil)
		end
	end
	
	self.Humanoid.Died:Connect(function()
		task.wait(4)
		self.NPC:Destroy()
		self = nil
	end)
	
	return self
end

function NPC.WalkTo(self:NPC, obj:BasePart): ()
	local function unstuck()
		self.Humanoid:MoveTo(self.HRP.Position + Vector3.new(math.random(-1, 1), 0, math.random(-1, 1)))
		self.Humanoid.Jump = true
		task.wait(1)
	end
	
	local success, errMsg
	local retries = 0
	repeat
		retries += 1
		task.wait(RETRY_TIME)
		success, errMsg = pcall(function()
			self.Path:ComputeAsync(self.HRP.Position, obj.Position)
		end)
	until success and self.Path.Status == Enum.PathStatus.Success or retries >= MAX_RETRIES

	if success and self.Path.Status == Enum.PathStatus.Success then
		self.IsPathing = true
		local waypoints = self.Path:GetWaypoints()
		
		for i, waypoint:PathWaypoint in ipairs(waypoints) do
			if i == 1 then continue; end
			
			self.Humanoid:MoveTo(waypoint.Position)
			if waypoint.Action == Enum.PathWaypointAction.Jump then
				self.Humanoid.Jump = true
			end
			
			local reached = self.Humanoid.MoveToFinished:Wait(2)
			if reached == false then
				--print("cancelled1", self.NPC:GetFullName())
				unstuck()
				break
			end
			
			if self:CheckSight(obj.Position, obj) and math.abs(math.abs(obj.Position.Y) - math.abs(self.HRP.Position.Y)) < 3 then
				--print("cancelled2", self.NPC:GetFullName())
				break
			end
			
			if (obj.Position - waypoints[#waypoints].Position).Magnitude > 30 then
				--print("cancelled3", self.NPC:GetFullName())
				break
			end
		end
		self.IsPathing = false
	else
		print("Path failed", self.NPC:GetFullName(), obj:GetFullName())
		if not success then
			print(errMsg)
		end
	end
end

function NPC.CheckSight(self:NPC, pos:Vector3, obj:BasePart): ()
	local rayParams = RaycastParams.new() do
		rayParams.FilterType = Enum.RaycastFilterType.Exclude
		rayParams.FilterDescendantsInstances = {self.NPC, self.Friendlies}
	end
	
	local origin = self.HRP.Position
	local raycastResult = workspace:Raycast(origin, (pos - origin) * RAYDIST, rayParams)
	return (raycastResult ~= nil and raycastResult.Instance:IsDescendantOf(obj.Parent))
end

function NPC.Attack(self:NPC, target:Instance): ()
	if self.CanAttack then
		self.CanAttack = false
		self:PlayAnimation("AttackAnim") --if self.Animations["AttackAnim"] ~= nil then self.Animations["AttackAnim"]:Play(); end
		task.wait(0.25)
		
		local raycast = workspace:Raycast(self.HRP.Position, self.HRP.CFrame.LookVector * self.Range)
		if raycast and raycast.Instance.Parent == target then
			target:FindFirstChildOfClass("Humanoid"):TakeDamage(self.Damage * (_G.difficulty/100))
			print("hit")
		end
		
		task.delay(self.Cooldown, function() self.CanAttack = true end)
	end
end

function NPC.PlayAnimation(self:NPC, anim:string): ()
	if self.Animations[anim] ~= nil then
		self.Animations[anim]:Play()
	else
		warn("missing animation")
	end
end

function NPC.FindTargets(self:NPC): Instance
	local potentialTargets = {}
	local dist = math.huge
	local target = nil
	
	for i, v in ipairs(workspace.Friendlies:GetChildren()) do
		local hum = v:FindFirstChildOfClass("Humanoid")
		local torso = hum.RootPart --v:FindFirstChild("HumanoidRootPart")
		if torso and hum and hum.Health > 0 then
			table.insert(potentialTargets, v)
		end
	end
	
	for i, v in ipairs(potentialTargets) do
		local targetDist = (v.HumanoidRootPart.Position - self.HRP.Position).Magnitude
		if targetDist < dist then
			dist = targetDist
			target = v
		end
	end
	
	return target
end

return NPC

typical AI implementation

local NPC = require(game:GetService("ServerScriptService").NPC)

local dummy = NPC.new(script.Parent, {
	AgentRadius  = 1.5,
	AgentHeight = 4.5,
	AgentCanJump = true,
	AgentCanClimb = true
})
dummy.Friendlies = workspace.Enemies
dummy.Damage = 12.5

while task.wait(0.1) do
	if dummy.Humanoid.Health <= 0 then break; end
	local target = dummy:FindTargets()
	
	if target ~= nil then
		if dummy:CheckSight(target.HumanoidRootPart.Position, target.HumanoidRootPart) and math.abs(math.abs(target.HumanoidRootPart.Position.Y) - math.abs(dummy.HRP.Position.Y)) < 3 then
			dummy.Humanoid:MoveTo(target.HumanoidRootPart.Position)
		else
			if dummy.IsPathing == false then
				task.spawn(dummy.WalkTo, dummy, target.HumanoidRootPart)
			end
		end
		
		if (target.HumanoidRootPart.Position - dummy.HRP.Position).Magnitude < dummy.Range then
			dummy:Attack(target)
		end
	end
end
  • What potential improvements have you considered?
    Adding a check straight path function to check if there is a straight path possible is feasible. apart from that I really have no clue what to do.

  • How (specifically) do you want to improve the code?
    I want to fix and improve the pathfinding. It jitters and cannot often decide where it wants to go.

dr_help.rbxl (73.3 KB)
should contain one map, a npc chasing you, set up script and module

1 Like

if you want npc to update fast, replace with

task.spawn(dummy.WalkTo, dummy, target.HumanoidRootPart)

because when you call humanoid:MoveTo(), the MoveToFinished event fires automatically, so your previous path loop will break. It’s unnecessary to wait for the previous path to finish

1 Like