Any optimalization tips? (Managed about 1500 NPCs mostly without lag.)

Hello, Cartoon_Corpze here.
So I managed to get about 1000 - 1500 NPCs running in a game, mostly without lag.
Though, I’m looking for more ways to optimize my NPCs.
Script activity stays mostly below 4%, I actually haven’t had many perfomance issues, I have a pretty decent gaming laptop and I even tested the game in a real server with someone else, there wasn’t a lot of lag, sometimes we had a ping ranging from 60 to 120 ms but that’s it pretty much.

Here is a gif.
https://gyazo.com/f2412779af094dda63771b08737cc8d2

I tried to make something looking a little like Dead Rising.
All NPCs are zombies and there is about 1500 of them currently.

Here is the code.


local targetradius = game:GetService("ReplicatedStorage").npcs.targetradius

local npcs = {}


local activeais = 0
local maxactiveais = 150

local airange = 60

local cf = CFrame.new
local v3 = Vector3.new
local random = math.random


--Functions.


--[[Main AI function]]--
local function createai(npc)
	
		npcs[npc].main = function()
			--local npctable = npcs[npc] --Used for breaking while loop.
			local npc = npc
			local human = npc.Humanoid
			local root = npc.Torso
			local target = npc.target
			local active = npc.active
			
			local wandering = false
			local chasing = false
			
			human.WalkSpeed = random(8, 15)
			
			wait(random(1, 5))
			
			
			local targetevent = target.Changed:Connect(function()
				local targethuman = target.Value.Parent.Humanoid
				local targetroot = target.Value --.Parent.HumanoidRootPart
				
				if (not chasing) and targethuman.Health > 0 and activeais < maxactiveais then
					chasing = true
					wandering = false
					human:MoveTo(targetroot.Position, targetroot)
					active.Value = true
				end
			end)
			
			
			
			local mtf = human.MoveToFinished:Connect(function()
				if (not target.Value) and wandering then
					active.Value = false
					wandering = false
					chasing = false
					return
				elseif chasing and target.Value and (target.Value.Position - root.Position).magnitude < airange and activeais <= maxactiveais then
						active.Value = true
						root.Anchored = false
						human:MoveTo(target.Value.Position, target.Value)
				end
				wandering = false
			end)
			
			
			local activateevent = active.Changed:Connect(function()
				if active.Value == true then
					activeais = activeais + 1
					root.Anchored = false
				elseif active.Value == false then
					activeais = activeais - 1
					delay(1, function()
						repeat wait(1) until root.Velocity.magnitude < 0.1
						root.Anchored = true
					end)
					
				end
				--print("Active AIs:", activeais, "/", maxactiveais)
			end)
			
			
			--Start of while loop.
			while npc.Parent ~= nil and human.Health > 0 do
				if activeais < maxactiveais / 1.5 and not chasing then
					active.Value = true
					root.Anchored = false
					human:MoveTo(root.Position + v3(random(-20, 20), 0, random(-20, 20)))
					wandering = true
				end
				wait(random(5, 10))
				if npc.Torso.Anchored or npc.Torso.Velocity.magnitude < 0.05 then
					active.Value = false
					npc.Torso.Anchored = true
				end
			end
			--End of while loop.
			
			--Code below will run when NPC despawns.
			print("NPC despawned.")
			mtf:Disconnect()
			activateevent:Disconnect()
			targetevent:Disconnect()
			print("Events disconnected.")
			return
		end
		
		spawn(npcs[npc].main)
end
--[[Secondary AI function]]--
local function createai2(obj)
	obj.Touched:Connect(function(hit)
		wait(random(1, 10) / 10)
		if hit.Parent ~= nil and hit.Parent.Parent == workspace.npcs then
			local npc = hit.Parent
			
			if not npc.target.Value then
				npc.target.Value = obj.Parent.Parent.HumanoidRootPart
			elseif (obj.Position - hit.Position).magnitude < (npc.target.Value.Position - hit.Position).magnitude then
				npc.target.Value = obj.Parent.Parent.HumanoidRootPart
			end
			
			if npc.target.Value and (npc.target.Value.Position - hit.Position).magnitude < 4 and (not npc.attack.Value) then
				npc.attack.Value = true
				obj.Parent.Parent.Humanoid:TakeDamage(30)
				delay(1.5, function()
					npc.attack.Value = false
				end)
			end
		end
		
	end)
end
------------------------


--Events.

--NPC removed.
workspace.npcs.ChildRemoved:Connect(function(npc)
	if npc.active.Value == true and activeais > 0 then
		activeais = activeais - 1
	end
	
	for i = 1, #npcs do
		if npcs[i].root == npc.Torso then
			table.remove(npcs, i)
			print("NPC removed.")
		end
	end
	
end)


--NPC added.
workspace.npcs.ChildAdded:Connect(function(obj)
	if obj:FindFirstChild("Torso") then
		
		npcs[obj] = {root = obj.Torso}
		
		spawn(function()
			--[[Humanoid states.]]--
			--Disabled, unused.
			obj.Humanoid:SetStateEnabled(Enum.HumanoidStateType.Climbing, false)
			obj.Humanoid:SetStateEnabled(Enum.HumanoidStateType.FallingDown, false)
			obj.Humanoid:SetStateEnabled(Enum.HumanoidStateType.Flying, false)
			obj.Humanoid:SetStateEnabled(Enum.HumanoidStateType.Freefall, false)
			obj.Humanoid:SetStateEnabled(Enum.HumanoidStateType.Swimming, false)
			--?
			obj.Humanoid:SetStateEnabled(Enum.HumanoidStateType.PlatformStanding, false)
			obj.Humanoid:SetStateEnabled(Enum.HumanoidStateType.Landed, false)
			obj.Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, false) --?
			--Enabled.
			obj.Humanoid:SetStateEnabled(Enum.HumanoidStateType.Dead, true)
			obj.Humanoid:SetStateEnabled(Enum.HumanoidStateType.Running, true)
			obj.Humanoid:SetStateEnabled(Enum.HumanoidStateType.RunningNoPhysics, true)
			
			wait(5)
			local t = tick() + 20
			repeat
				wait()
			until obj.Torso.Velocity.magnitude < 0.01 or tick() > t
			obj.Torso.Anchored = true
		end)
		
		createai(obj)
		--print("NPC added.")
	end
end)




--Player added.
workspace.players.ChildAdded:Connect(function(plr)
	
	spawn(function()
		local root = plr:WaitForChild("HumanoidRootPart", 60)
		if not root then return end
		
		local range = targetradius:Clone()
		
		range.Parent = root
		range.CFrame = root.CFrame
		
		local weld = Instance.new("Motor6D")
		weld.Part0 = root
		weld.Part1 = range
		weld.Parent = range
		
		createai2(range)
	end)
end)



while true do
	wait(2)
	print("Active AIs:", activeais, "/", maxactiveais)
end

Surprisingly the script activity is pretty low.
The zombies aren’t incredibly smart or do a lot of thinking, I haven’t really done anything with pathfinding.
I was purely experimenting how much humanoid NPCs I could have in a game having it run still pretty decent.

If you have any hints or tips for me on how to improve my work I’d gladly listen to it.
You may notice I haven’t scripted a way to kill the NPCs yet but that may come later.
Right now I’m focusing on perfomance and high NPC counts.

For so far the game can handle 1500 NPCs. (They chase you as well when nearby and damage.)

Edit: Forgot to mention that there also is a clientside version of the script but that’s mostly for visual effects and animations.

28 Likes

A good start could be not rendering animations or not rendering NPCs at all that aren’t within reach or not visible to the user. You shouldn’t run constant logic on NPCs that are too far away from players either, instead just more or less predicting when they would be close enough to be rendered.

10 Likes

Alright, sounds like a good idea, though I do need distance NPCs to be visible to the player, else it looks a bit weird to me.
Thanks for the tip tho.

Currently I’m mostly looking for server optimalization tips.
The script I posted is the server side version of the NPC.

(NPCs are already rendered on client and such.)

1 Like

This is more suited to Code Review than Scripting Support, although your code seems to be working fine - 1500 NPCs without lag is generally a sign of decent scripting; I’m not certain that there are any non-trivial improvements to be made here.

3 Likes

How many FPS is your Computer maintaining with 1000 NPCs?

You are using Roblox R6 Humanoid, Disabling some HumanoidStates and Anchoring the Rootpart when it’s not moving right?

I did the same but I was using R15 and didn’t Anchor the RootPart, my Computer maintained 60 FPS with 200 NPCs Moving and Animated at the same time.

5 Likes

Even at 3000 NPCs I still have 60 FPS most of the time.

The root is anchored when the NPC isn’t moving indeed and visual effects such as animation and appearance is clientside.

I use Roblox R6 humanoids, a lot of the states are disabled except the ones I truly need.

Though I would like to know if there are more ways top optimize to even get the limit to 10000 NPCs possibly.

I have no idea, I’m still trying to figure out how you manage to maintain 60 FPS with 3000 NPCs with Roblox Humanoid while most of the DevForum members would recommend to not use Roblox Humanoid and Rendering Characters on the Client.

So let me get this right;

You are using Only Roblox Humanoid with R6 rigs with most States disabled and RootPart Anchored and you can maintain 3000 NPCs at 60 FPS with no other special gimmick?

My hypothesis is that with 3000 NPCs you’ll have a total of at least 21000 (3000x7) parts

Roblox struggles while handling 500 moving sphere parts at once so moving all of those NPCs at once would be too much for the Engine (in extreme cases)

So if you have a Max cap of 150 moving NPCs at once that’s 1050 (150x7) parts max at any given moment

Since you are using a Humanoid few parts are forced to be CanCollide = true, although most of the NPCs aren’t moving thus Anchored.

So if you want to have 10000 NPCs which is 70000 (10000x7) parts, you should figure out if Roblox can handle at least 70000 parts in a game in a small space



If you don’t mind me asking what is your computer’s spec?

My Laptop Specs

Alienware 17 R5

OS: Windows 10 Pro


GPUs:

  • NVIDIA GeForce GTX 1080
  • Intel(R) UHD Graphics 630

I believe that Computer’s specs plays a role but I would say that Roblox Studio has it’s limits and inefficiencies so theoretically you could have a Super Computer but Roblox Studio might not be able to perform above it’s limits.

The zombies are handled on the client.
On the server I have 2 parts with a humanoid welded together (Torso and Head, nothing more), the client renders the arms, legs, etc and does the animations.
the humanoid has ALL states disabled except Dead and Running(NoPhysics).
And zombies that are not moving or active are anchored, I check their velocity and then anchor and set them inactive if they aren’t doing anything and unachored when actively doing something. That’s how I did it.
3000 NPCs worked even.

Edit: Oh oops hadn’t read.
I play on a pretty decent gaming laptop.
Has about 16 GB RAM, Intel core i7 (Quad) and a Nvidea GTX 1050.

Back to humanoids.
Humanoids really aren’t that bad as some may think.
What really causes most lag is physics and unused states such as climbing (which does a lot of raycasts just to see if it can climb a ladder).
I literally only use states I actually need, I even disable the jumping state if not needed and the ragdoll state.
Only Dead and Running are enabled.

3 Likes

I have used almost the same method

I get around 35 FPS with 13 HumanoidStates Disabled and 27 FPS without any HumanoidStates Disabled for 250 NPCs moving around without Animations played while also using CollisionGroups to disable collisions between all of the NPCs, however some NPCs will Teleport if I SetNetworkOwner to nil (Server).

Although from doing some testing at 200 NPCs Disabling HumanoidStates and Collision doesn’t make any difference but does when going beyond 200+ NPCs

These are full Server-Sided NPCs nothing is done on the Client.



I have made a system without using a Humanoid nor Parts on the Server-Side and maintained 40 FPS with 400 NPCs moving and animated at once in a 512, Y, 512 space, goes back to 60 FPS when they stop moving

1 Like

That honestly is strange.
Im not entirely sure why I can run 1500 NPCs and you only 250.
it’s a experiment I did and results turned out to be better than I actually expected.

Maybe it’s so optimized because I literally use 1 single script to run 1500 NPCs?

I have a table containing all NPC.
When a NPC spawns I do

npcs[npc].mainfunction = function()
--I do my stuff here
end

--Then I do this.
spawn(npcs[npc].mainfunction)

That way I could run 1500 NPCs with 1 single script.
Take a good look at the code I posted, that’s literally the entire AI.

Just quickly looking through your code, you seem to be creating new threads for no reason at all.

Both of your delays are unneeded as they’re at the end of another thread anyways, so it wouldn’t block any code from executing. 2/3 spawns also do this. Any connection will run on another thread, unless you actually have multiple things that need to run you don’t need to create new threads in this case.

Not sure how much it would impact performance.

I guess it also kind of depends on what you do in those threads really.

I’m also using only one Script to control all of the NPCs, although they are Server-Sided.

Don’t forget that they are R15 Rigs so that’s twice the amount of Parts of a R6 Rig

I took at look at your Script several times, you are doing quite a few things differently than the place I linked but I can see that the major performance gain is from anchoring RootParts.

While using one script can be performant, I can see that even though you are using 1 script to control all of the NPCs they have their own Threads.

So again, if you want to run 10000 NPCs we’ll have to answer the question of whether or if Roblox can handle 70000 anchored parts in any given space.



How big is that space shown in the OP?

I’m going to assume that it’s bigger than 2048, Y, 2048

1 Like

When talking about ur pc specs, you should really include clock speeds for Processor and GFX Card. He has a 2.9GHz processor, which is most likely why his pc struggles. I personally won’t buy anything with less than 4GHz

Sorry forgot to inculde that.
Here is a screenshot of my processor.

https://gyazo.com/77ea749ff9c2adad885b99ad88a1d2e8

I remember watching crazyman32 do a similar approach to this before for his game but rendering only visible NPCs. I think he managed to get over 1500 NPCs at 60 FPS, but I’m not too sure on the NPC count. If I find the video I’ll link it.

So as AMD said, you could render only visible NPCs, as well as distant ones (basically any that is shown on screen). Don’t really see a use case for rendering non visible ones.

1 Like

If it isn’t processing power, what else could limit NPCs?
I personally think it’s mostly physics and such that limit NPCs.
But don’t really know what to optimize futher.
I actually want more NPCs to move at the same time.
I kind of limited the amount of NPC that are allowed to move at the same time to prevent all 1500 of them to move at the same time.

I don’t think it’s possible at this time to achieve that but until Roblox Upgrades it’s Software and Server-Hardware to be more performant and/or powerful you are stuck with it’s current limits.

We don’t have the technology yet basically, or more accurately Roblox.



Oh, alright.
3000 NPCs was the highest I’ve tested but noticed that 1500 is more stable.
The server can handle 3000 NPCs, however for patato PCs 1500 NPCs already is pretty heavy.
NPCs are rendered clientside mostly, the server only does hitboxes, targetting, movement, thinking in general, etc.

I made AI for 1500 NPCs and there is no lag at all (they are cubes)
so i think having alot of NPCs will only lag your game because of the amount of parts not because they are humanoid or AI

The two scripts that control them only have 2-3% script activity altogether.

6 Likes