Help with Tower Defense Game Migrating from MoveTo to nodes and position syncing

I’ve used to have my enemies system simply use the MoveTo function on the humanoid but after some testing I realized the game gets quite a high Recv when alot of enemies are spawned in so after some looking around I came across this post which seems to have solved the issue, I’m trying to use the code provided in it but instead this happens:

Not only that I don’t know what seems to be the issue, this has a lot of complications for example when stunning the enemy how would that work?

here’s the client code I used:

--- 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.Enemies.Models.Test:Clone()
	cloneClientSide.Name = ClientTableEnemyInfo["EnemyId"]
	cloneClientSide.Parent = game.Workspace.Enemies
	cloneClientSide:PivotTo(CFrameWithOrientation)
end)

function HandleMovement()
	for i,Enemy in pairs(tableOfEnemies) do
		local clientTarget = game.Workspace.Enemies[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

and this is the server code:

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, #workspace.Map.Waypoints:GetChildren(), 1 do
		Points[i] = workspace.Map.Waypoints[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)
			--local visual_dot = Instance.new("Part")
			--visual_dot.Size = Vector3.new(1,1,1)
			--visual_dot.CanCollide = false
			--visual_dot.Anchored = true
			--visual_dot.Parent = game.Workspace.Nodes
			--visual_dot.Position = p
			--visual_dot.Name = counter
		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"] = 0.1;
		["Path"] = math.random(1,100);
		["CFrame"] = CFrame.lookAt(workspace.Map.Start.Spawn.Position, workspace.Map.Waypoints["1"].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;

wait(5)

task.spawn(function()
	for _ = 1, enemyCount do
		task.wait(0.25) -- Spawn delay between each unit
		SpawnNPC()
	end
end)

while true do
	task.wait(0.01)
	HandleMovement()
end
2 Likes

Hello! All you need to do is change the way your CFrame is done in the SpawnNPC function in your server script. I’m not exactly sure as to why it breaks, but it might be due to the spawn point not existing in the DrawPath() function. It just breaks when you use your spawn point as the beginning lookAt(). All you would need to do is to move the spawn point to the Waypoints folder and name it to 1. Also, change the code to what is below.

["CFrame"] = CFrame.lookAt(workspace.Map.Start.Spawn.Position, workspace.Map.Waypoints["1"].Position); -- doesnt work

["CFrame"] = CFrame.lookAt(workspace.Map.Waypoints["1"].Position, workspace.Map.Waypoints["2"].Position); -- Fix

For stun, I would make sure the enemy has a stun effect in the TableOfEnemies. When you turn stun to true, it will stop it on the server, then you will send to the players the position, id, and stun time length of the enemy.

2 Likes

It did seem to fix it however out of the 50 enemies it seems to spawn 1-4 ones doing that weird moving away issue, could it be my setup is whats causing this?


that’s how my waypoints are made, could it be the way the waypoints are facing is the reason for this?

2 Likes

It shouldn’t be due to the way they are facing, as it is using the position of the two points to make the direction. Can you send a picture of the way the paths are made in explorer or send a rbxl file of the system? I’m not sure why it’s still happening, as I’m not experiencing this issue at all.

2 Likes

hi! sorry for the late reply, for some reason when i do the system on a new place it works fine but when testing in the full game alongside the other scripts it causes the weird issue, i cant seem to be able to narrow it down, could there be a way to check if one enemy is moving out of bound and cancel that particular enemy perhaps?

2 Likes

That is extremely strange. Do your scripts interact with the client-sided parts in any way? What I would do first is to disable each script at a time to verify what script is causing the weird flying effect you’re experiencing.

Maybe you can check if the enemy is getting too far from a node, and if it is, remove it. I would personally rather try to find the root cause of the issue instead.

2 Likes

I assume a local script is what’s causing the issue since everything is happening in the client right?

2 Likes

I would say so, if you want to verify that, you can replicate it on the server side as well to see if its happening server or client.

2 Likes

even with all scripts disabled it cant seem to help with the issue which is quite frustrating to me. maybe its a problem that happens only on that specific place or it just triggers randomly, could I perhaps make the server continuously tween some blocks to the target and using a local script i can give these blocks a character welded to them, playing the animation and all? or would that also be intensive on the Recv?

2 Likes

You could tween them, but it would cause unnecessary stress on the server, causing lag and higher recv. It may not be a problem if you do not want a lot of enemies at a time. But, if I’m not wrong, you said that it works perfectly fine on a different game?

I would just transfer everything to the other place and update the old place from there. Hopefully, it will work from then on. I think there may be a setting or something in the game messing with it.

2 Likes

Sadly it looks quite messy no matter what i do, i might have to write my own system for it and use yours as a reference for usage. Thank you still, i really appreciate your help and ill keep this topic updated with my progress if youre interested

1 Like

Yeah, I can understand that it was messy lol. If you need any help still let me know.

1 Like

I ended up writing my own movement system relying on pure data from the server, in this example I’m visualizing the movement from the server using lerp but for some reason the speed feels inconsistent, any ideas how to fix that? here’s the code I made:

local CframeTable = {}
local enemies = {}
local HS = game:GetService("HttpService")
local speedBoost = 200
local function ReturnVector3(offset, i, start, target)
	
	local posA = start.Position
	local posB = target.Position

	local deltaX = posA.X - posB.X
	local deltaY = posA.Y - posB.Y
	local deltaZ = posA.Z - posB.Z
	
	if math.abs(deltaX) > math.abs(deltaY) and math.abs(deltaX) > math.abs(deltaZ) then
		if deltaX > 0 then
			return Vector3.new(-offset*i,0,0)
		else
			return Vector3.new(offset*i,0,0)
		end
	elseif math.abs(deltaY) > math.abs(deltaX) and math.abs(deltaY) > math.abs(deltaZ) then
		if deltaY > 0 then
			return Vector3.new(0,-offset*i,0)
		else
			return Vector3.new(0,offset*i,0)
		end
	else
		if deltaZ > 0 then
			return Vector3.new(0,0,-offset*i)
		else
			return Vector3.new(0,0,offset*i)
		end
	end
	
end

for e, v in ipairs(workspace.Map.Waypoints:GetChildren()) do
	if e ~= #workspace.Map.Waypoints:GetChildren() then
	local start = workspace.Map.Waypoints[e]
	local target = workspace.Map.Waypoints[e + 1]
	local magintude = (start.Position - target.Position).Magnitude
	local offset = magintude/10
	local max = 100
	local currentOffset = 100
	local i = 1
	--[[table.insert(CframeTable, workspace.Map.Waypoints[e].CFrame)]]
	while currentOffset > 1 and i ~= max do
		local cframe = CFrame.lookAt(start.Position,
				target.Position
			) + ReturnVector3(offset, i, start, target)

		currentOffset = (cframe.Position - target.Position).Magnitude
		i+=1
		local lastindex: CFrame = CframeTable[#CframeTable - 1]
		if lastindex then
			local mag = (cframe.Position - lastindex.Position).Magnitude
			if mag >= 0.5 then
				table.insert(CframeTable, cframe)
			end
		else
			table.insert(CframeTable, cframe)
		end
	end
	--[[table.insert(CframeTable, workspace.Map.Waypoints[e + 1].CFrame)]]
	end
end
print(CframeTable)
for index = 1, #CframeTable do
	local new = Instance.new("Part", workspace.Nodes)
	new.Name = index; new.Size = Vector3.new(1,1,1);
	new.Transparency = 0.8
	new.Anchored = true
	new.CFrame = CframeTable[index]
	wait()
end


local function spawnEnemy()

	local newEnemy = {
		["Id"] = HS:GenerateGUID(false):gsub('-', ""),
		["Node"] = 1,
		["Speed"] = 1,
		["CFrame"] = CframeTable[1],
		["CurrentCFrame"] = CframeTable[1],
		['Stunned'] = false,
		["Done"] = false
	}
	
	task.spawn(function()
		local indicator = Instance.new("Part", workspace)
		indicator.Name = "Indicator";
		indicator.Size = Vector3.new(1,1,1);
		indicator.CFrame = CframeTable[newEnemy.Node]
		indicator.Color = Color3.new(1,0,0)
		indicator.Anchored = true
		local attempt = 1
		while newEnemy.Node < #workspace.Nodes:GetChildren() and attempt <= 200 do
			local Cframe: CFrame = newEnemy.CurrentCFrame
			local distance: number = (Cframe.Position - CframeTable[newEnemy.Node + 1].Position).Magnitude
			
			for i = 0, 1, 0.1 do
				local newCframe = Cframe:Lerp(CframeTable[newEnemy.Node + 1], i)
				task.wait(newEnemy.Speed/(distance+speedBoost))
				newEnemy.CurrentCFrame = newCframe
				indicator.CFrame = indicator.CFrame:Lerp(newCframe, 1)
			end
			attempt += 1
			newEnemy.Node += 1
		end
		
		warn("movement completed")
	end)
end

spawnEnemy()

and here’s an example video of how it works, you might be able to notice the speed inconsistency there (please don’t mind the background music)

if you have any improvements for the system feel free to modify it

I decided to go ahead and change it up a lot.

local enemies = {}
local HS = game:GetService("HttpService")
local TweenService = game:GetService("TweenService")

-- Lowered speed cause 200 is fast
local speedBoost = 20

local Nodes = workspace.Map.Waypoints:GetChildren()

local function spawnEnemy()

	local Enemy = {
		["Id"] = HS:GenerateGUID(false):gsub('-', ""),
		["NextNode"] = 2,
		["Speed"] = 1,
		["CFrame"] = workspace.Map.Waypoints[1].CFrame,
		["CurrentCFrame"] = workspace.Map.Waypoints[1].CFrame,
		['Stunned'] = false,
		['MovementTween'] = nil,
		["Done"] = false
	}
	
	local EnemyObj = Instance.new("Part")
	EnemyObj.Name = Enemy.Id
	EnemyObj.Size = Vector3.one
	EnemyObj.CFrame = Enemy.CurrentCFrame
	EnemyObj.Color = Color3.fromRGB(255,0,0)
	EnemyObj.Anchored = true
	EnemyObj.Parent = workspace.Enemies
	
	Enemy["Obj"] = EnemyObj
	
	table.insert(enemies, Enemy)
	local EnemyIndex = table.insert(enemies, Enemy)
	
	task.spawn(function()
		local HasReachedEnd = false
		
		while not HasReachedEnd do
			local NextNode = workspace.Map.Waypoints:FindFirstChild(Enemy.NextNode)
			
			if NextNode then
				local Distance = (Enemy.Obj.Position - NextNode.Position).Magnitude
				
				local ReachedNextNode = false
				
				-- Mvement for enemy
				local Movement = TweenService:Create(EnemyObj, TweenInfo.new((Distance / Enemy.Speed) / speedBoost, Enum.EasingStyle.Linear, Enum.EasingDirection.Out), {Position = NextNode.Position})
				Enemy.MovementTween = Movement
				
				Movement:Play()
				Movement.Completed:Wait()
				
				-- Sets up for next loop
				Enemy.NextNode += 1
			else
				HasReachedEnd = true
				
				EnemyObj:Destroy()
				table.remove(enemies, EnemyIndex)
				
				-- do base damage or what ever you want
			end
		end
	end)
end

while true do
	spawnEnemy()
	
	task.wait(1)
end

I would suggest moving it to how it was originally with the client doing the visual work to put less stress and lag on the server. But as of now being strictly server-sided, it is probably fine and easier to manage for now.

hi! thank you for replying back to my post and yes i did go with it being client sided like you said, the code you made makes it server sided which; after many and i mean many tests I figured out its not a viable approach to do this. I went back to using client-sided and here’s my updated code:

server:

local CframeTable = {}
local enemies = {}
local globalcount = 0
local HS = game:GetService("HttpService")
local speedBoost = 50
local msgpack = require(game.ReplicatedStorage.UtilityModules.MsgPack)
local function ReturnVector3(offset, i, start, target)
	
	local posA = start.Position
	local posB = target.Position

	local deltaX = posA.X - posB.X
	local deltaY = posA.Y - posB.Y
	local deltaZ = posA.Z - posB.Z
	
	if math.abs(deltaX) > math.abs(deltaY) and math.abs(deltaX) > math.abs(deltaZ) then
		if deltaX > 0 then
			return Vector3.new(-offset*i,0,0)
		else
			return Vector3.new(offset*i,0,0)
		end
	elseif math.abs(deltaY) > math.abs(deltaX) and math.abs(deltaY) > math.abs(deltaZ) then
		if deltaY > 0 then
			return Vector3.new(0,-offset*i,0)
		else
			return Vector3.new(0,offset*i,0)
		end
	else
		if deltaZ > 0 then
			return Vector3.new(0,0,-offset*i)
		else
			return Vector3.new(0,0,offset*i)
		end
	end
	
end

for e, v in ipairs(workspace.Map.Waypoints:GetChildren()) do
	if e ~= #workspace.Map.Waypoints:GetChildren() then
	local start = workspace.Map.Waypoints[e]
	local target = workspace.Map.Waypoints[e + 1]
	local magintude = (start.Position - target.Position).Magnitude
	local offset = magintude/10
	local max = 100
	local currentOffset = 100
	local i = 1
	--[[table.insert(CframeTable, workspace.Map.Waypoints[e].CFrame)]]
	while currentOffset > 1 and i ~= max do
		local cframe = CFrame.lookAt(start.Position,
				target.Position
			) + ReturnVector3(offset, i, start, target)

		currentOffset = (cframe.Position - target.Position).Magnitude
		i+=1
		local lastindex: CFrame = CframeTable[#CframeTable]
		if lastindex then
			local mag = (cframe.Position - lastindex.Position).Magnitude
			if mag >= 2 then
				table.insert(CframeTable, cframe)
			end
		else
			table.insert(CframeTable, cframe)
		end
	end
	--[[table.insert(CframeTable, workspace.Map.Waypoints[e + 1].CFrame)]]
	end
end
print(CframeTable)
for index = 1, #CframeTable do
	local new = Instance.new("Part", workspace.Nodes)
	new.Name = index; new.Size = Vector3.new(1,1,1);
	new.Transparency = 0.8
	new.Anchored = true
	new.CFrame = CframeTable[index]
	task.wait()
end


local function spawnEnemy()

	local newEnemy = {
		["Id"] = tostring(globalcount + 1),
		["Node"] = 1,
		["Speed"] = 1,
		["CurrentCFrame"] = CframeTable[1],
		['Stunned'] = 0,
		["Done"] = 0
	}
	
	globalcount += 1
	
	task.spawn(function()
		--local indicator = Instance.new("Part", workspace)
		--indicator.Name = "Indicator";
		--indicator.Size = Vector3.new(1,1,1);
		--indicator.CFrame = CframeTable[newEnemy.Node]
		--indicator.Color = Color3.new(1,0,0)
		--indicator.Anchored = true
		local attempt = 1
		enemies[newEnemy.Id] = newEnemy
		while newEnemy.Node < #workspace.Nodes:GetChildren() and attempt <= 200 do
			local Cframe: CFrame = newEnemy.CurrentCFrame
			local distance: number = (Cframe.Position - CframeTable[newEnemy.Node + 1].Position).Magnitude
			
			for i = 0, 1, 0.1 do
				local newCframe = Cframe:Lerp(CframeTable[newEnemy.Node + 1], i)
				task.wait(newEnemy.Speed/(distance+speedBoost))
				newEnemy.CurrentCFrame = newCframe
				--indicator.CFrame = indicator.CFrame:Lerp(newCframe, 1)
				enemies[newEnemy.Id] = newEnemy
			end
			attempt += 1
			newEnemy.Node += 1
		end
		newEnemy.Done = 1
		enemies[newEnemy.Id] = newEnemy
		warn("movement completed")
	end)
	
	local cframetable = tostring(newEnemy["CurrentCFrame"]):gsub(" ", ""):split(",")
	local enemyData = {
		[1] = newEnemy["Id"],
		[2] = newEnemy["Node"],
		[3] = newEnemy["Speed"],
		[4] = { --cframe
			[1] = cframetable[1],
			[2] = cframetable[2],
			[3] = cframetable[3],
			[4] = cframetable[4],
			[5] = cframetable[5],
			[6] = cframetable[6],
			[7] = cframetable[7],
			[8] = cframetable[8],
			[9] = cframetable[9],
			[10] = cframetable[10],
			[11] = cframetable[11],
			[12] = cframetable[12],
		},
	}
	local encoded = msgpack.encode(enemyData)
	
	game.ReplicatedStorage.SpawnedEnemy:FireAllClients(encoded)
end

wait(5)

for i=1, 8, 1 do
	print(i)
	spawnEnemy()
	wait(0.1)
end


while true do
	wait(1)
	local clientData = {}
	for e,v in pairs(enemies) do
		local postiontable = tostring(v["CurrentCFrame"].Position):gsub(" ", ""):split(",")
		local orientationTable = tostring(v["CurrentCFrame"].Rotation):gsub(" ", ""):split(",")
		local enemyData = {
			[1] = v["Id"],
			[2] = v["Node"],
			[3] = v["Speed"],
			[4] = { --cframe
				[1] = postiontable[1], -- x
				[2] = postiontable[3], -- z
				[3] = orientationTable[2], -- y
			},
			[5] = v["Stunned"],
			[6] = v["Done"],
		}
		if v.Done ~= 1 then
			table.insert(clientData, enemyData)
		end
	end
	local encoded = msgpack.encode(clientData)
	game.ReplicatedStorage.UpdatePositions:FireAllClients(encoded)
end

client:

local msgpack = require(game.ReplicatedStorage.UtilityModules.MsgPack)
local tableOfEnemies = {}

function Decompile(EnemyData)
	return {
		["Id"] = EnemyData[1],
		["Node"] = EnemyData[2],
		["Speed"] = EnemyData[3],
		["CFrame"] = CFrame.new(EnemyData[4][1],EnemyData[4][2],EnemyData[4][3],EnemyData[4][4],EnemyData[4][5],EnemyData[4][6],EnemyData[4][7],EnemyData[4][8],EnemyData[4][9],EnemyData[4][10],EnemyData[4][11],EnemyData[4][12]),
		["Stunned"] = false,
		["Done"] = false,
	}
end

function UpdateDecompile(EnemyData)
	local y = tableOfEnemies[EnemyData[1]] and tableOfEnemies[EnemyData[1]].CFrame.Position.Y or 0
	
	return {
		["Id"] = EnemyData[1],
		["Node"] = EnemyData[2],
		["Speed"] = EnemyData[3],
		["CFrame"] = CFrame.new(EnemyData[4][1], y, EnemyData[4][2]) * CFrame.fromOrientation(0, EnemyData[4][3], 0),
		["Stunned"] = EnemyData[5] == 1 and true or false,
		["Done"] = EnemyData[6] == 1 and true or false,
	}
end

game.ReplicatedStorage.SpawnedEnemy.OnClientEvent:Connect(function(EnemyInfo)
	local Decoded = msgpack.decode(EnemyInfo)
	local ClientTableEnemyInfo = Decompile(Decoded)
	local key = ClientTableEnemyInfo["Id"]

	tableOfEnemies[key] = ClientTableEnemyInfo
	local clientEnemy = Instance.new("Part")
	clientEnemy.Color = Color3.new(0,0,1)
	clientEnemy.CFrame = ClientTableEnemyInfo.CFrame
	clientEnemy.Anchored = true
	clientEnemy.Size = Vector3.new(1,1,1)
	clientEnemy.Name = key
	clientEnemy.Parent = workspace.Enemies
end)

game.ReplicatedStorage.UpdatePositions.OnClientEvent:Connect(function(Enemies)
	local Decoded = msgpack.decode(Enemies)
	local list = {}
	for e,v in ipairs(Decoded) do
		local ClientTableEnemyInfo = UpdateDecompile(v)
		local key = ClientTableEnemyInfo["Id"]
		table.insert(list, key)
		tableOfEnemies[key] = ClientTableEnemyInfo
		if workspace.Enemies:FindFirstChild(key) then
			workspace.Enemies:FindFirstChild(key).CFrame = ClientTableEnemyInfo.CFrame
		else
			local clientEnemy = Instance.new("Part")
			clientEnemy.Color = Color3.new(0,0,1)
			clientEnemy.CFrame = ClientTableEnemyInfo.CFrame
			clientEnemy.Anchored = true
			clientEnemy.Size = Vector3.new(1,1,1)
			clientEnemy.Name = key
			clientEnemy.Parent = workspace.Enemies
		end
	end
	for e,v in ipairs(workspace.Enemies:GetChildren()) do
		if not table.find(list, v.Name) then
			v:Destroy()
		end
	end
end)

the client part isn’t done yet but im working on it. as for the speedBoost variable its just so my testing isnt super slow especially with how long the path in my test place is.
with this current system I’m getting about 0.4 kb/s Recv per 8 enemies! (~0.33 is reserved by the server so the client doesnt disconnect which is why I excluded it from my calculations)
Screenshot 2024-04-25 065318

any ideas how to make the task.wait() wait an even time when dealing with the nodes though? cause currently the task.wait() i have does the job very good but its literally in reverse. the higher the enemy speed the slower they would be, this isthe only thing that’s been bothering me

task.wait(newEnemy.Speed/(distance+speedBoost))

Just one thing, is there any reason to use the way you make the paths? What I sent above just does the same (sorta of) thing but is more simplified albeit without client-side support.

Edit: It also allows for angled nodes and doesn’t need all the extra math involved.

For the second question, are you doing this by updates? Updating should be an even amount of time, like 0.1 or something, I would put this equation here.

(Distance / Enemy.Speed) / speedBoost

This is what I used for the tween above as it makes the speed not change due to different distances.

Thanks for pointing it out for me, the reason I did this way was to simplify it for my basic understanding of math and having more control over it with each point looking directly at the end direction while also having the CFrame so I can tween with it on the client and after testing around I noticed it indeed didn’t allow for angled positions, I’ve already updated my code to enable angled no matter where the points are and it’s pretty good, take a look!

for e, v in ipairs(workspace.Map.Waypoints:GetChildren()) do
		if e ~= #workspace.Map.Waypoints:GetChildren() then
			local start = workspace.Map.Waypoints[e]
			local target = workspace.Map.Waypoints[e + 1]
			local magnitude = (start.Position - target.Position).Magnitude
			local offset = magnitude / 10
			local max = 100
			local currentOffset = 100
			local i = 1

			while currentOffset > 1 and i ~= max do
				local direction = (target.Position - start.Position).Unit
				local cframe = CFrame.lookAt(start.Position, target.Position) + (direction * (offset*i))
				local distanceFromLastNode = CframeTable[#CframeTable] ~= nil and (cframe.Position - CframeTable[#CframeTable].Position).Magnitude or math.huge
				currentOffset = (cframe.Position - target.Position).Magnitude
				i = i + 1

				if #CframeTable == 0 or distanceFromLastNode >= 2 then
					table.insert(CframeTable, cframe)
				end
			end
		end
	end

        for index = 1, #CframeTable do
		local new = Instance.new("Part", workspace.Nodes)
		new.Name = index; new.Size = Vector3.new(1,1,1);
		new.Transparency = 0.8
		new.Anchored = true
		new.CFrame = CframeTable[index]
		task.wait()
	end

and yes i’m doing it with updates every 0.5 second or 1 second. I don’t want to make it very frequent to reduce the Recv as much as possible. I even went as far as to strip out values from CFrame and serialize any data sent to the client to reduce the size of the request. everything has been replaced by numbers and true/false has been replaced with 1/0 accordingly and the results are pretty impressive so far

good job! Just something to point out is that maybe you can smoothly lerp it on the client to make it smoother instead of being choppy. It may not be the best solution, but it works.

Hopefully, that is everything, let me know if you need anything else!

1 Like

one last thing which is this

the lerping seems to slow down at the end, the delay between each node and beginning another node is quite obvious. is there any way i can fix this? also after so much testing this is the best i ended up with to sync the client and server.

server: (when spawning enemy)

		connection = RS.Heartbeat:Connect(function(delta)
			if newEnemy.Node >= #workspace.Nodes:GetChildren() or not CframeTable[newEnemy.Node + 1] then
				newEnemy.Done = 1
				enemies[newEnemy.Id] = newEnemy
				warn("movement completed")
				connection:Disconnect()
				return
			end
			
			local Cframe: CFrame = newEnemy.CurrentCFrame
			local distance: number = (Cframe.Position - CframeTable[newEnemy.Node + 1].Position).Magnitude
			local duration = (distance / newEnemy.Speed) * 25
			local lerpedCFrame = Cframe:Lerp(CframeTable[newEnemy.Node + 1], alpha)
			newEnemy.CurrentCFrame = lerpedCFrame
			if EnableServerVisulization then
				indicator.CFrame = lerpedCFrame
			end
			enemies[newEnemy.Id] = newEnemy
			alpha += delta / duration
			if alpha >= 1 then
				newEnemy.Node += 1
				alpha = 0
			end
		end)

client: (when getting the enemySpawned event)

local connection = nil
		local alpha = 0
		connection = game:GetService("RunService").Heartbeat:Connect(function(delta)
			if EnemyInfo.Node >= #workspace.Nodes:GetChildren() or not workspace.Nodes[EnemyInfo.Node + 1] then
				warn("movement completed")
				connection:Disconnect()
				return
			end

			local Cframe: CFrame = ClientModel.PrimaryPart.CFrame
			local distance: number = (Cframe.Position - workspace.Nodes[EnemyInfo.Node + 1].Position).Magnitude
			local duration = (distance / EnemyInfo.Speed) * 25
			local lerpedCFrame = Cframe:Lerp(workspace.Nodes[EnemyInfo.Node + 1].CFrame, alpha)
			ClientModel.PrimaryPart.CFrame = lerpedCFrame
			alpha += delta / duration
			if alpha >= 1 then
				EnemyInfo.Node += 1
				alpha = 0
			end
		end)

the update event which fires each 1 second basically sets the client model’s Cframe to the one sent by the server so i didnt include it here (i can if you want)

do you have any idea how to fix this cause im quite exhausted from testing back and forth