Zombie Pathfinding works like a slideshow

I’m trying to get my zombie OOP module to have smooth pathfinding without lagging too much.

The problem is that when the zombies are actively tracking for players, these zombies stop in between every second when following the moving player. Even with this system, it is still lagging badly with about 20 to 30 zombies.

I tried modifying the pathfinding functions countless times but I just can’t really figure out a way to make the movement not look like a slideshow…

Where I'm creating zombie objects (Server Script)
for i, v in pairs(game.Workspace.CurrentZombies:GetChildren()) do
	
	task.wait(0.5)
			
	local zombie = zombieClass.new(v, 100, 0.7, 15, 3, 500, 10, CFrame.new((math.random(200)-250), 2, (math.random(300)-150)))
	CollisionModule.charCollisionGroup(zombie.char, "Zombies")
		
	zombie:MoveToNearestEntrance(entrances)
	
	zombie:TrackingPathfind()
	
	
end
Zombie Module (There's a section for pathfinding, it's commented)
---------------------------------------------------------------------
--[[
This Server Module handles Zombies

Author: Void_Trader
Last Edit: 7/15/2024
Version: 1.0
]]
---------------------------------------------------------------------

--Services
local CollectionService = game:GetService("CollectionService")
local PathfindingService = game:GetService("PathfindingService")
local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")

--Modules
local Modules = ServerStorage:WaitForChild("Modules")

local GeneralModules = Modules:WaitForChild("General")
local EntranceClass = require(GeneralModules:WaitForChild("Entrance"))


--Main module
local zombie = {}
zombie.__index = zombie


--Setting up zombie
function zombie.new(char: Model, health: number, breakSpeed: number, damage: number, attackRange: number, detectRange: number, smoothness: number,position: CFrame)
	
	--Setting up Metatable
	local self = setmetatable({}, zombie)
	
	--Setting up values and variables
	self.char = char
	self.root = char:FindFirstChild("HumanoidRootPart")
	self.humanoid = char:FindFirstChild("Humanoid") :: Humanoid
	self.head = char:FindFirstChild("Head")
	self.torso = char:FindFirstChild("Torso")
	
	self.target = nil
	
	--Animations
	self.animator = self.humanoid:FindFirstChild("Animator")
	self.grabAnim = self.animator:LoadAnimation(self.char:FindFirstChild("Grab"))
		
	--Properties
	self.health = health
	self.humanoid.MaxHealth = health
	self.humanoid.Health = health
	
	self.breakSpeed = breakSpeed
	
	self.damage = damage
	self.attackRange = attackRange
	self.detectRange = detectRange
	self.smoothness = smoothness
	
	--Thread variables
	self.threads = {}
	
	
	self.__type = "Zombie"
	
	--Run code
	self.char.PrimaryPart:SetNetworkOwner(nil)
	self.humanoid:AddTag("Zombie")
	self:position(position)
	
	-------AI-------
	--Entrance is called in main script
	--Tracking
	
	
	return self
	
end


------------------------------------------------------------
--METAMETHODS


------------------------------------------------------------
--MOVEMENT FUNCTIONS


--Move zombie to position instantly
function zombie:position(position: CFrame)
	
	if self.__type ~= "Zombie" then return end

	
	self.root:PivotTo(position)
	
	
	return self
	
end


--------------------------------------------
--AI FUNCTIONS--


--Entrance Functions--

--Break plank
function zombie:BreakPlanks(Entrance)
	
	repeat task.wait(self.breakSpeed) Entrance:Break()
		until (Entrance.currentPlanks == 0) or (self.humanoid.Health <= 0)
	
	
	return self
	
end

--Find nearest entrance
function zombie:MoveToNearestEntrance(Entrances)
	
	if self.__type ~= "Zombie" then return end

	--Variables
	local target = nil
	local dist = self.detectRange
	local entrance = nil
	
	--Finding entrance
	for _, entranceObject in pairs(Entrances) do
		local breakZone = entranceObject.breakZone
		if (breakZone) and (self.checkDist(breakZone, self.root) < dist) then
			dist = self.checkDist(breakZone, self.root)
			target = breakZone
			entrance = entranceObject
		end
	end
	
	self.target = target
	
	
	--Moving to entrance--
	
	--Creating path
	if self.target then
	
		self:threadSpawn(function()	
		
			local path = PathfindingService:CreatePath()
			path:ComputeAsync(self.root.Position, self.target.Position)

			local waypoints = path:GetWaypoints()
			local currentTarget = self.target

			--Setting up pathfinding
			if path.Status == Enum.PathStatus.Success then
				for i, waypoint in pairs(waypoints) do
					--[[
					-----------------
					local part = Instance.new("Part")
					part.CanCollide = false
					part.Anchored = true
					part.Material = "Neon"
					part.BrickColor = BrickColor.new("White")
					part.Position = waypoint.Position + Vector3.new(0, 2, 0)
					part.Shape = "Ball"
					part.Size = Vector3.new(1,1,1)
					part.Parent = game.Workspace
					
					]]-----------------

					--Creating waypoints and moving zombie there
					if waypoint.Action == Enum.PathWaypointAction.Jump then
						self.humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
					else
						self.humanoid:MoveTo(waypoint.Position)

						self:threadSpawn(function()
							task.wait(0.5)
							if self.humanoid.WalkToPoint.Y > self.root.Position.Y then
								self.humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
							end
						end)
					end

					--Makes sure zombie walking is correct
					self.humanoid.MoveToFinished:Wait()

					--Making sure tracking works
					if not self.target then
						print("Lost target")
						break
					elseif (self.checkDist(currentTarget, waypoints[#waypoints]) > self.smoothness) or currentTarget ~= self.target then
						print("?")
						self:MoveToNearestEntrance()
						break
					end

				end

				--Breaking Planks--
				self:BreakPlanks(entrance)

			else
				print("Path unable to be computed")
				self:removeZombie()
			end
			
		end)
		
	end
	
	
	return self
	
end


--------------------------------------------
--TRACKING FUNCTIONS--

--Tracking Pathfind
function zombie:TrackingPathfind()

	if self.__type ~= "Zombie" then return end


	self:threadSpawn(function()

		task.wait(15)

		while task.wait(0.1) do
			
			--Find Target
			self:findTarget()
			
			--Create Path
			if self.target then
				
				local path = PathfindingService:CreatePath()
				path:ComputeAsync(self.root.Position, self.target.Position)
				
				local waypoints = path:GetWaypoints()
				
				for _, waypoint in pairs(waypoints) do
					if self.target and self.target.Parent:FindFirstChildWhichIsA("Humanoid").Health > 0 then
						
						print(self.target.Parent.Name)
						
						--Move towards next waypoint
						if waypoint.Action == Enum.PathWaypointAction.Jump then
							self.humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
						else
							self.humanoid:MoveTo(waypoint.Position)

							self:threadSpawn(function()
								task.wait(0.5)
								if self.humanoid.WalkToPoint.Y > self.root.Position.Y then
									self.humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
								end
							end)
						end

						--Makes sure zombie walking is correct
						self.humanoid.MoveToFinished:Wait()
						
						--Attack
						self:attack()

						--Making sure tracking works
						if not self.target then
							print("Lost target")
							break
						--elseif (self.checkDist(self.target, waypoints[#waypoints]) > 20) then
						--	print("?")
						--	self:TrackingPathfind()
						--	break
						end
						
					else
						print("!!!!!!!!!!!!!!!!!!")
					end
				end
				
			end
		end
	end)


	return self

end


--Find Target
function zombie:findTarget()
	
	if self.__type ~= "Zombie" then return end
	
	--Variables
	local players = Players:GetPlayers()
	local maxDistance = self.detectRange
	self.target = nil
	
	--Check for closest player
	for i, plr in pairs(players) do
		if plr.Character then
			local target = plr.Character
			--local distance = (self.root.Position - target.HumanoidRootPart.Position).Magnitude
			local distance = self.checkDist(self.root, target:FindFirstChild("HumanoidRootPart"))
			
			if distance < maxDistance then
				self.target = target:FindFirstChild("HumanoidRootPart")
				maxDistance = distance
			end
		end
	end
	
	
	return self
	
end


--------------------------------------------
--ATTACK FUNCTIONS--

--Attack
function zombie:attack()
	
	if self.__type ~= "Zombie" then return end
	if self.target == nil then return end
	
	--Get distance
	local distance = self.checkDist(self.root, self.target)
	
	--if distance > self.attackRange then
		--Move zombie to player
	--	self.humanoid:MoveTo(self.target.Position)
	--else
	if distance < self.attackRange then
		--Attack
		self.target.Parent:FindFirstChildWhichIsA("Humanoid").Health -= self.damage
	end
	
	
	return self
	
end

--------------------------------------------
--UTILITY FUNCTIONS--


--Remove zombie
function zombie:removeZombie()

	if self.__type ~= "Zombie" then return end


	task.wait(5)
	
	--Close all threads
	for _, thread in pairs(self.threads) do
		coroutine.close(thread)
		thread = nil
	end
	
	self.char:Destroy()
	
	self = nil
	

	return self

end


--Check distance(NORMAL FUNCTION)
function zombie.checkDist(part1: Part | Vector3, part2: Part | Vector3): number
	
	local part_1
	local part_2
		
	if typeof(part1) ~= Vector3 then part_1 = part1.Position end
	if typeof(part2) ~= Vector3 then part_2 = part2.Position end

	return (part_1 - part_2).Magnitude
end


--Spawn thread
function zombie:threadSpawn(functionObject, ...)
	
	if self.__type ~= "Zombie" then return end
	
	
	local co = coroutine.wrap(functionObject)
	
	table.insert(self.threads, co)
	
	co(...)
	
end


------------------------------------------------------------


return zombie

I’m guessing someone else knows an easy method to this, but I’m far from an expert with pathfinding.

Thanks :happy2:

I’m not 100% sure, but does moving

to the bottom of the else statement fix the issue.

It doesn’t unfortunately,

Here’s what the issue looks like: