How to Optimize NPCs?

My current code uses a vast amount of the CPU and continues to increase over time.

After 15 minutes in-game:
image

After 25 minutes in-game:
image

--^ Game Services
local collectionService = game:GetService("CollectionService")
local pathfindingService = game:GetService("PathfindingService")

local replicatedStorage = game:GetService("ReplicatedStorage")
local assetsFolder = replicatedStorage:WaitForChild("Assets[Folder]") 
local animationFolder = assetsFolder:WaitForChild("Animations[Folder]")

local workspace = game:GetService("Workspace")
	local entityContainer = workspace:WaitForChild("EntityContainer")
	local pathfindingContainer = workspace:WaitForChild("PathfindingContainer")

function tagHumanoid(instance:Instance) --^ Tags the given "instance" with the "Entity" tag using the Collection Service
	collectionService:AddTag(instance:FindFirstChild("HumanoidRootPart"), "Interactable")
end

function randomPosition() -- ^ Returns a random position within the "pathfindingContainer"
	local pathfindingParts = pathfindingContainer:GetChildren()
	local randomPartIndex = math.random(#pathfindingParts)
	local randomPart = pathfindingParts[randomPartIndex]
	local randomPos = (randomPart.CFrame * CFrame.new(
		(math.random() - 0.5) * randomPart.Size.X,
		(math.random() - 0.5) * randomPart.Size.Y,
		(math.random() - 0.5) * randomPart.Size.Z
		)).Position
	return randomPos
end

local function AiPathfindingController(entity, destination) --^ Pathfinding Function
	local waypoints
	local nextWaypointIndex
	local reachedConnection
	local blockedConnection

	local humanoid = entity:FindFirstChild("Humanoid")
	local animator = humanoid:WaitForChild("Animator")

	local fowardAnim = animationFolder:WaitForChild("PenguinFoward")
	local idleAnim = animationFolder:WaitForChild("PenguinIdle")

	local forwardAnimTrack = animator:LoadAnimation(fowardAnim)
	local idleAnimTrack = animator:LoadAnimation(idleAnim)
	forwardAnimTrack.Priority = Enum.AnimationPriority.Movement
	idleAnimTrack.Priority = Enum.AnimationPriority.Idle
	idleAnimTrack:Play()

	local randomPosition = randomPosition() --^ assigns random Vector3 value to variable

	local path 

	local defaultEntityPath = pathfindingService:CreatePath({ --^ Path Parameters Table
		["AgentRadius"] = 2.5,
		["AgentHeight"] = 3,
		["AgentCanJump"] = false,
		["AgentJumpHeight"] = 7,
		["AgentCanClimb"] = false,
		["AgentMaxSlope"] = 45,
		["WaypointSpacing"] = 2,
		["Cost"] = {
			["Water"] = 20,
		}
	}) 

	local largeEntityPath = pathfindingService:CreatePath({ --^ Path Parameters Table
		["AgentRadius"] = 5,
		["AgentHeight"] = 7,
		["AgentCanJump"] = false,
		["AgentJumpHeight"] = 7,
		["AgentCanClimb"] = false,
		["AgentMaxSlope"] = 45,
		["WaypointSpacing"] = 2,
		["Cost"] = {
			["Water"] = 20,
		}
	}) 

	if entity.Name == "GiantPenguin" then
		path = largeEntityPath
	else
		path = defaultEntityPath
	end
	
	--^ Compute the path
	local success, errorMessage = pcall(function()
		path:ComputeAsync(entity.PrimaryPart.Position, destination)
	end)
	if  entity.Parent == entityContainer then
		if success and path.Status == Enum.PathStatus.Success then
			waypoints = path:GetWaypoints() --^ Get the path waypoints
--[[
			for i, waypoint in pairs(waypoints) do --^ Visualizes Waypoints
				local visualWaypoint = Instance.new("Part")
				visualWaypoint.Size = Vector3.new(0.3, 0.3, 0.3)
				visualWaypoint.Anchored = true
				visualWaypoint.CanCollide = false
				visualWaypoint.Material = Enum.Material.Neon
				visualWaypoint.Shape = Enum.PartType.Ball
				visualWaypoint.Position = waypoint.Position
				visualWaypoint.Parent = workspace
			end
]]
			--^ Detect if path becomes blocked
			blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
				--^ Check if the obstacle is further down the path
				if blockedWaypointIndex >= nextWaypointIndex then
					--^ Stop detecting path blockage until path is re-computed
					blockedConnection:Disconnect()
					--^ Call function to re-compute new path
					AiPathfindingController(entity, destination)
				end
			end)

			--^ Detect when movement to next waypoint is complete
			if not reachedConnection then
				reachedConnection = entity:FindFirstChild("Humanoid").MoveToFinished:Connect(function(reached)
					if reached and nextWaypointIndex < #waypoints then
						--^ Increase waypoint index and move to next waypoint
						nextWaypointIndex += 1
						entity:FindFirstChild("Humanoid"):MoveTo(waypoints[nextWaypointIndex].Position)
					else
						forwardAnimTrack:Stop()
						idleAnimTrack:Play()
						path:Destroy()
						reachedConnection:Disconnect()
						blockedConnection:Disconnect()
						wait(math.random(1, 15))
						AiPathfindingController(entity, randomPosition)
					end
				end)
			end
			--^ Initially move to second waypoint (first waypoint is path start; skip it)
			nextWaypointIndex = 2
			entity:FindFirstChild("Humanoid"):MoveTo(waypoints[nextWaypointIndex].Position)
			forwardAnimTrack:Play()
		else
			--print(path.Status)
			warn("Path not computed!", errorMessage)
			AiPathfindingController(entity, destination)
		end
	end
end

entityContainer.ChildAdded:Connect(function(entity)
	local animator = Instance.new("Animator", entity:FindFirstChild("Humanoid"))
	--print(entity)
	tagHumanoid(entity) --^ Tags the child that was added to "pathfindingContainer" with the "Entity" tag
	AiPathfindingController(entity, randomPosition())

	local humanoid = entity:FindFirstChild("Humanoid") --^ Checks instance for a humanoid
	if humanoid then
		for i, parts in pairs(entity:GetDescendants()) do --^ Loops through all children of the instance
			if parts:IsA("BasePart") or parts:IsA("MeshPart") or parts:IsA("UnionOperation") then --^ Checks if the child is a part
				parts.CollisionGroup = "Entity" --^ Sets the collision group of the part to "Entity"
			end
		end
	end
end)

I’m sure there are solutions to this; I’ve played several games that handle well over 200-300+ NPCs with less than 5ms CPU usage. I’m hoping someone can explain why my code uses so much of the CPU and why the CPU usage continually increases over time. As well as an overall solution to the problem. Thank you.

the script looks fine, could there be something else in the game that is causing your cpu to go up? The easiest way to test would be to disable the script and go in game and see if the same results occur, if so. Maybe you have too many of them pathfinding at the same time?

2 Likes

When I disable the script it goes down significantly but the ms still increases but much slower. As for the number of NPCs, I’m trying to have Pathfind it’s around 250 - 300. I don’t know if this could be part of the problem but every entity has a rigid constraint.

image

You should comment out chunks of your code until the performance increases.

That way you can determines which parts of the code are causing the issues.

Could be that you have to do less pathfunding calls, but without profiling it’s impossible to tell.

2 Likes

If the fps is still good while the CPU doing this, then I think that is weird. But otherwise I believe that MULTITHREADING is what you need.
Here is the video I used to learn everything about multithreading.

Also the CPU workload depends on what kind of CPU you have.
The video shows how a NPC system can benefit from multithreading as well.
Happy Coding!

2 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.