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!