NPC movement starts to lagging when there is too many target

the problem is your script was just here where you are making a loop of creating many threads and each thread try to move the zombie to a different target or maybe to the same targets but with different paths

1 Like

you made a thread for the loop to make run forever 0.5 sec is not enough for the zombie to finish it is path

i tried with 1 second but still it gives the same result

1 Like

try a huge number like 10 or 20 sec , you can make more dynamic by not updating a zombie if it is already have a target

1 Like

Is there any delay in the MoveToFinished:Wait() function? Whenever the zombie reached my old position, it stops for a split of seconds and then continue moving to my newest position

1 Like

I tried 10 and 20, they just go for my position and then stood for 20 seconds then continue…

EDIT: disable the humanoid move to finished somehow fix everything, but no smart movement

1 Like

yes there is some delay with MoveToFinish event it yields until the humanoid reach the point that set by MoveTo function after that it waits until the humanoid speed to be equal 0

1 Like

just make 1 thread for every zombie and in this thread make a while loop that move the zombie rather than making a new thread every time

what about the waypoints, they seem still wanted to change route everytime they move

Here is the new script

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

local stunmodule = require(game.ReplicatedStorage.EffectsDebuff)
local calculator = require(game.ReplicatedStorage.DamageCalculator)

local ZOMBIE_TAG = "zombie"
local TARGET_FIND_RADIUS = 150
local PATH_REFRESH_INTERVAL = .5
local ATTACK_RADIUS = 5.5
local DAMAGE = 4.5
local ATTACK_COOLDOWN = 0.55

local zombies = {}

local function getNearestTarget(zombie)
	local root = zombie:FindFirstChild("HumanoidRootPart")
	if not root then return nil end

	local closestTarget
	local closestDistance = math.huge
	for _, model in ipairs(workspace:GetChildren()) do
		if model:IsA("Model") and model ~= zombie and model.Name ~= zombie.Name then
			local humanoid = model:FindFirstChildOfClass("Humanoid")
			local targetRoot = model:FindFirstChild("HumanoidRootPart")
			if humanoid and humanoid.Health > 0 and targetRoot then
				local distance = (root.Position - targetRoot.Position).Magnitude
				if distance < closestDistance and distance <= TARGET_FIND_RADIUS then
					closestTarget = targetRoot
					closestDistance = distance
				end
			end
		end
	end
	return closestTarget
end

local function attackZombie(zombie, target)
	if zombie:GetAttribute("CanAttack", true) then 
		local root = zombie:FindFirstChild("HumanoidRootPart")
		if not root then return end
		local attackAnim = zombie.HumanoidRootPart:FindFirstChild("punch")
		local attackSound = zombie.HumanoidRootPart:FindFirstChild("hit")
		local attacktrack = zombie.Humanoid:LoadAnimation(attackAnim)

		local targetCharacter = target.Parent
		
		local victimstats = targetCharacter:FindFirstChild("Stats")
		local victimdefend = (victimstats and victimstats:FindFirstChild("Defensive")) and victimstats.Defensive.Value 
		
		local humanoid = targetCharacter:FindFirstChildOfClass("Humanoid")
		local isBlocking = targetCharacter:GetAttribute("isBlocking")
		if isBlocking then
			local targetHumanoidRoot = targetCharacter:FindFirstChild("HumanoidRootPart")
			if targetHumanoidRoot then
				local directionToTarget = (targetHumanoidRoot.Position - root.Position).unit
				local dotProduct = directionToTarget:Dot(targetHumanoidRoot.CFrame.LookVector)
				if dotProduct > 0 then
					return
				end
			end
		end
		
		local damage = calculator:Calculate(DAMAGE,1,victimdefend)
		
		if (root.Position - target.Position).Magnitude < ATTACK_RADIUS then
			if attacktrack then
				attacktrack:Play()
			end

			if humanoid and humanoid.Health > 0 and zombie.Humanoid.Health > 0 and humanoid ~= zombie.Humanoid then
				humanoid:TakeDamage(damage)
				stunmodule:Stun(targetCharacter, 10, 0.85)
				if attackSound then
					attackSound:Play()
				end
			end
		end
	end
end

local function updateZombie(zombie)
	local humanoid = zombie:FindFirstChildOfClass("Humanoid")
	if not humanoid or humanoid.Health <= 0 then return end  

	local target = getNearestTarget(zombie)
	if target then
		local root = zombie:FindFirstChild("HumanoidRootPart")
		if not root then return end  

		local path = PathfindingService:CreatePath()

		local success, errorMessage = pcall(function()
			path:ComputeAsync(root.Position, target.Position)
			if path.Status == Enum.PathStatus.Success then
				for _, waypoint in ipairs(path:GetWaypoints()) do
					humanoid:MoveTo(waypoint.Position)
					--humanoid.MoveToFinished:Wait()

					if waypoint.Action == Enum.PathWaypointAction.Jump then
						humanoid.Jump = true
					end
				end
			end
		end)
		
		humanoid:MoveTo(target.Position)
		--humanoid.MoveToFinished:Wait()

		if not success then
			warn("Pathfinding failed:", errorMessage)
		end

		local lastAttackTime = zombie:GetAttribute("LastAttackTime") or 0
		if os.clock() - lastAttackTime >= ATTACK_COOLDOWN then
			zombie:SetAttribute("LastAttackTime", os.clock())
			attackZombie(zombie, target)
		end
	end
end

RunService.Heartbeat:Connect(function()
	for zombie, lastUpdate in pairs(zombies) do
		if zombie.Parent and os.clock() - lastUpdate >= PATH_REFRESH_INTERVAL then
			zombies[zombie] = os.clock()
			updateZombie(zombie)
		elseif not zombie.Parent then
			zombies[zombie] = nil
		end
	end
end)

CollectionService:GetInstanceAddedSignal(ZOMBIE_TAG):Connect(function(zombie)
	zombies[zombie] = os.clock()

	zombie:SetAttribute("LastAttackTime", 0)

	for _, v in pairs(zombie:GetDescendants()) do
		if v:IsA("BasePart") and v:CanSetNetworkOwnership() then
			v:SetNetworkOwner(nil)
		end
	end
end)

for _, zombie in ipairs(CollectionService:GetTagged(ZOMBIE_TAG)) do
	zombies[zombie] = os.clock()
	zombie:SetAttribute("LastAttackTime", 0)
	for _, v in pairs(zombie:GetDescendants()) do
		if v:IsA("BasePart") and v:CanSetNetworkOwnership() then
			v:SetNetworkOwner(nil)
		end
	end
end
1 Like

this is why you should make 1 thread that control 1 zombie , in that thread you can make a check if the last waypoint position is equal to the target position or not if not make a new path

1 Like

i am gonna test the script by my self right now and i will try to fix the issue

1 Like

Thanks, i will wait for the result

1 Like

try this one : -

local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")
local PathfindingService = game:GetService("PathfindingService")

local stunmodule = require(game.ReplicatedStorage.EffectsDebuff)
local calculator = require(game.ReplicatedStorage.DamageCalculator)

local ZOMBIE_TAG = "zombie"
local TARGET_FIND_RADIUS = 150
local PATH_REFRESH_INTERVAL = .5
local ATTACK_RADIUS = 5.5
local DAMAGE = 4.5
local ATTACK_COOLDOWN = 0.55

local zombies = {}

local function getNearestTarget(zombie)
	local root = zombie:FindFirstChild("HumanoidRootPart")
	if not root then return nil end

	local closestTarget
	local closestDistance = math.huge
	for _, model in ipairs(workspace:GetChildren()) do
		if model:IsA("Model") and model ~= zombie and model.Name ~= zombie.Name then
			local humanoid = model:FindFirstChildOfClass("Humanoid")
			local targetRoot = model:FindFirstChild("HumanoidRootPart")
			if humanoid and humanoid.Health > 0 and targetRoot then
				local distance = (root.Position - targetRoot.Position).Magnitude
				if distance < closestDistance and distance <= TARGET_FIND_RADIUS then
					closestTarget = targetRoot
					closestDistance = distance
				end
			end
		end
		task.wait()
	end
	return closestTarget
end

local function attackZombie(zombie, target)
	if zombie:GetAttribute("CanAttack", true) then 
		local root = zombie:FindFirstChild("HumanoidRootPart")
		if not root then return end
		local attackAnim = zombie.HumanoidRootPart:FindFirstChild("punch")
		local attackSound = zombie.HumanoidRootPart:FindFirstChild("hit")
		local attacktrack = zombie.Humanoid:LoadAnimation(attackAnim)

		local targetCharacter = target.Parent

		local victimstats = targetCharacter:FindFirstChild("Stats")
		local victimdefend = (victimstats and victimstats:FindFirstChild("Defensive")) and victimstats.Defensive.Value 

		local humanoid = targetCharacter:FindFirstChildOfClass("Humanoid")
		local isBlocking = targetCharacter:GetAttribute("isBlocking")
		if isBlocking then
			local targetHumanoidRoot = targetCharacter:FindFirstChild("HumanoidRootPart")
			if targetHumanoidRoot then
				local directionToTarget = (targetHumanoidRoot.Position - root.Position).unit
				local dotProduct = directionToTarget:Dot(targetHumanoidRoot.CFrame.LookVector)
				if dotProduct > 0 then
					return
				end
			end
		end

		local damage = calculator:Calculate(DAMAGE,1,victimdefend)

		if (root.Position - target.Position).Magnitude < ATTACK_RADIUS then
			if attacktrack then
				attacktrack:Play()
			end

			if humanoid and humanoid.Health > 0 and zombie.Humanoid.Health > 0 and humanoid ~= zombie.Humanoid then
				humanoid:TakeDamage(damage)
				stunmodule:Stun(targetCharacter, 10, 0.85)
				if attackSound then
					attackSound:Play()
				end
			end
		end
	end
end

local function updateZombie(zombie)
	local humanoid:Humanoid = zombie:FindFirstChildOfClass("Humanoid")
	while true do
		if not humanoid or humanoid.Health < 1 then
			break
		end
		
		local target = getNearestTarget(zombie)
		if target then
			local root = zombie:FindFirstChild("HumanoidRootPart")
			if root then 
				local path = PathfindingService:CreatePath({
					["AgentRadius"] = 4,
					["AgentHeight"] = 6,
					["AgentCanJump"] = true,
					["AgentCanClimb"] = true,
					["WaypointSpacing"] = 4,
				})

				local success, errorMessage = pcall(function()
					path:ComputeAsync(root.Position, target.Position)
				end)

				if not success then
					warn("Pathfinding failed:", errorMessage)
				elseif path.Status == Enum.PathStatus.Success then
					local WayPoints = path:GetWaypoints()
					for _, waypoint in pairs(WayPoints) do
						humanoid:MoveTo(waypoint.Position)
						
						if waypoint.Action == Enum.PathWaypointAction.Jump and humanoid:GetState() ~= Enum.HumanoidStateType.Jumping and humanoid:GetState() ~= Enum.HumanoidStateType.Freefall then
							humanoid.Jump = true
							humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
						end
						
					end
				end
				local Distance = (root.Position - target.Position).Magnitude
				local lastAttackTime = zombie:GetAttribute("LastAttackTime") or 0
				if os.clock() - lastAttackTime >= ATTACK_COOLDOWN and Distance < 10 then
					zombie:SetAttribute("LastAttackTime", os.clock())
					attackZombie(zombie, target)
				end
			end  
		end
	end
end

CollectionService:GetInstanceAddedSignal(ZOMBIE_TAG):Connect(function(zombie)
	zombies[zombie] = os.clock()

	zombie:SetAttribute("LastAttackTime", 0)

	for _, v in pairs(zombie:GetDescendants()) do
		if v:IsA("BasePart") and v:CanSetNetworkOwnership() then
			v:SetNetworkOwner(nil)
		end
	end
	
	task.spawn(updateZombie , zombie)
end)

for _, zombie in ipairs(CollectionService:GetTagged(ZOMBIE_TAG)) do
	zombies[zombie] = os.clock()
	zombie:SetAttribute("LastAttackTime", 0)
	for _, v in pairs(zombie:GetDescendants()) do
		if v:IsA("BasePart") and v:CanSetNetworkOwnership() then
			v:SetNetworkOwner(nil)
		end
	end
	task.spawn(updateZombie , zombie)
	task.wait()
end

i recommend saving the old code because this may not work , i tested it and they should work fine now

1 Like

They do work, thank you so much, although there is a small delay when reached the waypoints but is okay!

1 Like

no problem , i am glad to hear that , you can play with the “WaypointSpacing” value but this may increase or decrease the quality of the path

1 Like

wait do they able to unstuck they selves when path got block?

1 Like

hmm you can use Path.Blocked event to trigger when the path get blocked and then create a new path

1 Like