I heard that there is a method called stereming, it was a word like that, I don’t know exactly, it is in the humanoid section and you can adjust it.
I heard that enabling it will make the game much faster.
And try to use task.wait in all your loops so that the game doesn’t lag.
Try not to use things that consist of too many parts because they cause a lot of lag. If you can create them in Blender and then add them, I think it can be more helpful.
If you follow these tips, your game will be great.
If your making a zombie game what you can try to learn is server simulation and client replication. Essentially humanoids are VERY expensive because they are constantly replicating to clients meaning sending lots of data such as CFrames, positions, humanoid states, etc.
I recommend simulating “parts” that follow the zombie’s behavior movement ON the server and have that data (like position and orientation) sent to the players (clients) to replicate visually.
I am currently working on a similar system with smart NPCs and have utilized this method.
Additionally:
I recommend to learning bit packing and using network buffers if you do decide to do server simulation as it significantly decreases the amount of data being sent to the client. From me for example before I implemented this 2 NPCs were taking up over 517 bytes sending info like positions constantly, after packing that data into bits, 1 NPC only consumes around 5 bytes for me.
Optimization is key if you want to handle loads of NPC without lag.
exclude cframe and position, cause these properties are replicated of every instance from server
Not unless you’re simulating on a server the NPC behavior where CFrame and Positions are not replicated. Other things like animations, orientation, etc. anything to do with the NPC is not replicated while using server simulation.
The main benefit of server simulation is you can control what gets replicated to the client so essential things like positions and CFrame orientations can be bitpacked and sent to the client to be replicated (I’ve done this and it only cost me 5 bytes!), behavior logic and such can be relayed onto the client using behavior trees as well by bitpacking certain numbers:
idle = 1
walking = 2
running = 3
and so on…
The main reason handling a lot of NPC cause lag is the humanoid replicating humanoid states, health, position, cframe, and more constantly each frame. Using server simulation controls the rate and allows you to only send necessary data that the client needs to know, eg. positional data (x,y,z).
I’ve been able to bitpack positional data into 32-bit (4 bytes) and orientation into a 8 bit (1 byte) [1+4 = 5 bytes per npc]. However, OP you might need more or less bytes for position depending on the scale of the game as the way I packed my bytes is the X and Z value can range from 0 to 2054ish smth, and the Y value to 256.
Below is a tutorial going over bitpacking, etc.
Simulating thousands of moving NPCs with humanoids/physics performantly
u didnt get my point obviously, what i meant was Humanoids dont have CFrame NOR position, these are properties of basepart, not humanoid
sorry for being late, I’ve found the DOD manager i’ve written long ago, its very simple, includes replication batch, entity ID batching and syncing.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Remotes = ReplicatedStorage.Remotes
local ReplicateRemote = Remotes.DataReplicate
local IDCache = {}
local Updates, Count = {}, 0
local DataManager = {}
--// Base \\
DataManager.ID = 0
DataManager.Active = {} :: {[number]: boolean}
--// World \\
DataManager.WorldName = nil
DataManager.WorldWidth = nil
DataManager.WorldHeight = nil
DataManager.Tile = {} :: { number }
DataManager.ForegroundTile = {} :: { number } --// [GlobalIndex]: TileId
DataManager.ForegroundId = {} :: { number }
--// Player \\
DataManager.Username = {} :: { string }
DataManager.UserId = {} :: { number }
DataManager.PlayerPosition = {} :: { vector }
local Enums = {
GenerateID = 1,
Track = 2,
Set = 3,
Destroy = 4
}
function DataManager.GenerateID(id: number)
local ID = id
if not ID then
if #IDCache > 0 then
ID = table.remove(IDCache)
else
DataManager.ID += 1
ID = DataManager.ID
end
end
Count += 1
Updates[Count] = {Enums.GenerateID, id}
return ID
end
function DataManager.Track(id: number)
DataManager.Active[id] = true
Count += 1
Updates[Count] = {Enums.Track, id}
end
function DataManager.Set(...)
if select("#", ...) == 3 then
local Id, Field, Value = ...
if not DataManager[Field] then
return
end
DataManager[Field][Id] = Value
else
local Field, Value = ...
DataManager[Field] = Value
end
Count += 1
Updates[Count] = {Enums.Set, ...}
end
function DataManager.Destroy(id: number)
if not DataManager.Active[id] then
return
end
DataManager.Active[id] = nil
for Key, Field in next, DataManager do
if typeof(Field) == "table" and Field[id] then
Field[id] = nil
end
end
table.insert(IDCache, id)
Count += 1
Updates[Count] = {Enums.Destroy, id}
end
function DataManager.Sync(plr: Player)
local FreshData = {}
for Key, Field in next, DataManager do
if typeof(DataManager[Key]) == "function" then
continue
end
FreshData[Key] = Field
end
ReplicateRemote:FireClient(plr, "Init", FreshData)
end
if RunService:IsServer() then
local function Heartbeat()
if #Updates == 0 then
return
end
local BatchSize = math.min(#Updates, 5000)
local Batch = {}
for Index = 1, BatchSize do
Batch[Index] = Updates[Index]
end
if #Updates > BatchSize then
table.move(Updates, BatchSize + 1, #Updates, 1, Updates)
for Index = #Updates, #Updates - BatchSize + 1, -1 do
Updates[Index] = nil
Count -= 1
end
else
Updates = {}
Count = 0
end
ReplicateRemote:FireAllClients("Update", Batch)
end
RunService.Heartbeat:Connect(Heartbeat)
else
local function OnClientEvent(Action: string, ...)
if Action == "Update" then
local Batch = ...
for _, Update in next, Batch do
local MethodEnum = Update[1]
local Method
for MethodName, Index in next, Enums do
if Index == MethodEnum then
Method = DataManager[MethodName]
break
end
end
if Method then
Method(table.unpack(Update, 2))
end
end
elseif Action == "Init" then
local Data = ...
for Key, Field in next, Data do
if typeof(DataManager[Key]) == "function" then
continue
end
DataManager[Key] = Field
end
end
Updates = {}
Count = 0
end
ReplicateRemote.OnClientEvent:Connect(OnClientEvent)
game:GetService("UserInputService").InputBegan:Connect(function(inp, gpe)
if gpe then return end
if inp.KeyCode == Enum.KeyCode.F then
print(DataManager)
end
end)
end
return DataManager
and this is minimal example
local function LoadWorld(worldName: string)
local Result, WorldData = GetWorldData(worldName)
if Result ~= "Success" or not WorldData then
warn("Failed to load World")
return
end
DataManager.Set("WorldName", WorldData.name)
DataManager.Set("WorldWidth", WorldData.width)
DataManager.Set("WorldHeight", WorldData.height)
for _, Block: WorldTypes.Block in next, WorldData.blocks do
local GlobalIndex = Block.y * DataManager.WorldWidth + Block.x
local BlockId = DataManager.GenerateID()
DataManager.Track(BlockId)
DataManager.Set(BlockId, "Tile", true)
DataManager.Set(BlockId, "ForegroundId", Block.fg)
DataManager.Set(GlobalIndex, "ForegroundTile", BlockId)
end
warn(`Loaded world ({WorldData.name})`)
end
even if there were thousands of WorldData.blocks, it would batch without any lag
these are “components”, you can create seperate module for it and implement it within this manager if you want to
