Hey I was wondering I could get help on optimizing my entity system
This system works by the client rendering the enemies and the server moving them, it gets around 1300 enemies before fps drop starts to happen does anyone know how I can improve this?
I tried looking for post to help but didn’t find anything useful any help is appreciated
Server Side
local mob = {}
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Events = ReplicatedStorage:WaitForChild("Events")
local LastUpdate = tick()
local UpdateDT = 0.1
local ClientInfo = {}
local Positions = {}
game:GetService("RunService").Heartbeat:Connect(function(DeltaTime)
local map = workspace:WaitForChild("Map")
for number, data in pairs(Positions) do
local CurrentWaypoint = map.Waypoints:FindFirstChild(data.Waypoint)
local OldWaypoint = map.Waypoints:FindFirstChild(data.Waypoint - 1)
if not OldWaypoint then
OldWaypoint = map.Waypoints.Start
end
if not CurrentWaypoint then
map.Base.Humanoid:TakeDamage(data.SpeedHealth.Y)
ClientInfo[number] = {Dead = true}
Events.Movement:FireAllClients(ClientInfo)
Positions[number] = nil
continue
end
local distance = (OldWaypoint.Position - CurrentWaypoint.Position).Magnitude
data.Moved += DeltaTime * data.SpeedHealth.X / distance
if data.Moved >= 1 then
data.Waypoint += 1
data.Moved = 0
end
ClientInfo[number] = Vector2int16.new(math.floor(CurrentWaypoint.Position.X*50), math.floor(CurrentWaypoint.Position.Z*50))
end
if tick() - LastUpdate >= UpdateDT then
LastUpdate = tick()
Events.Movement:FireAllClients(ClientInfo)
end
end)
local num = 0
function mob.Spawn(name, quantity, dly)
local Exists = ReplicatedStorage.Enemies:FindFirstChild(name)
if Exists then
for i = 1, quantity do
num += 1
Positions[num] = {
Waypoint = 1,
SpeedHealth = Vector2int16.new(math.round(Exists.Configuration.Speed.Value), math.round(Exists.Humanoid.Health)),
Moved = 0
}
Events.Render:FireAllClients(name, num)
task.wait(dly)
end
end
end
return mob
Client side
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Events = ReplicatedStorage:WaitForChild("Events")
local LastUpdate = tick()
local ud = 0.001
local Enemies = {}
local positions = {}
local map = workspace:WaitForChild("Map")
local base = map:WaitForChild("Base")
local PS = game:GetService("PhysicsService")
Events.Movement.OnClientEvent:Connect(function(info)
positions = info
end)
game:GetService("RunService").Heartbeat:Connect(function(deltaTime)
if tick() - LastUpdate >= ud then
LastUpdate = tick()
for Enemyobject, data in pairs(Enemies) do
if Enemyobject and positions[data] then
local pos = positions[data]
if not pos.X or not pos.Y then
Enemies[Enemyobject] = nil
Enemyobject:Destroy()
continue
end
local Position = Vector3.new(pos.X/50, Enemyobject.PrimaryPart.Position.Y, pos.Y/50)
local Distance = (Enemyobject.PrimaryPart.Position - Position).Magnitude
local speed = Enemyobject.Configuration.Speed.Value
local cframe = CFrame.new(pos.X/50, Enemyobject.PrimaryPart.Position.Y, pos.Y/50)
Enemyobject:PivotTo(Enemyobject.PrimaryPart.CFrame:Lerp(cframe, deltaTime*speed/Distance))
end
end
end
end)
Events.Render.OnClientEvent:Connect(function(name, EnemyNumber)
local map = workspace:WaitForChild("Map")
local start = map:WaitForChild("Waypoints"):WaitForChild("Start")
local enemy = ReplicatedStorage.Enemies:FindFirstChild(name):Clone()
enemy.Parent = workspace.Enemies
enemy.PrimaryPart.Anchored = true
enemy.PrimaryPart.CanCollide = false
for i, v in pairs(enemy:GetChildren()) do
if v:IsA("BasePart") then
v.CollisionGroup = "Mobs"
end
end
enemy.PrimaryPart.CFrame = start.CFrame + Vector3.new(0, (enemy.PrimaryPart.Size.Y/2 - 1), 0)
Enemies[enemy] = EnemyNumber
end
Theres a lot that could be clearer here. I think it would help if you made a simple diagram on what is being sent from server to client and when / how often. That will help you identify what is consuming all the performance. Make sure to identify all the different loops that are running on each, I count three but I could be wrong. You could also use the profiler to measure how much time each part of the code is using, it is a very powerful tool, but what it tells you will be more helpful if you make a diagram first. That way you know what can be changed.
I tried using BulkMoveTo in the past I couldn’t figure out how to get it smooth and working correctly.
Do you mind showing me an example of how I could use GetServerTimeNow() sorry I’m not very familiar with these.
For BulkMoveTo you can just keep using the BulkMoveTo function every heartbeat to make it smooth with no lag
An example would be:
local Parts = {workspace.Part0, workspace.Part1}
local CFrameList = {part0CFrame, part1CFrame}
workspace:BulkMoveTo(Parts, CFrameList, Enum.BulkMoveMode.FireCFrameChanged)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Events = ReplicatedStorage:WaitForChild("Events")
local LastUpdate = tick()
local ud = 0.001
local Enemies = {}
local positions = {}
local map = workspace:WaitForChild("Map")
local base = map:WaitForChild("Base")
local PS = game:GetService("PhysicsService")
Events.Movement.OnClientEvent:Connect(function(info)
positions = info
end)
local startTime = workspace:GetServerTimeNow()
game:GetService("RunService").Heartbeat:Connect(function()
local deltaTime = (workspace:GetServerTimeNow()-startTime)
startTime = workspace:GetServerTimeNow()
if tick() - LastUpdate >= ud then
LastUpdate = tick()
for Enemyobject, data in pairs(Enemies) do
if Enemyobject and positions[data] then
local pos = positions[data]
if not pos.X or not pos.Y then
Enemies[Enemyobject] = nil
Enemyobject:Destroy()
continue
end
local Position = Vector3.new(pos.X/50, Enemyobject.PrimaryPart.Position.Y, pos.Y/50)
local Distance = (Enemyobject.PrimaryPart.Position - Position).Magnitude
local speed = Enemyobject.Configuration.Speed.Value
local cframe = CFrame.new(pos.X/50, Enemyobject.PrimaryPart.Position.Y, pos.Y/50)
Enemyobject:PivotTo(Enemyobject.PrimaryPart.CFrame:Lerp(cframe, deltaTime*speed/Distance))
end
end
end
end)
Events.Render.OnClientEvent:Connect(function(name, EnemyNumber)
local map = workspace:WaitForChild("Map")
local start = map:WaitForChild("Waypoints"):WaitForChild("Start")
local enemy = ReplicatedStorage.Enemies:FindFirstChild(name):Clone()
enemy.Parent = workspace.Enemies
enemy.PrimaryPart.Anchored = true
enemy.PrimaryPart.CanCollide = false
for i, v in pairs(enemy:GetChildren()) do
if v:IsA("BasePart") then
v.CollisionGroup = "Mobs"
end
end
enemy.PrimaryPart.CFrame = start.CFrame + Vector3.new(0, (enemy.PrimaryPart.Size.Y/2 - 1), 0)
Enemies[enemy] = EnemyNumber
end
Instead of using delta time from heartbeat you should get the servers time and just find the delta time from the server so it’s synced from the server.
I have rewritten the client-side script and my fps dropped at around 1200 enemies (150 fps with no enemies and 50 fps with 1200) I don’t know if its supposed to be better than this and that I’m doing something wrong
local mob = {}
local RunService = game:GetService("RunService")
local Mobs = {}
local waypoints = workspace.Waypoints
local start = workspace.Start
local health = game.ReplicatedStorage.Value
local bitpack = require(script.Parent.SimpleBit)
local Parts = {}
local CFrames = {}
local StartTime = workspace:GetServerTimeNow()
local function UpdatePositions()
local delta = (workspace:GetServerTimeNow()-StartTime)
StartTime = workspace:GetServerTimeNow()
for model, data in pairs(Mobs) do
local Waypoint = waypoints:FindFirstChild(data.Waypoint)
local OldWaypoint = waypoints:FindFirstChild(data.Waypoint-1) or start
local speed = data.Speed
if Waypoint and OldWaypoint then
local Id = model:GetAttribute("ID")
local Distance = (OldWaypoint.Position - Waypoint.Position).Magnitude
local cframe = OldWaypoint.CFrame:Lerp(Waypoint.CFrame, data.Moved)
data.Moved += delta*speed/Distance
if data.Moved>=1 then
data.Waypoint +=1
data.Moved = 0
end
CFrames[Id] = cframe
end
end
end
game:GetService("RunService").Heartbeat:Connect(function()
UpdatePositions()
workspace:BulkMoveTo(Parts, CFrames, Enum.BulkMoveMode.FireCFrameChanged)
end)
local id = 0
local mobs = workspace.NPCs
local a = script.Parent.Abnormal
function mob.Spawn()
for i = 1,5000 do
id +=1
local new =a:Clone()
new.Parent = mobs
new:SetAttribute("ID", id)
Mobs[new] = {
Waypoint = 1,
Speed =6,
Moved = 0
}
Parts[id] = new.PrimaryPart
task.wait(.1)
end
end
return mob
Make sure you aren’t using any humanoids. They can cause lag. Make sure can touch and canquery and cancollide is all set to false for slight performance increase. Make sure humanoidrootpart is only anchored for enemies nothing else to remove all physics. Also make sure to use profiler for the rendering part because it can help solve performance issues.
Also since it is client sided fps will probably drop a bit but ping would be very low because your enemies are being handled locally. If it was done on server then fps would probably be same but ping would be insanely high. So doing client sided is best. But still if it were handled locally with 1000s of enemies your fps should be good. I’ve tested my game with enemies handled locally on my phone and i still get around 60 fps and below 1kb of recv data with 1000s of enemies on max graphics with similar methods.
I used microprofiler and it said bulkmoveto was causing some lag and the “UpdatePositions” function as well how could I further optimize it? Is there a way to get waypoint without storing it in a table.
Run BulkMoveTo if there’s anything in the Parts & CFrames table.
Run UpdatePositions if there’s any entity in your mob table.
You can make them run under a tick rate, implement an accumulator in your Heartbeat connection to achieve this as so:
-- // Example of an accumulator
-- // You may replace the deltaTime from Heartbeat to (workspace:GetServerTime() - StartTime) if
-- // it "synchronizes" with the server if you wish.
local tickRate = 1/20 -- Make it only run 20 times a second
Heartbeat:Connect(function(deltaTime)
accumulatorDelta += deltaTime
while accumulatorDelta >= tickRate do
accumulatorDelta -= tickRate
if next(Mobs) then
--// next is similar to #Parts > 0, I use this in if statements to check
--// if a dictionary is not empty.
UpdatePositions()
end
if #Parts > 0 and #CFrames > 0 then
workspace:BulkMoveTo(Parts, CFrames, Enum.BulkMoveMode.FireCFrameChanged)
end
end
end)
You can use Parallel LUA for this, create an Actor/Parallel Thread for each group of 25 mobs though you have to use a BindableEvent/Signal module to send entities processed by the actor back to the main server script.