Pathfinding Zombie Not Efficient

  • I am trying to develop an efficient, non-hesitating zombie.

  • The current script I am using (see source script below) for the zombie makes the zombie stop for a few seconds after a player has moved. Basically, I am trying to make my zombie act like zombies in Zombie Rush, which generate new paths instantly, as soon as a player moves and doesn’t have to wait a few seconds (unlike my zombie at the moment).

  • I have researched beforehand on this topic and no topics have answered my problem as of yet.

I also want my zombie to constantly be moving towards each waypoint without stopping (also like Zombie Rush)

Source Script:

local myHuman = script.Parent:WaitForChild("Humanoid")
local myRoot = script.Parent:WaitForChild("HumanoidRootPart")
local head = script.Parent:WaitForChild("Head")
local lowerTorso = script.Parent:WaitForChild("Torso")

local clone = script.Parent:Clone()

function walkRandomly()
	local xRand = math.random(-50, 50)
	local zRand = math.random(-50, 50)
	local goal = myRoot.Position + Vector3.new(xRand,0,zRand)

	local path = game:GetService("PathfindingService"):CreatePath()
	path:ComputeAsync(myRoot.Position, goal)
	local waypoints = path:GetWaypoints()
	myHuman.WalkSpeed = 20
	
	if path.Status == Enum.PathStatus.Success then
		for _, waypoint in ipairs(waypoints) do
			if waypoint.Action == Enum.PathWaypointAction.Jump then
				myHuman.Jump = true
			end
			myHuman:MoveTo(waypoint.Position)
			local timeOut = task.wait(myHuman.MoveToFinished)
			if not timeOut then
				print("Got stuck")
				myHuman.Jump = true
				walkRandomly()
				break
			end
		end
	else
		print("Path failed from random")
		wait(1)
		walkRandomly()
	end
end

function findPath(target)
	local path = game:GetService("PathfindingService"):CreatePath()
	path:ComputeAsync(myRoot.Position,target.Position)
	local waypoints = path:GetWaypoints()
	myHuman.WalkSpeed = 20
	if path.Status == Enum.PathStatus.Success then
		for _, waypoint in ipairs(waypoints) do
			if waypoint.Action == Enum.PathWaypointAction.Jump then
				myHuman.Jump = true
			end
			myHuman:MoveTo(waypoint.Position)
			local timeOut = myHuman.MoveToFinished:Wait(0.000001)
			if not timeOut then
				myHuman.Jump = true
				print("Path too long!")
				findPath(target)
				break
			end
			if checkSight(target) then
				repeat
					print("Moving directly to the target")
					myHuman.WalkSpeed = 16
					myHuman:MoveTo(target.Position)
					attack(target)
					wait(0.000001)
					if target == nil then
						break
					elseif target.Parent == nil then
						break
					end
				until checkSight(target) == false or myHuman.Health < 1 or target.Parent.Humanoid.Health < 1
				break
			end
			if (myRoot.Position - waypoints[1].Position).magnitude > 20 then
				print("Target has moved, generating new path")
				findPath(target)
				break
			end
		end
	else
		myHuman.WalkSpeed = 16
		walkRandomly()
		--myHuman:MoveTo(target.Position)
	end
end

function checkSight(target)
	local ray = Ray.new(myRoot.Position, (target.Position - myRoot.Position).Unit * 40)
	local hit,position = workspace:FindPartOnRayWithIgnoreList(ray, {script.Parent})
	if hit then
		if hit:IsDescendantOf(target.Parent) and math.abs(hit.Position.Y - myRoot.Position.Y) < 10 then
			print("I can see the target")
			return true
		end
	end
	return false
end

function findTarget()
	local dist = 500
	local target = nil
	local potentialTargets = {}
	local seeTargets = {}
	for i, v in ipairs(workspace:GetChildren()) do
		local Torso = v:FindFirstChild("Torso") or v:findFirstChild("HumanoidRootPart")
		local Humanoid = v:findFirstChild("Humanoid")
		local Zombie = v:FindFirstChild("RealZombieScript")
		if (Torso ~= nil) and (Humanoid ~= nil) and (Humanoid.Health > 0) and (Zombie == nil) then
			if (myRoot.Position - Torso.Position).magnitude < dist and Humanoid.Health > 0 then
				table.insert(potentialTargets,Torso)
			end
		end
	end
	if #potentialTargets > 0 then
		for i, v in ipairs(potentialTargets) do
			if checkSight(v) then
				table.insert(seeTargets, v)
			elseif #seeTargets == 0 and (myRoot.Position - v.Position).magnitude < dist then
				target = v
				dist = (myRoot.Position - v.Position).magnitude
			end
		end
	end
	if #seeTargets > 0 then
		dist = 400
		for i,v in ipairs(seeTargets) do
			if (myRoot.Position - v.Position).magnitude < dist then
				target = v
				dist = (myRoot.Position - v.Position).magnitude
			end
		end
	end
	--if target then
	--	if math.random(20) == 1 then
	--	end
	--end -- could be used for random attacks
	return target
end

function attack(target)
	if (myRoot.Position - target.Position).magnitude < 5 then
		if target.Parent ~= nil then
			target.Parent.Humanoid:TakeDamage(25)
			wait(0.5)
		end
	end
end

function died()
	wait(1)
	--clone.Parent = workspace
	game:GetService("Debris"):AddItem(script.Parent, 0.1)
end

myHuman.Died:Connect(died)

lowerTorso.Touched:Connect(function(obj)
	if not obj.Parent:FindFirstChild("Humanoid") then
		myHuman.Jump = true
	end
end)

function main()
	local target = findTarget()
	if target then
		myHuman.WalkSpeed = 16
		findPath(target)
	else
		myHuman.WalkSpeed = 16
		walkRandomly()
	end
end

while wait(0.1) do
	if myHuman.Health < 1 then
		break
	end
	main()
end

I would recommend not relying on PathFinding strictly. Pathfinding takes some time, which is why your zombie “hesitates”, but using :MoveTo is simple, and thus faster. So, what you should do is check if there’s anything between the player and the zombie (tree, wall etc), and just use :MoveTo when nothing is blocking the zombie’s path

Yes I have used MoveTo before but I am using Pathfinding as these zombies will have to get players without getting stuck on walls. And it’s definitely possible as, as I have said, Zombie Rush zombies can do what I am looking for.

If there is any way to make the zombies instantly generate a new path and basically never stop moving then please let me know.

well you are waiting every 0.1 second before calling the main function again.

switch it to wait 0 seconds


your loops also yield the script (pause), causing it to have to finish its path’s point before switching to the next.

if you really want it to always be detecting a player (not a good idea for large amount of npcs) you will have to use corutines to move it to a different thread (so it wont yield the script) and check the players on a loop; if found break your pathfinding and move to rushing the player


you timeout for the movetofinished is under a milisecond, making it pretty much not even wait until it moved to its path. this causing it just keep on generating paths over and over (resource heavy/laggy) without actually traveling on that path

by what do you mean generate a new path instantly?

  • always generate a path to each player every tick? (not a good idea)
  • check if the player is in range and in view after moving to its point?
  • while walking randomly check each player if they get close enough? (less laggy but still could become an issue with more npcs)

could you explain further what you wish for

What I mean is, as soon as a Player moves from an area that the zombie is following a path to, it should generate a new path to the player’s new Position.

I have had no experience with Coroutines, could you tell me how and where I could use this?

I really don’t understand why you think you are allowing 0.000001 of a second for the npc to move to the next waypoint, before seemingly recomputing the path. The npc will walk a tony percentage of a stud in the time and any player will likely have moved similar amount unless they are in a jet fighter.

Why not change this:

local timeOut = myHuman.MoveToFinished:Wait(0.000001)

to a standard

myHuman.MoveToFinished:Wait(1)

to allow the npc to move to the first waypoint, then recompute the path. The player will not have moved very far in that timeout duration of 1 second.
I would also suggest only referencing the first waypoint as you recalculate the path anyway, why bother taking them all.
A quick and dirty rewrite of the section:

local path = game:GetService("PathfindingService"):CreatePath()
	path:ComputeAsync(myRoot.Position,target.Position)
	myHuman.WalkSpeed = 20
	if path.Status == Enum.PathStatus.Success then
		local waypoints = path:GetWaypoints()  -- move this here, why take them if not success??
		local  waypoint = waypoints [1] -- only use the first waypoint
		if waypoint.Action == Enum.PathWaypointAction.Jump then
				myHuman.Jump = true
		end
		myHuman:MoveTo(waypoint.Position)
		local timeOut = myHuman.MoveToFinished:Wait(1) -- wait 1s or even 0.5s
	end
-- skip some stuff
	if checkSight(target) then
		-- move directly to target, but pls change yr wait(0.000001) to something sensible

Wouldn’t I need to increase the waypoint index once timeOut has finished?

No. You just allow that function to end. It will then return to main, then return to the while wait and run through the sequence again.

1 Like