Pathfinding Misbehaving

I’ve been working on a pathfinding-based monster NPC. The core AI logic is contained within a ModuleScript, and for the most part it functions as expected. However, I’ve noticed that the monster will sometimes glitch—for example, it won’t move at all, or it will get stuck in place.

I’m not sure if this behavior is caused by limitations of Roblox’s PathfindingService itself, or if the problem lies in my implementation. I’d appreciate any feedback on how I could improve the script’s reliability and overall performance.

Module Script

local AImodule = {}
AImodule.__index = AImodule

function AImodule.new(object, pathParams, configuration)
	-- Create a new instance of AImodule
	local self = setmetatable({}, AImodule)

	-- Initialize variables for this instance
	self.object = object
	self.pathParams = pathParams
	self.configuration = configuration
	self.walkanim = nil
	self.path2 = nil
	self.target = nil
	self.npc = nil
	self.playerNormalWalkingSpeed = nil
	self.atackingValue = nil

	-- Assign configuration values
	self.walkSpeed = configuration.walkSpeed
	self.runSpeed = configuration.runSpeed
	self.playerSpeed = configuration.playerSpeed
	self.range = configuration.range
	self.damage = configuration.damage
	
	-- Debounce Position
	self.previousPosition = Vector3.new(0,0,0)
	self.currentPosition = Vector3.new(0,0,0)
	self.positionCount = 0
	
	-- Return the instance
	return self
end

function AImodule:AImodulescript()
	local success, errorMessage = pcall(function()
		local pathfindingservice = game:GetService("PathfindingService")
		local dbStudRadius = 3
		self.npc = self.object
		local humanoid = self.npc:WaitForChild("Humanoid")
		local hrp = self.npc:WaitForChild("HumanoidRootPart")
		local body = self.npc:FindFirstChild("HumanoidRootPart")
		local db = true

		self.path2 = pathfindingservice:CreatePath()

		hrp:SetNetworkOwner(nil)

		self.walkanim = humanoid.Animator:LoadAnimation(humanoid.Animator.Walk)
		local attackanim = humanoid.Animator:LoadAnimation(humanoid.Animator.Attack)

		local rayprams = RaycastParams.new()
		rayprams.FilterType = Enum.RaycastFilterType.Exclude
		rayprams.FilterDescendantsInstances = {self.npc}

		local lastPos
		local animPlaying = false

		self.walkanim:Play()
	
		local function canseetarget(target)
			local origin = hrp.Position
			local direction = (target.HumanoidRootPart.Position - hrp.Position).Unit * self.range
			local ray = workspace:Raycast(origin, direction, rayprams)

			if ray and ray.Instance and ray.Instance:IsDescendantOf(target) then
				return true
			else
				return false
			end
		end

		local function findtarget()
			local players = game.Players:GetPlayers()
			local maxDistance = self.range
			local nearestTarget

			for _, player in pairs(players) do
				if player.Character and #player:WaitForChild("SafeValues"):GetChildren() <= 0 and (not player:FindFirstChild("BeingAtacked")) then
					local target = player.Character
					local distance = (hrp.Position - target.HumanoidRootPart.Position).Magnitude

					if distance < maxDistance and canseetarget(target) then
						nearestTarget = target
						maxDistance = distance
						self:CallPlayer(player, "Found")
					else
						--self:CallPlayer(player, "Lost")
					end
				end
			end
			return nearestTarget
		end
	
		local function getpath(destination)
			local path = pathfindingservice:CreatePath(self.pathParams)
			path:ComputeAsync(hrp.Position, destination.Position)

			if path.Status == Enum.PathStatus.Success then
				return path
			else
				self.npc:SetPrimaryPartCFrame(CFrame.new(self.object.Parent.FailSafePart.Position))
				self:CallPlayer(game.Players:GetPlayerFromCharacter(destination), "Lost")
				path:Destroy()
				return nil
			end
		end

		local function DebugPos()
			self.currentPosition = self.npc.HumanoidRootPart.Position

			if (self.currentPosition - self.previousPosition).Magnitude <= dbStudRadius then
				self.positionCount = self.positionCount + 1
			else
				self.positionCount = 0
			end
			
			if self.positionCount > 20 then
				error("positionCount: ".. self.positionCount)
			end

			self.previousPosition = self.currentPosition
		end

		local function attack(target)
			self.walkanim:Stop()
			attackanim:Play()

			while true do
			
				local distance = (hrp.Position - target.HumanoidRootPart.Position).Magnitude

				if distance >= self.range or #game.Players:GetPlayerFromCharacter(target):WaitForChild("SafeValues"):GetChildren() > 0 or target.Humanoid.Health <= 0 or target.Humanoid:GetState() == Enum.HumanoidStateType.Dead then
					break
				end
				
				DebugPos()
				
				task.wait()

				self.path2:ComputeAsync(body.Position, target.HumanoidRootPart.Position)
				local waypoints = self.path2:GetWaypoints()

				for _, waypoint in pairs(waypoints) do
					if distance >= self.range or #game.Players:GetPlayerFromCharacter(target):WaitForChild("SafeValues"):GetChildren() > 0 or target.Humanoid.Health <= 0 then
						break
					end
					humanoid:MoveTo(waypoint.Position)
					humanoid.MoveToFinished:Wait()
				end
			end

			if self.path2 then
				self.path2:Destroy()
			end

			self.walkanim:Play()
			self:CallPlayer(game.Players:GetPlayerFromCharacter(target), "Lost")
			target = nil
			self:patrol()
		end

		local function walkto(destination)
			
			local path = getpath(destination)
			if not path then
				self:patrol()
				return
			end
			
			for _, waypoint in pairs(path:GetWaypoints()) do
				path.Blocked:Connect(function()
					path:Destroy()
					self:patrol()
				end)

				if not animPlaying then
					self.walkanim:Play()
					animPlaying = true
				end
				attackanim:Stop()

				self.target = findtarget()
				if self.target and self.target.Humanoid.Health > 0 then
					lastPos = self.target.HumanoidRootPart.Position
					attack(self.target)
					break
				else
					if waypoint.Action == Enum.PathWaypointAction.Jump then
						humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
					end

					if lastPos then
						humanoid:MoveTo(lastPos)
						humanoid.MoveToFinished:Wait()
						lastPos = nil
						break
					else
						humanoid:MoveTo(waypoint.Position)
						humanoid.MoveToFinished:Wait()
					end
				end
			end
		end

		function self:patrol()
			local waypoints = self.object.Parent.WayPoints:GetChildren()
			if #waypoints == 0 then
				return
			end

			local randomNum = math.random(1, #waypoints)
			local selectedWaypoint = waypoints[randomNum]
			if selectedWaypoint then
				DebugPos()
				walkto(selectedWaypoint)
			end
		end
		
		task.spawn(DebugPos)
		
		while task.wait() do
			self:patrol()
		end
	end)
	
	
	
	if not success then
		task.wait()
		warn("Script encountered an error: " .. errorMessage)
		
		self.positionCount = 0
		
		if self.path2 then
			self.path2:Destroy()
		end
		self.walkanim:Play()

		local s, e = pcall(function()
			task.wait()
			self:CallPlayer(game.Players:GetPlayerFromCharacter(self.target), "Lost")
		end)

		if self.target and self.target:FindFirstChild("humanoid") then
			self:CallPlayer(game.Players:GetPlayerFromCharacter(self.target), "Lost")
			self.npc.Humanoid.WalkSpeed = self.walkSpeed
			self.target:FindFirstChild("humanoid").WalkSpeed = self.playerNormalWalkingSpeed
		end

		wait(1)
		self.npc:SetPrimaryPartCFrame(CFrame.new(self.object.Parent.FailSafePart.Position))
		task.wait(1)
		self:AImodulescript()
	end
end

function AImodule:CallPlayer(player, Value)
	if Value then
		if Value == "Found" then
			self.atackingValue = Instance.new("Configuration")
			self.atackingValue.Name = "BeingAtacked"
			self.atackingValue.Parent = player
			game.ReplicatedStorage.AIEvents.TargetUpdate:FireClient(player, "Found", self.npc:FindFirstChild("Chase"))
			self.playerNormalWalkingSpeed = player.Character:FindFirstChild("Humanoid").WalkSpeed
			task.wait()
			player.Character:FindFirstChild("Humanoid").WalkSpeed = self.playerSpeed-- + self.playerNormalWalkingSpeed
			self.npc.Humanoid.WalkSpeed = self.runSpeed
		elseif Value == "Lost" then
			if self.atackingValue then
				self.atackingValue:Destroy()
			end
			task.wait()
			if player and (not player:FindFirstChild("BeingAtacked")) then
				if self.playerNormalWalkingSpeed and player then
					player.Character:FindFirstChild("Humanoid").WalkSpeed = self.playerNormalWalkingSpeed
				end
				game.ReplicatedStorage.AIEvents.TargetUpdate:FireClient(player, "Lost")
			end
			
			self.npc.Humanoid.WalkSpeed = self.walkSpeed
			self.playerNormalWalkingSpeed = nil
		end
	end
end

return AImodule

Server Script

local run = require(game:GetService("ServerScriptService").MainModules.AIModule)

local pathParams = {
	AgentHeight = 8,
	AgentRadius = 4,
	AgentCanJump = false,
}

local configuration = {
	walkSpeed = 20,
	runSpeed = 45,
	playerSpeed = 35,
	range = 100,
	damage = 75,
}

task.wait(1)
local aiInstance = run.new(script.Parent, pathParams, configuration)
aiInstance:AImodulescript()
2 Likes

Try adding prints like ‘Wandering around’ when the monster is patroling, ‘See player’ when they found a player in sight. This helps us identify when the bug happens.

1 Like

Try change the radius to 2. Hands doesn’t collide with object.

1 Like