Tower Defense Game has High Recv Network Traffic Performance Issue

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.

1 Like

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.

1 Like

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

1 Like

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.

Once one single enemy unit reaches the end, they all freeze and it gives me the error below.

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.

  1. 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.
  2. 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!

5 Likes

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.

1 Like

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.

2 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.