That could potentially be one of the reasons why.
Yes, you might be right, because of how often I fire the event it does cause a lot of bad performance but I currently have no idea about how I can go around this problem.
Also:
So far the performance dropped down from around 250 KB/s to 75 KB/s. Which is really nice. But other then bitpacking I’m not sure there’s another way to improve the performance unless another function has something to do with the bad performance.
Edit: I was considering modifying the wait() time of the main code execution flow but that causes the animations to be wonky.
Sorry for not clarifying but there is no risk of having the client control the movement for their perspective. What I mean by this is that when an enemy spawns, both the server and client will make their own version of the enemy movement (all you would need to do is give them the speed, maybe let them have information of each enemy speed on a module script?). The server would still do all of the handling of enemy systems, leading to no compromises.
This is exactly what I thinking. Why not handle enemy movements on the server.
I ignored the part of you looping it because It would update each enemy, from my testing, it seems to do little performance hit (0.05% usage).
I think tweening the enemies could help with animations, not the best cause if a player lag spikes you would notice, but its better i guess.
But will the server also store the position of the enemy as well? The enemy movement is already basically handled on the server. via the
Enemy.CFrame = Enemy.CFrame:ToWorldSpace(CFrame.new(0,0,-0.1 * Enemy.Speed))
line, which constantly changes the position of it, and then fires a remote to inform the client where to move the part to.
Tweening on the client side or the server side?
The server handles the actual position of the enemy (so the enemy can stop when the path ends), and the client will control the movement, it would be much smoother and less network traffic as you won’t need to update the position each time. Only problem I see if you want the enemies to stop or slow down.
If you want to update it each time, you can use tween on the client. After getting the next position, you can tween the next position by the speed of how fast the server updates.
So this line specifically:
Enemy.CFrame = Enemy.CFrame:ToWorldSpace(CFrame.new(0,0,-0.1 * Enemy.Speed))
Will basically not exist on the server right? Or something along those lines?
Won’t I have to predict the position where the enemy will have to be to be able to sync it with the client every single time?
Maybe I don’t seem to really understand what you’re trying to say. May you please show me an example of what you mean by that? Like I get the general idea but I can’t seem to really imagine the way I would do it.
-- Server
whenEnemySpawns:Connect(function()
remoteSpawnedEnemy:FireAllClients(enemyId, metadata)
end)
while true do
task.wait(20) -- arbitrary delay
local positions = getCurrentPositionsOfEnemies()
remoteSyncPositions:FireAllClients(positions)
end
-- Client
remoteSpawnedEnemy.OnClientEvent:Connect(function(enemyId, metadata)
local enemy = createEnemy(enemyId, metadata)
addEnemyToPathInterpolator(enemy)
end)
remoteSyncPositions.OnClientEvent:Connect(function(positions)
-- re sync the positions of all enemies
end)
Do you mean something like this with predicting the position at which the client has to be?
Yes, but the server isnt predicting, the client is replicating what the server is doing. Basically, all movement code is copied to the client, and the client gets to make their own positions for each enemy, if you’re having multiple paths, you can always send which path you want the enemy to go. Also if you are worried about syncing, send a workspace.DistributedGameTime and compare the delay between server sending and client receiving.
(clienttime - servertime) -- This will get the delay between server and client, and you can multiply this when an enemy first spawns to sync
Also note, if a hacker tampers with the client side, only they can see it. It won’t affect what the server and others see because all the client is doing is trying to replicate what the server is doing when an enemy spawns.
Alright, sounds nice, I’m going to attempt to do this.
stop using “while” for endless/infinite looping, use Heartbeat/PostSimulation/RenderStepped instead for better performance. Heartbeat delta is depending server performance frame, but postsimulation (another version of heartbeat) is locked to 60 (0.01666…) and will never change if server frame is decreasing
Well… here is what I came up with…
Server Code:
wait(5)
local SyncClients = game.ReplicatedStorage:WaitForChild("SyncClients")
local SpawnedEnemy = game.ReplicatedStorage:WaitForChild("SpawnedEnemy")
--- Spline Calculations
-- example for p0 with tension
local dot = game.ServerStorage.dot
local tableOfPaths = {}
function DrawPath()
local NodesTable = {}
local tensionMain = math.random(1, 100)/100 -- math.random(90, 100)/100
local counter = 0
-- To DO: the point calculation is already in this table, just run DrawPath() for each NPC individually and then have them recalculate where to go
-- this will cause them to be scattered and different
local Points = {
}
for i=1, #game.Workspace.Points:GetChildren(), 1 do
Points[i] = game.Workspace.Points[i].Position
end
-- print("Tension is: " .. tensionMain)
local NumPoints = #Points
local LastPoint = Points[1]
for i = 1, NumPoints - 1 do
local p0 = Points[i - 1] or Points[1]
local p1 = Points[i]
local p2 = Points[i + 1]
local p3 = Points[i + 2] or Points[NumPoints]
-- print(i)
for j = 0, 1, 0.05 do
local tension = tensionMain
local t = j
local t2 = t * t
local t3 = t2 * t
local p =
(
(-tension * t + 2 * tension * t2 - tension * t3) * p0 +
(2 + (tension - 6) * t2 + (4 - tension) * t3) * p1 +
(tension * t - 2 * (tension - 3) * t2 + (tension - 4) * t3) * p2 +
(-tension * t2 + tension * t3) * p3
) * 0.5
--[[
-- EnemyPosVal Object Purposes + Visual Demonstration of Points
local visual_dot = dot:Clone()
visual_dot.Parent = game.Workspace.Nodes
visual_dot.Position = p
-- counter = counter + 1
visual_dot.Name = counter
]]
counter = counter + 1
NodesTable[counter] = CFrame.new(p)
end
end
return NodesTable
end
-- Pre-Process Generate a Bunch of Paths so we don't have to generate new ones later on
for i = 1, 100 do
tableOfPaths[i] = DrawPath()
end
-- NPC Spawning Table Putting Function
local ServerTableOfEnemies = {}
local EnemyId = "1";
function SpawnNPC()
local Precision = 10^2
local EnemyInfo = {
["EnemyId"] = EnemyId;
["Node"] = "1";
["Speed"] = 4;
["Path"] = tableOfPaths[math.random(1,100)];
["CFrame"] = CFrame.lookAt(game.Workspace.Points["1"].Position, game.Workspace.Points["2"].Position);
["Done"] = false;
}
ServerTableOfEnemies[EnemyInfo.EnemyId] = EnemyInfo;
EnemyId = EnemyId + 1;
-- print(EnemyId)
local ClientEnemyInfo = {
[1] = EnemyInfo.EnemyId;
[2] = Vector3int16.new(EnemyInfo.CFrame.X * Precision, EnemyInfo.CFrame.Y * Precision, EnemyInfo.CFrame.Z * Precision);
[3] = Vector2int16.new(0, 0);
[4] = EnemyInfo.Node; -- Node
[5] = 4; -- Speed
[6] = tableOfPaths[math.random(1,100)]; -- Path
}
SpawnedEnemy:FireAllClients(ClientEnemyInfo)
end
-- Actual Movement Handling
local ClientTableOfEnemies = {}
function HandleMovement()
for i,Enemy in pairs(ServerTableOfEnemies) do
if (Enemy.Done == true) then
-- in the future fire some event to notify clients about deleting the NPC since its dead or reached the end
-- make another check about the NPC having 0 health or less in the future
-- print("Done")
else
Enemy.CFrame = Enemy.CFrame:ToWorldSpace(CFrame.new(0,0,-0.1 * Enemy.Speed))
local magnitude = (Enemy.CFrame.Position - Enemy.Path[Enemy.Node + 1].Position).Magnitude
-- print(Enemy.Node)
if (Enemy.CFrame.Position - Enemy.Path[Enemy.Node + 1].Position).Magnitude <= 1 then
Enemy.Node += 1
if Enemy.Node == #Enemy.Path then
Enemy.Done = true
else
Enemy.CFrame = CFrame.new(Enemy.Path[Enemy.Node].Position, Enemy.Path[Enemy.Node + 1].Position)
end
end
local Precision = 10^2
local EulerAnglesThingy = Vector3.new(Enemy.CFrame:ToEulerAnglesXYZ())
local ClientEnemyInfo = {
[1] = Enemy.EnemyId; -- EnemyId
[2] = Vector3int16.new(Enemy.CFrame.Position.X * Precision, Enemy.CFrame.Position.Y * Precision, Enemy.CFrame.Position.Z * Precision); -- Position
[3] = Vector2int16.new(0, EulerAnglesThingy.Y * Precision); -- Rotation
[4] = Enemy.Node; -- Node
}
ClientTableOfEnemies[i] = ClientEnemyInfo
end
end
end
-- Main Routine
local enemyCount = 50;
task.spawn(function()
for _ = 1, enemyCount do
task.wait(0.1) -- Spawn delay between each unit
SpawnNPC()
end
end)
local count = 0
while true do
if count == 100 then
count = 0
SyncClients:FireAllClients(ClientTableOfEnemies)
else
wait(0.01)
count += 1
HandleMovement()
end
end
Client Code
--- Some client stuff
local Precision = 10^2
local tableOfEnemies = {}
game.ReplicatedStorage.SpawnedEnemy.OnClientEvent:Connect(function(ClientTableEnemyInfo)
print("i got called")
local key = ClientTableEnemyInfo[1]
print(key)
tableOfEnemies[key] = ClientTableEnemyInfo
local Precision = 10^2
local CFrameNoOrientation = CFrame.new(ClientTableEnemyInfo[2].X / Precision, ClientTableEnemyInfo[2].Y / Precision, ClientTableEnemyInfo[2].Z / Precision)
local CFrameWithOrientation = CFrameNoOrientation * CFrame.Angles(0, ClientTableEnemyInfo[3].Y / Precision, 0)
local cloneClientSide = game.ReplicatedStorage.ClientSide:Clone()
cloneClientSide.Name = ClientTableEnemyInfo[1]
cloneClientSide.Parent = game.Workspace.Mobs
cloneClientSide:PivotTo(CFrame.new(cloneClientSide.Position, CFrameWithOrientation.Position))
end)
game.ReplicatedStorage.SyncClients.OnClientEvent:Connect(function(ClientTableOfEnemies)
for i, Enemy in pairs(ClientTableOfEnemies) do
local clientTarget = game.Workspace.Mobs[Enemy[1]]
tableOfEnemies[Enemy[1]][2] = Enemy[2];
tableOfEnemies[Enemy[1]][3] = Enemy[3];
tableOfEnemies[Enemy[1]][4] = Enemy[4];
local CFrameNoOrientation = CFrame.new(Enemy[2].X / Precision, Enemy[2].Y / Precision, Enemy[2].Z / Precision)
local CFrameWithOrientation = CFrameNoOrientation * CFrame.Angles(0, Enemy[3].Y / Precision, 0)
clientTarget:PivotTo(CFrame.new(clientTarget.Position, CFrameWithOrientation.Position))
end
end)
function HandleMovement()
for i,Enemy in pairs(tableOfEnemies) do
local clientTarget = game.Workspace.Mobs[Enemy[1]]
if (Enemy[4] == #Enemy[6]) then
-- in the future fire some event to notify clients about deleting the NPC since its dead or reached the end
-- make another check about the NPC having 0 health or less in the future
-- print("Done")
else
clientTarget.CFrame = clientTarget.CFrame:ToWorldSpace(CFrame.new(0,0,-0.1 * Enemy[5]))
local magnitude = (clientTarget.CFrame.Position - Enemy[6][Enemy[4] + 1].Position).Magnitude
if (magnitude <= 1) then
Enemy[4] += 1
clientTarget.CFrame = CFrame.new(Enemy[6][Enemy[4]].Position, Enemy[6][Enemy[4] + 1].Position)
end
end
end
end
while true do
task.wait(0.01)
HandleMovement()
end
Now well… the animatronics… uh, they’re acting a little… sus lately…
Edit: I’ll replace the waits with a heartbeat later on, currently I have to figure out why this is happening.
I did quite a bit of changes in your code. But I seemed to have fixed everything and optimized it a bit. Heres how I did it.
Instead of sending the path directly each time the event was called, the client would request to the server for the list of paths as each path is keyed with a number, this would be a huge benefit as all you would need to send for the enemy info is send a single number for the path. Sending the path directly caused the network to be around 10kb~, doing it that way changes it to just under 1kb.
-- Creates the paths
for i = 1, 100 do
tableOfPaths[i] = DrawPath()
end
-- Client requests the paths
function RequestPath()
return tableOfPaths
end
game.ReplicatedStorage.RequestPaths.OnServerInvoke = RequestPath
Another thing I did was make the decompiler (just because it was hard to read).
function Decompile(EnemyData)
return {
["EnemyId"] = EnemyData[1],
["EnemyPosition"] = EnemyData[2],
["EnemyRotation"] = EnemyData[3],
["Node"] = EnemyData[4],
["Speed"] = EnemyData[5],
["Path"] = tableOfPaths[EnemyData[6]], -- obtained by just sending a number between 1-100
["SyncTime"] = workspace.DistributedGameTime - EnemyData[7], -- will get to this later
["Done"] = false, -- will get to this later
}
end
The last thing I did was fix the bug where an enemy would break everything when it gets to the end. I am unsure as to why this bug occurred, but when I recoded a few things fixed it somehow. All I really did was add Enemy.Done like in the server-side code.
function HandleMovement()
for i,Enemy in pairs(tableOfEnemies) do
local clientTarget = game.Workspace.Mobs[Enemy["EnemyId"]]
if Enemy.Done then
tableOfEnemies[i] = nil
game.Debris:AddItem(clientTarget, 0)
else
clientTarget.CFrame = clientTarget.CFrame:ToWorldSpace(CFrame.new(0, 0, -0.1 * Enemy.Speed))
local magnitude = (clientTarget.CFrame.Position - Enemy.Path[Enemy.Node + 1].Position).Magnitude
if magnitude <= 1 then
Enemy.Node += 1
if Enemy.Node == #Enemy.Path then
Enemy.Done = true
else
if Enemy.SyncTime then
clientTarget.CFrame = CFrame.new(Enemy.Path[Enemy.Node].Position, Enemy.Path[Enemy.Node + 1].Position + (Enemy.Path[Enemy.Node + 1].Position * Enemy.SyncTime))
Enemy.SyncTime = nil
else
clientTarget.CFrame = CFrame.new(Enemy.Path[Enemy.Node].Position, Enemy.Path[Enemy.Node + 1].Position)
end
end
end
end
end
end
Heres the entire code
Server Side
local SpawnedEnemy = game.ReplicatedStorage:WaitForChild("SpawnedEnemy")
--- Spline Calculations
-- example for p0 with tension
--local dot = game.ServerStorage.dot
local tableOfPaths = {}
function DrawPath()
local NodesTable = {}
local tensionMain = math.random(1, 100)/100 -- math.random(90, 100)/100
local counter = 0
-- To DO: the point calculation is already in this table, just run DrawPath() for each NPC individually and then have them recalculate where to go
-- this will cause them to be scattered and different
local Points = {
}
for i=1, #game.Workspace.Points:GetChildren(), 1 do
Points[i] = game.Workspace.Points[i].Position
end
-- print("Tension is: " .. tensionMain)
local NumPoints = #Points
local LastPoint = Points[1]
for i = 1, NumPoints - 1 do
local p0 = Points[i - 1] or Points[1]
local p1 = Points[i]
local p2 = Points[i + 1]
local p3 = Points[i + 2] or Points[NumPoints]
-- print(i)
for j = 0, 1, 0.05 do
local tension = tensionMain
local t = j
local t2 = t * t
local t3 = t2 * t
local p =
(
(-tension * t + 2 * tension * t2 - tension * t3) * p0 +
(2 + (tension - 6) * t2 + (4 - tension) * t3) * p1 +
(tension * t - 2 * (tension - 3) * t2 + (tension - 4) * t3) * p2 +
(-tension * t2 + tension * t3) * p3
) * 0.5
--[[
-- EnemyPosVal Object Purposes + Visual Demonstration of Points
local visual_dot = dot:Clone()
visual_dot.Parent = game.Workspace.Nodes
visual_dot.Position = p
-- counter = counter + 1
visual_dot.Name = counter
]]
counter = counter + 1
NodesTable[counter] = CFrame.new(p)
end
end
return NodesTable
end
-- Pre-Process Generate a Bunch of Paths so we don't have to generate new ones later on
for i = 1, 100 do
tableOfPaths[i] = DrawPath()
end
function RequestPath()
return tableOfPaths
end
game.ReplicatedStorage.RequestPaths.OnServerInvoke = RequestPath
task.wait(5)
-- NPC Spawning Table Putting Function
local ServerTableOfEnemies = {}
local EnemyId = "1";
function SpawnNPC()
local Precision = 10^2
local EnemyInfo = {
["EnemyId"] = EnemyId;
["Node"] = "1";
["Speed"] = 4;
["Path"] = math.random(1,100);
["CFrame"] = CFrame.lookAt(game.Workspace.Points["1"].Position, game.Workspace.Points["2"].Position);
["Done"] = false;
}
ServerTableOfEnemies[EnemyInfo.EnemyId] = EnemyInfo;
EnemyId = EnemyId + 1;
-- print(EnemyId)
local ClientEnemyInfo = {
[1] = EnemyInfo.EnemyId;
[2] = Vector3int16.new(EnemyInfo.CFrame.X * Precision, EnemyInfo.CFrame.Y * Precision, EnemyInfo.CFrame.Z * Precision);
[3] = Vector2int16.new(0, 0);
[4] = EnemyInfo.Node; -- Node
[5] = 4; -- Speed
[6] = EnemyInfo.Path; -- Path
[7] = workspace.DistributedGameTime, -- Time in server, used to sync
}
SpawnedEnemy:FireAllClients(ClientEnemyInfo)
end
-- Actual Movement Handling
local ClientTableOfEnemies = {}
function HandleMovement()
for i,Enemy in pairs(ServerTableOfEnemies) do
if (Enemy.Done == true) then
-- in the future fire some event to notify clients about deleting the NPC since its dead or reached the end
-- make another check about the NPC having 0 health or less in the future
-- print("Done")
else
Enemy.CFrame = Enemy.CFrame:ToWorldSpace(CFrame.new(0,0,-0.1 * Enemy.Speed))
local magnitude = (Enemy.CFrame.Position - tableOfPaths[Enemy.Path][Enemy.Node + 1].Position).Magnitude
-- print(Enemy.Node)
if (Enemy.CFrame.Position - tableOfPaths[Enemy.Path][Enemy.Node + 1].Position).Magnitude <= 1 then
Enemy.Node += 1
if Enemy.Node == #tableOfPaths[Enemy.Path] then
Enemy.Done = true
else
Enemy.CFrame = CFrame.new(tableOfPaths[Enemy.Path][Enemy.Node].Position, tableOfPaths[Enemy.Path][Enemy.Node + 1].Position)
end
end
local Precision = 10^2
local EulerAnglesThingy = Vector3.new(Enemy.CFrame:ToEulerAnglesXYZ())
local ClientEnemyInfo = {
[1] = Enemy.EnemyId; -- EnemyId
[2] = Vector3int16.new(Enemy.CFrame.Position.X * Precision, Enemy.CFrame.Position.Y * Precision, Enemy.CFrame.Position.Z * Precision); -- Position
[3] = Vector2int16.new(0, EulerAnglesThingy.Y * Precision); -- Rotation
[4] = Enemy.Node; -- Node
}
ClientTableOfEnemies[i] = ClientEnemyInfo
end
end
end
-- Main Routine
local enemyCount = 50;
task.spawn(function()
for _ = 1, enemyCount do
task.wait(0.1) -- Spawn delay between each unit
SpawnNPC()
end
end)
while true do
task.wait(0.01)
HandleMovement()
end
Client Side
--- Some client stuff
local Precision = 10^2
local tableOfPaths = game.ReplicatedStorage.RequestPaths:InvokeServer()
local tableOfEnemies = {}
function Decompile(EnemyData)
return {
["EnemyId"] = EnemyData[1],
["EnemyPosition"] = EnemyData[2],
["EnemyRotation"] = EnemyData[3],
["Node"] = EnemyData[4],
["Speed"] = EnemyData[5],
["Path"] = tableOfPaths[EnemyData[6]],
["SyncTime"] = workspace.DistributedGameTime - EnemyData[7],
["Done"] = false,
}
end
game.ReplicatedStorage.SpawnedEnemy.OnClientEvent:Connect(function(EnemyInfo)
local ClientTableEnemyInfo = Decompile(EnemyInfo)
local key = ClientTableEnemyInfo["EnemyId"]
tableOfEnemies[key] = ClientTableEnemyInfo
local Precision = 10^2
local CFrameNoOrientation = CFrame.new(ClientTableEnemyInfo["EnemyPosition"].X / Precision, ClientTableEnemyInfo["EnemyPosition"].Y / Precision, ClientTableEnemyInfo["EnemyPosition"].Z / Precision)
local CFrameWithOrientation = CFrameNoOrientation * CFrame.Angles(0, ClientTableEnemyInfo["EnemyRotation"].Y / Precision, 0)
local cloneClientSide = game.ReplicatedStorage.ClientSide:Clone()
cloneClientSide.Name = ClientTableEnemyInfo["EnemyId"]
cloneClientSide.Parent = game.Workspace.Mobs
cloneClientSide:PivotTo(CFrame.new(cloneClientSide.Position, CFrameWithOrientation.Position))
end)
function HandleMovement()
for i,Enemy in pairs(tableOfEnemies) do
local clientTarget = game.Workspace.Mobs[Enemy["EnemyId"]]
if Enemy.Done then
tableOfEnemies[i] = nil
game.Debris:AddItem(clientTarget, 0)
else
clientTarget.CFrame = clientTarget.CFrame:ToWorldSpace(CFrame.new(0, 0, -0.1 * Enemy.Speed))
local magnitude = (clientTarget.CFrame.Position - Enemy.Path[Enemy.Node + 1].Position).Magnitude
if magnitude <= 1 then
Enemy.Node += 1
if Enemy.Node == #Enemy.Path then
Enemy.Done = true
else
if Enemy.SyncTime then
clientTarget.CFrame = CFrame.new(Enemy.Path[Enemy.Node].Position, Enemy.Path[Enemy.Node + 1].Position + (Enemy.Path[Enemy.Node + 1].Position * Enemy.SyncTime))
Enemy.SyncTime = nil
else
clientTarget.CFrame = CFrame.new(Enemy.Path[Enemy.Node].Position, Enemy.Path[Enemy.Node + 1].Position)
end
end
end
end
end
end
while true do
task.wait(0.01)
HandleMovement()
end
All you would need to add is a Remote function named RequestPaths
There are a few more things that can be changed to further optimize, but at this point, it’s more of a micro-optimization.
- If you have enemy variety, you can remove speed and put it in a module script that both sides can access, that will lower it a little more.
- The final thing I think you could remove is EnemyRotation, I’m unsure if you need that if the enemy movement can be done on the client.
Hopefully, this helps!
Awesome, I’ve tested the code and it looks pretty good.
I really like the way you did the path selection from the table by storing a number and then taking it out of the table using the specific number, instead of storing the entire path inside the table, that’s really nice.
Now there’s barely any network traffic even with 1000 units.
Although I do have a couple questions.
Why did you remove the SyncClients event? Is it unnecessary? What if something goes wrong with the client movement calculation? Shouldn’t we be syncing with the client every now and then?
For example, this is completely useless now in the server side script:
local Precision = 10^2
local EulerAnglesThingy = Vector3.new(Enemy.CFrame:ToEulerAnglesXYZ())
local ClientEnemyInfo = {
[1] = Enemy.EnemyId; -- EnemyId
[2] = Vector3int16.new(Enemy.CFrame.Position.X * Precision, Enemy.CFrame.Position.Y * Precision, Enemy.CFrame.Position.Z * Precision); -- Position
[3] = Vector2int16.new(0, EulerAnglesThingy.Y * Precision); -- Rotation
[4] = Enemy.Node; -- Node
}
ClientTableOfEnemies[i] = ClientEnemyInfo
Should I remove it? Or should I still come up with some sync or is it unnecessary?
Edit: Also, yeah I decided to remove the orientation part from the table as it was completely unnecessary.
I don’t believe you need to sync clients every second.
It should be event based rather than polling every few second.
You can rely on other methods such as workspace:GetServerTimeNow() to account for the initial spawn offset as tower defence movement is pretty deterministic ( at x= 5 seconds the xyz position of enemy is at position Y). This is another optimization trick to reduce network lag.
Only if you add more complex mechanics such as enemy abilities or tower stunning will then you need to fire this event.
After all the enemies spawn, it would try to sync every second, which I think was not the best case as you would send the client a ton of data at once (which if enough enemies, could cause lag spikes for some people). I think it would be better to send the time the server made the enemy, compare it to the player time, and add the extra position to the enemy spawn position, making it synced.
This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.