NPC Stuttering / Pathfinding Issues

Hey there! I have a game where waves of NPCs spawn and attack the NPCs on the opposite team, as well as any players on the opposite team. I run into issues when more waves spawn, the game starts to lag. The NPCs also start to stutter and do not move correctly. I’m also having issues with their animations. If you could help in any way, that would be amazing! Here is a clip of what I mean. Code can be found below, and I can give more visuals if you need.

https://gyazo.com/a3ef5805ce574892ae413e35a436c9c8

This is my full script for NPC management. Any advice would be amazing. Thanks for all your help!

wait(5)
-- CONSTANTS
local numNPCs = 70
local RANGE = 50
local DAMAGE = 5

-- SERVICES
local PathfindingService = game:GetService("PathfindingService")

-- Pathing
local pathParams = {
	AgentHeight = 5,
	AgentRadius = 5,
	AgentCanJump = false,
}

-- Tables
local humans = {}
local orcs = {}

-- Variables
local orcs = game.ReplicatedStorage:WaitForChild("OrcsUnlocked")
local humans = game.ReplicatedStorage:WaitForChild("HumansUnlocked")

local map = game.Workspace:FindFirstChild(game.ServerStorage.Events.MatchStart.Event:Wait(), true)

local humansRecurringSpawn = map:FindFirstChild("HumansRecurring", true)
local orcsRecurringSpawn = map:FindFirstChild("OrcsRecurring", true)
local humansUnlocked = map:FindFirstChild("HumansUnlocked", true)
local orcsUnlocked = map:FindFirstChild("OrcsUnlocked", true)


local waypoints = map:FindFirstChild("Pathpoints", true)

local lastPos
local animPlaying = false


function spawnNPC(npcName, team)
	local npc
	local spawner 
	if team == "human" then
		npc = game.ReplicatedStorage.HumansUnlocked:FindFirstChild(npcName):Clone()
		npc.Parent = humansUnlocked
		for i, spawner in pairs(humansRecurringSpawn:GetChildren()) do
			if spawner.Name == npcName then
				npc:PivotTo(spawner.CFrame)
			end
		end
	elseif team == "orc" then
		npc = game.ReplicatedStorage.OrcsUnlocked:FindFirstChild(npcName):Clone()
		npc.Parent = orcsUnlocked
		for i, spawner in pairs(orcsRecurringSpawn:GetChildren()) do
			if spawner.Name == npcName then
				npc:PivotTo(spawner.CFrame)
			end
		end
	end

	-- Get Humanoid and Set Network Owner for less lag
	local humanoid = npc:WaitForChild("Humanoid")
	local hrp = npc:WaitForChild("HumanoidRootPart")
	hrp:SetNetworkOwner(nil)

	-- Establish Raycasting for the npc
	local rayParams = RaycastParams.new()
	rayParams.FilterType = Enum.RaycastFilterType.Exclude
	rayParams.FilterDescendantsInstances = {npc}

	-- Load Animations for the NPC
	local walkAnim = humanoid.Animator:LoadAnimation(script.WalkAnim)
	local attackAnim = humanoid.Animator:LoadAnimation(script.Attack)


	-- Functions for the NPC
	local function canSeeTarget(target)
		local orgin = hrp.Position
		local direction = (target.HumanoidRootPart.Position - hrp.Position).Unit * RANGE
		local ray = workspace:Raycast(orgin, direction, rayParams)

		if ray and ray.Instance then
			if ray.Instance:IsDescendantOf(target) then
				return true
			else
				return false
			end
		else
			return false
		end
	end
	
	local function findTarget()
		local enemies 
		if team == "orc" then
			enemies = humansUnlocked
			
		elseif team == "human" then
			enemies = orcsUnlocked
		end
		local targets = {}

		for _, child in ipairs(enemies:GetChildren()) do
			if child:IsA("Model") and child:FindFirstChild("Humanoid").Health > 0 then
				table.insert(targets, child)
			end
		end
				
		local players = game.Players:GetPlayers()
		local maxDistance = RANGE
		local nearestTarget
		
		for i, player in pairs(players) do
			if player.Character and player.Team.TeamColor ~= npc.Team.TeamColor and player.Character.Humanoid.Health > 0 then
				table.insert(targets, player)
			end
		end

		for i, item in pairs(targets) do
			if item.Team.TeamColor ~= npc.Team.TeamColor then
				if item:isA("Player") then
					local target = item.Character
					local distance = (hrp.Position - target.HumanoidRootPart.Position).Magnitude
					if distance < maxDistance and canSeeTarget(target) then
						nearestTarget = target
						maxDistance = distance
					end
				else
					local target = item
					local distance = (hrp.Position - target.HumanoidRootPart.Position).Magnitude
					if distance < maxDistance and canSeeTarget(target) then
						nearestTarget = target
						maxDistance = distance
					end
				end
			end
		end
		return nearestTarget
	end

	local function getPath(destination)
		local path = PathfindingService:CreatePath(pathParams)

		path:ComputeAsync(hrp.Position, destination.Position)

		return path	
	end

	local function attack(target)
		local distance = (hrp.Position - target.HumanoidRootPart.Position).Magnitude
		local debounce = false

		if distance > 7 then
			humanoid:MoveTo(target.HumanoidRootPart.Position)
		else
			if debounce == false and humanoid.Health > 0 then
				debounce = true
				npc.Head.AttackSound:Play()
				attackAnim:Play()
				target.Humanoid.Health -= DAMAGE
				task.wait(0.5)
				debounce = false
			end
		end
	end

	local function walkTo(destination)
		local path = getPath(destination)

		if path.Status == Enum.PathStatus.Success then
			for i, waypoint in pairs(path:GetWaypoints()) do
				path.Blocked:Connect(function()
					path:Destroy()
				end)

				if animPlaying == false then
					walkAnim:Play()
					animPlaying = true
				end

				attackAnim:Stop()

				local target = findTarget()

				if target and target.Humanoid.Health > 0 then
					lastPos = target.HumanoidRootPart.Position
					local distance = (hrp.Position - target.HumanoidRootPart.Position).Magnitude
					if distance <= 7 then
						walkAnim:Stop()
						animPlaying = false
					end
					attack(target)
					--break
				else
					if waypoint.Action == Enum.PathWaypointAction.Jump then
						humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
					end

					if lastPos then
						humanoid:MoveTo(lastPos)
						--humanoid.MoveToFinished:Wait()
						lastPos = nil
						--break
					else
						wait(0.1)
						humanoid:MoveTo(waypoint.Position)
						humanoid.MoveToFinished:Connect(function()
							wait(0.2)
						end)
					end
				end
			end
		else
			return
		end
	end
	
	local lastWaypoint = {}

	local function patrol()
		while humanoid.Health > 0 do
			wait(0.02)
			print("printing")
			local waypoints = waypoints:GetChildren()
			if team == "orc" then
				for waypoint = 2, #waypoints do
					if waypoints[waypoint] ~= lastWaypoint[npc] and humanoid.MoveDirection.Magnitude == 0 then
						walkTo(waypoints[waypoint])
						lastWaypoint[npc] = waypoints[waypoint]
					end
				end
			elseif  team == "human" then
				for waypoint = (#waypoints - 1), 1, - 1 do
					if waypoints[waypoint] ~= lastWaypoint[npc] and humanoid.MoveDirection.Magnitude == 0 then
						walkTo(waypoints[waypoint])
						lastWaypoint[npc] = waypoints[waypoint]
					end
				end
			end
		end
	end

	while task.wait(0.02) and humanoid.Health > 0 do
		print("running")
		patrol()
	end
end

local matchInProgress = false

function main()
	print("Match Started")
	matchInProgress = true
	while matchInProgress do
		wait(5)
		for i, spawners in pairs(humansRecurringSpawn:GetChildren()) do
			task.spawn(function()
				spawnNPC(spawners.Name, "human")
			end)
		end
		
		for i, spawners in pairs(orcsRecurringSpawn:GetChildren()) do
			task.spawn(function()
				spawnNPC(spawners.Name, "orc")
			end)
		end
		wait(40)
	end
end

game.ServerStorage.Events.MatchStart.Event:Connect(main())

game.ServerStorage.Events.MatchEnd.Event:Connect(function()
	matchInProgress = false
end)

You could try to create multiple destinations OR have the destination update every 0.1 seconds OR try using the Humanoid:MoveTo (goal = humanoid.WalkToPart.CFrame:pointToObjectSpace(humanoid.WalkToPoint)) and mess around with that.

(From dev hub)

WalkToPart is a reference to a part that the Humanoid is trying to reach. This property is normally set when a part is passed as the 2nd argument of the Humanoid’s Humanoid:MoveTo function.

When WalkToPart is set and a humanoid is actively trying to reach the part, it will keep updating its Vector3 goal to be the position of the part, plus the Humanoid.WalkToPoint translated in object space relative to the rotation of the part

Currently what I want is that all NPC’s stay on the same “path” so that they HAVE to interact with enemy NPC’s. However once they detect a player or an NPC, they abandon the path and go attack them. Once they finish defeating an enemy, the NPC goes back on the path where it was and continues on. That’s the biggest issue I think. I’m not sure what you mean by multiple destinations? Thanks for your help

1 Like

Guess that script didn’t help.

This is exactly how they should act …
Maybe set them as attackable when they get within the range you wish.
Or set the attack range itself super low.

I mean by multiple destinations is that, you could have, let’s say: 1,2,3,4,5,6 destinations. 1 being at the start and 6 at the end.

Yes, that is the way I am doing it currently.

1 Like

That is probably why they’re stopping. Try to make them keep going after after they’ve touched the current destination.

1 Like

have you tried using a coroutine? That is how my stutter got fixed

Based on the video, I would consider it highly likely that is the issue as the npc are moving one after another.

Also consider using MoveToFinish:Wait() since the wait inside the function won’t happen until reached, and by then (if you used a coroutine, the npc won’t wait for everyone else to finish) the npc would already be moving toward the next waypoint prematurely

Please dont use roblox’s pathfinding service :sob::pray: there are plenty of other resources out there that have simply better path finding

What are you talking about? (no offense, this is the first time I heard that)

This is what im talking about:
Forbidden is proven to be way better than traditional roblox pathfinding