Pathfinding causes massive lag

I’ve been working on better pathfinding for monsters for my game, 2 functions really:

  • Wander around every x seconds (Every 5-7 seconds)
  • Follow the player if nearby (~100 Studs, executed every frame)

I have 480 Monsters, which is causing extreme lag making the game next to unplayable.

I’m using a very modified version of SimplePath removing all of it’s events and all other extra stuff I don’t need (the pathfinding does work perfectly).

Here are the things I’ve tried:

  • If there are no players nearby the check for nearby players to then follow them then we’ll wait 1 second in the Follow function before doing checks and trying to follow again instead of doing it every frame.

  • Same as above but for Wandering, making the wait a lot longer too.

Making the follow player function wait more doesn’t improve in performance and just makes the following look choppy and it’s unprecise.

Scripts incase you want to check them out:

PathfindingModule
local Path = {}
Path.__index = Path

local PathfindingService = game:GetService("PathfindingService")

function Path.new(Agent, AgentParameters)
	local NewPath = {}

	setmetatable(NewPath, Path)

	NewPath["Path"] = PathfindingService:CreatePath(AgentParameters)
	NewPath["Agent"] = Agent
	NewPath["Humanoid"] = Agent:FindFirstChildWhichIsA("Humanoid")
	NewPath["AgentParameters"] = AgentParameters

	NewPath["LastTime"] = os.clock()

	NewPath["Position"] = {
		Count = 0,
		Last = Vector3.new(),
	}

	NewPath["Connections"] = {}

	NewPath["VisualWaypoints"] = {}

	return NewPath
end

local function GetNonHumanoidWaypoint(self)
	
	if not self.Humanoid then return end
	
	for Waypoint = 2, #self.Waypoints do
		if (self.Waypoints[Waypoint].Position - self.Waypoints[Waypoint - 1].Position).Magnitude > 0.1 then
			return Waypoint
		end
	end
	return 2
end

local function Jump(self)
		
	pcall(function()
		if self.Humanoid:GetState() ~= Enum.HumanoidStateType.Jumping and self.Humanoid:GetState() ~= Enum.HumanoidStateType.Freefall then
			self.Humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
		end
	end)
end

local function ComparePosition(self)
	
	if self.CurrentWaypoint == #self.Waypoints then return end

	self.Position.Count = ((self.Agent.PrimaryPart.Position - self.Position.Last).Magnitude <= 0.07 and (self.Position.Count + 1)) or 0
	self.Position.Last = self.Agent.PrimaryPart.Position

	if self.Position.Count >= 1 then
		Jump(self)
	end
end

local function Move(self)
	
	if self.Waypoints[self.CurrentWaypoint].Action == Enum.PathWaypointAction.Jump then
		Jump(self)
	end

	self.Humanoid:MoveTo(self.Waypoints[self.CurrentWaypoint].Position)
end

local function MoveToFinished(self, Reached)
	
	
	if Reached and self.CurrentWaypoint + 1 <= #self.Waypoints  then

		self.CurrentWaypoint += 1
		Move(self)
	elseif Reached then
		self.Connections["MoveConnection"]:Disconnect()
		self.Connections["MoveConnection"] = nil
	else
		self.Connections["MoveConnection"]:Disconnect()
		self.Connections["MoveConnection"] = nil
	end
end

local function Visualize(self)
	
	for _, Waypoint in pairs(self.VisualWaypoints) do
		Waypoint:Destroy()
	end
	
	table.clear(self.VisualWaypoints)

	for _, Waypoint in pairs(self.Waypoints) do
		local Part = Instance.new("Part")
		Part.Size = Vector3.new(1, 1, 1)
		Part.Anchored = true
		Part.CanCollide = false
		Part.Position = Waypoint.Position
		Part.Parent = game.Workspace.Map

		if Waypoint.Action == Enum.PathWaypointAction.Jump then
			Part.Color = Color3.fromRGB(0, 200, 255)
		end

		table.insert(self.VisualWaypoints, Part)
	end

end

function Path:Run(Target)
	
	if not self.Humanoid then return end
	
	if os.clock() - self.LastTime <= 0.07 and self.Humanoid then
		task.wait(os.clock() - self.LastTime)
		return false

	elseif self.Humanoid then
		self.LastTime = os.clock()
	end

	local ComputedPath, _ = pcall(function()
		self.Path:ComputeAsync(self.Agent.PrimaryPart.Position, Target)
	end)

	if not ComputedPath
		or self.Path.Status == Enum.PathStatus.NoPath
		or #self.Path:GetWaypoints() < 2
		or (self.Humanoid and self.Humanoid:GetState() == Enum.HumanoidStateType.Freefall) then
		task.wait()
		return 
	end

	self.Target = Target

	pcall(function()
		self.Agent.PrimaryPart:SetNetworkOwner(nil)
	end)

	self.Waypoints = self.Path:GetWaypoints()
	self.CurrentWaypoint = 2

	for _, Waypoint in pairs(self.VisualWaypoints) do
		Waypoint:Destroy()
	end

	ComparePosition(self)

	self.Connections["MoveConnection"] = self.Humanoid and (self.Connections["MoveConnection"] or self.Humanoid.MoveToFinished:Connect(function(...)
		MoveToFinished(self, ...)
	end))

	if self.Humanoid then
		self.Humanoid:MoveTo(self.Waypoints[self.CurrentWaypoint].Position)
	elseif #self.Waypoints == 2 then
		self.Target = nil
	else
		self.CurrentWaypoint = GetNonHumanoidWaypoint(self)
		MoveToFinished(self, true)
	end
	return true
end

function Path:Destroy()
	setmetatable(self, nil)
	table.clear(self)
	table.freeze(self)
	self = nil
end


return Path
Wander/Follow/Pathfinding function for the Monsters
local function WanderScript(Monster)
	local Humanoid = Monster:WaitForChild("MonsterHumanoid")
	local StartPosition = Monster:WaitForChild("StartPosition")
	local Status = Monster:WaitForChild("Status")
	local HumanoidRootPart = Monster:WaitForChild("HumanoidRootPart")

	local ValuesFolder = ValuesModule[Monster.Name]

	local AgentParameters = {
		AgentHeight = 10,
		AgentRadius = 3,
		AgentCanJump = true,
		AgentCanClimb = true,
		Costs = {
			Water = 20,
		}
	}

	local Path = PathfindingModule.new(Monster, AgentParameters)

	local IsDead = false

	local HasPlayerNearby = false
	local HasPlayersInZone = false

	local LastHitTick = 0


	if ValuesFolder["CanFollow"] then
		if ValuesFolder["CanFollow"] == true then
			coroutine.wrap(function()

				while true do
					
					if HasPlayerNearby == false then
						task.wait(1)
					end
					
					task.wait()
					
					if IsDead == true then break end
					if not Monster:FindFirstChild("HumanoidRootPart") then continue end

					local Target = FindNearestTorso(Monster, HumanoidRootPart.Position, 100)
					
					

					if Target then
						
						
						Status.Value = "Following"
						Path:Run(Target.Position)
						HasPlayerNearby = false


					elseif Target == nil and Status.Value == "Following" then

						Status.Value = "Wandering"
						Path:Run(StartPosition.Position)
						HasPlayerNearby = false
					end
				end

			end)()
		end
	end




	if ValuesFolder["CanWander"] then
		if ValuesFolder["CanWander"] == true then
			coroutine.wrap(function()
				while true do
					task.wait(math.random(5, 7))
					if HasPlayerNearby == false then
						task.wait(10)
					end
					if IsDead == true then break end
					if not Monster:FindFirstChild("HumanoidRootPart") then continue end
					if Status == "Following" then continue end
					
					

					Path:Run(Vector3.new(HumanoidRootPart.Position.X + math.random(-50, 50), HumanoidRootPart.Position.Y, HumanoidRootPart.Position.Z + math.random(-50, 50)))
				end
			end)()
		end
	end


	coroutine.wrap(function()
		while task.wait(1) do
			if IsDead == true then break end
			if (tick() - LastHitTick) < 15 then continue end

			Humanoid.Health = Humanoid.Health + (Humanoid.MaxHealth * 0.01)
		end
	end)()


	local CurrentHealth = Humanoid.Health

	Humanoid.HealthChanged:Connect(function()
		if Humanoid.Health < CurrentHealth then
			LastHitTick = tick()
		end

		CurrentHealth = Humanoid.Health
	end)

	Humanoid.Died:Connect(function()
		Path:Destroy()
		IsDead = true
	end)
end

What can I do to get rid of the lag? What do you recommend? How could it be optimised?

I’ve been trying for 3 days and I could really use the ideas/recomendations.
Please have mercy on my soul.

1 Like

You could potentially try using Parallel Luau. It might help you quite a bit if you haven’t tried using that already.

maybe instead of recalculating the path every frame, you could change it so that the monster moves every frame but the actual path only gets recalculated once every second.