Optimization For A Zombie Movement Script

Hey there,

So I am currently working on a huge project that relates to a zombie game. In this game we will be having a ton of moving zombies chasing players, and killing them. From what I currently have scripted, the zombie movement is similar to that of Zombie Attack or something similar.

Having potentially hundreds of moving zombies at once could pose the problem of optimization. The script that I currently have is probably very poorly optimized. After suplicating the zombie around like 30 to 50 times the game seems to struggle. What are some ideas or things that i could use to prevent this problem, but also still have the main functionality that is already in this script?

Let me know if you have any questions about this, and any help would be greatly appreciated!

Here is the current script, which is a child of the zombie rig itself.

local hum = script.Parent.Humanoid
local humRoot = script.Parent.HumanoidRootPart

local rs = game:GetService("RunService")

local function DetermineTarget()
	local nearestTarget = nil
	local nearestDistance = math.huge -- sets at a very larger number.  use this to make sure its closer than last target

	for i,v in game.Players:GetPlayers() do -- you can remove pairs completely and just pass a table and it will automatically determine which is better
		local character = v.Character
		local humanoid = character and character:FindFirstChildOfClass("Humanoid")

		if humanoid and humanoid.Health > 0 then
			local Distance = (character.HumanoidRootPart.Position - humRoot.Position).Magnitude

			if Distance < nearestDistance then -- if the current player distance is less than the previous nearestDistance then set target 
				nearestTarget = v
				nearestDistance = Distance -- remember to set the nearestdistance when you set a new target
			end
		end
	end

	return nearestTarget
end

while true do
	local target = DetermineTarget()

	if target then -- cleaned this to not use not...
		hum:MoveTo(target.Character.HumanoidRootPart.Position, target.Character.HumanoidRootPart)
	else
		print("No target found")
	end

	task.wait() -- it is bad practice to put wait() as the condition for the while loop
end
1 Like

Well, the first thing i will say is that using hum:moveto() has its restrictions because if the zombie doesn’t reach the target in 8 seconds, it will timeout and freeze in place. That could be part of your problem.

Also, the loop could be a problem. I know you used task.wait, but a while true loop in 50 different zombies could be potentially laggy.

Try those two things first, and if it is still lagging, let me know :slightly_smiling_face:

Is there a good alternative to the loop and the :MoveTo function?

Well, you could use Pathfindingservice and get all of the waypoints in the generated path, and moveto all of those. Waypoints aren’t that far spaced apart, and on average takes less than one second to move to them.

A video about a zombie Pathfinding killer:

From my experience pathfindingservice is super glitchy. The zombie goes to where the player used to be, so when they walk it doesn’t update where he is walking. When using the second argument in moveto, it follows the player no matter where they move, and with pathfinding it doesn’t.

This post and subsequent discussion seems to be divided into ideas for optimizing the Zombie chasing/movement, and optimizing the AI behaviour (loop steps), so I thought I’d weigh in on the latter issue with an idea based on a system I’ve used in the past.

Main Idea
One way you might be able to save on performance by doing less work every loop update is to use two states for the zombie AI:

Scan: The zombie scans for the nearest Player to its location and assigns it as its target.
Pursue: The zombie pursues the selected Player target.

Instead of constantly scanning the distance from every Player every loop update, only do this once when needed to choose a Player as a target.

When a target is found, switch to the Pursue State. In this state, while the target Player is alive in-game and within a certain threshold distance, move the Zombie to that target player and don’t bother checking the distance of the remaining Players.

The added “threshold distance” suggestion would be to allow the zombie to change targets if the current target player gets too far away from the pursuing zombie. It wouldn’t look great if a zombie is pursuing a player 200 studs away with several other players nearby.

Limitations
The limitation of this solution is that the zombies will become fixed on pursuing a specific player and ignore other players for some time. In the worst-case, you could have most or all of a game’s zombies pursuing a single player, (though this is unlikely if you use a smaller threshold distance for abandoning a target, and/or the Player isn’t invincible).

Other Ideas

  • You could incorporate more ways for the zombie to abandon its current target and go after a new one, i.e. if the Zombie is chasing PlayerA and PlayerB starts attacking the Zombie, you could have the Zombie switch its target to PlayerB instead. (This would help overcome the limitation I mentioned).

  • You could use more states i.e. an idle state when no players are near the zombie when it scans. You could even adjust how frequently a zombie scans by the minimum distance of the closest player. i.e., if no players are nearby, wait longer until the next scan. (This again may improve performance by doing the scanning work less frequently).

Conclusion
Anyways, I’d argue that this divided behaviour approach would be a bit more performant because when the zombie is pursuing a specific Player, you don’t have to bother checking distances for other players.

I won’t say that this will totally solve your performance issues, but it may be able to help it a bit.

2 Likes

hum:moveto() isn’t the best way to make the zombie move, I would suggest removing while true do, also maybe use a pathfinding system, I think you would be able to find one in the resources topic. if you don’t I don’t know what else to say.
I do credit The Carl_Weezer guy for some of the suggestions ofcourse

1 Like

Agreed to be honest, its an efficient method.

I had encountered a similar optimization issue in one of my projects. If you don’t mind the zombies AI being effectively stateless, an easy solution is to use flow field pathfinding. This allows O(n) scaling of zombies, up to the thousands even without any noticable lag from your own code.

Here’s a link that details this approach: Flow Field Pathfinding – Leif Node

One noticable pitfall of this solution is flow fields effectively limit your zombies to a grid, and if a zombie is too close to a player or your map has especially small details, the grid may prevent the zombies from moving to a desired location. I personally had to write two pathfinding implementations when using this approach - one for far pathfinding where it doesn’t matter if the NPC’s movements are off by a couple studs, and one for close pathfinding where accuracy and timing is critical.

That’s exactly why you need to modify every pathfinding system even if it was the smallest flaw, also very annoying and devforums will barely help. that’s an issue on the devforums for me, it doesn’t trust people enough until they spend 4 hours, I don’t trust premade code to be honest, but some code I can trust. also is good advice, might use it myself at one point.