Help on enemy td path optimization

i wanted to try and create an optimized tower defense system that could spawn hundreds of enemies with little lag. I was originally using fully client-sided rendering and visualization but I realized that could probably be prone to exploits so I decided to switch to server-sided rendering, client-sided visualization, the former using the bezier module to create a curved pathway.

Tried a few different solutions like trying to make sure that the hrps and cframe tables stay the same but this usually isnā€™t very accurate and sometimes doesnā€™t even work properly. The enemies donā€™t even move sometimes either.

hereā€™s how it currently looks (unfortunately:)

server code:

local RenderEnemies = game.ReplicatedStorage:WaitForChild("RemoteEvents").RenderEnemies

local enemyDictionary = require(script.EnemyInfo)
local enemyModule = require(script.Enemies)
local BezierPath = require(script.BezierPath)

local RunService = game:GetService("RunService")
local EnemyModels = game.ReplicatedStorage:WaitForChild("Enemies")

local RenderEnemies = game.ReplicatedStorage:WaitForChild("RemoteEvents").RenderEnemies
local Waypoints = {}

Waypoints = {
	workspace.EnemyWaypoints:WaitForChild("Spawn").Position,
	workspace.EnemyWaypoints["1"].Position,
	workspace.EnemyWaypoints["2"].Position,
	workspace.EnemyWaypoints["3"].Position,
	workspace.EnemyWaypoints["4"].Position,
	workspace.EnemyWaypoints.End.Position
}

print(Waypoints)
local Path = BezierPath.new(Waypoints, 6)

local enemies = {}
enemies["CFrames"] = {}

local totalenemies = 0
Path:VisualizePath()

enemyModule.SetupEnemies()

task.wait(5)

local function createEnemy(Enemy)
	print(Enemy)
	
	if not Enemy["Offset"] then Enemy["Offset"] = 2 end
	
	--local Animation = Instance.new("Animation")
	--Animation.AnimationId = "rbxassetid://"..Info.Animation

	--local Animator = VisualEnemy.AnimationController.Animator
	--Animator:LoadAnimation(Animation):Play()

	totalenemies += 1
	local visualTable = {
		["EnemyName"] = Enemy.EnemyName,
		["ID"] = totalenemies,
		
		["Alpha"] = 0,
		["StartTime"] = workspace:GetServerTimeNow(),
		["Offset"] = Enemy.Offset,
		["Speed"] = Enemy.Speed,
		["Health"] = Enemy.Health,
	}

	table.insert(enemies, visualTable)
end

task.spawn(function()
	for i = 1, 3 do
		createEnemy(enemyDictionary["Noob"])
		task.wait(3)
	end
end)

local ticks = 0

RunService.Heartbeat:Connect(function(DeltaTime)
	ticks += 1
	if ticks < 2 then return else ticks = 0 end

	for enemyIndex, enemyData in pairs(enemies) do
		if enemyData["EnemyName"] == nil or enemyData["Health"] == nil then continue end

		local Alpha = (workspace:GetServerTimeNow() - enemyData["StartTime"]) / (Path:GetPathLength() / enemyData["Speed"])
		local enemyCFrame = Path:CalculateUniformCFrame(Alpha) * CFrame.new(0, enemyData["Offset"], 0)
		enemies["CFrames"][enemyIndex] = enemyCFrame
		enemyData["Alpha"] = Alpha
	end
	
	if #enemies > 0 and #enemies["CFrames"] > 0 then
		local originCFrames = enemies["CFrames"]
		for _, player in pairs(game.Players:GetPlayers()) do
			if not player then continue end
			enemies = RenderEnemies:InvokeClient(player, enemies, enemies["CFrames"])
		end
		enemies["CFrames"] = originCFrames
	end
end)

client-sided code (activated through a remote function)

local EnemiesFolder = game.Workspace:WaitForChild("Enemies")
local RunService = game:GetService("RunService")
local Waypoints = {}

local EnemyStorage = game.ReplicatedStorage:WaitForChild("Enemies")
local RenderEnemies = game.ReplicatedStorage:WaitForChild("RemoteEvents").RenderEnemies

local enemyHRPs = {}
local pooledEnemies = {}

RenderEnemies.OnClientInvoke = function(enemyData, enemyCFrames)
	for dataindex, enemyInfo in pairs(enemyData) do
		if not enemyInfo["EnemyName"] or not enemyInfo["Speed"] then continue end
		
		local enemyName = enemyInfo["EnemyName"].."_"..enemyInfo["ID"]
		local enemyModel = EnemiesFolder:FindFirstChild(enemyName)
		
		if enemyInfo["Alpha"] >= 1 then 
			table.insert(pooledEnemies, enemyInfo["ID"]) 
			
			print("YOU MUST DIEEE") 
			
			if enemyModel then enemyModel:Destroy() end 
			table.remove(enemyData, dataindex)
			table.remove(enemyCFrames, dataindex)
			table.remove(enemyHRPs, dataindex)
			table.remove(pooledEnemies, enemyInfo["ID"])
			
			continue 
		end
		
		-- if there's no enemy model, then create a new one
		if not enemyModel then
			enemyModel = EnemyStorage:FindFirstChild(enemyInfo["EnemyName"]):Clone()
			enemyModel.Name = enemyName
			enemyModel.Parent = EnemiesFolder
			
			enemyHRPs[dataindex] = enemyModel.HumanoidRootPart
		end
	end
	
	-- make sure that the hrps and cframes match up
	local numEnemies = #enemyHRPs
	local numCFrames = #enemyCFrames
	
	print(numEnemies, numCFrames, tostring(numEnemies - numCFrames))
	
	for index, enemyCFrame in pairs(enemyCFrames) do
		if not enemyHRPs[index] then table.remove(enemyCFrames, index) end
	end
	
	--if numEnemies < numCFrames then
	--	-- add missing elements to enemyHRPs
	--	for i = 1, numCFrames - numEnemies do
	--		local enemyModel = EnemiesFolder:FindFirstChild("Enemy_" .. i)
	--		if enemyModel then
	--			enemyHRPs[i + numEnemies] = enemyModel.HumanoidRootPart
	--		else
	--			print("Error: Couldn't find enemy model for index " .. i)
	--		end
	--	end
	--elseif numEnemies > numCFrames then
	--	-- remove extra elements from enemyCFrames
	--	for i = numEnemies + 1, #enemyHRPs do
	--		table.remove(enemyHRPs, i)
	--	end
	--end
	
	workspace:BulkMoveTo(enemyHRPs, enemyCFrames)
	return enemyData
end

(the "YOU MUST DIEE" sometimes prints several times for one enemy)

Have you tried changing the 3 in this part of the script to a 4?

task.spawn(function()
	   for i = 1, 3 do
		createEnemy(enemyDictionary["Noob"])
		task.wait(3)
	end
end)

Commonly for most numeric scripts, if the minimum is 1 and the maximum is 3, it cannot use or exceed 3, if that makes sense. Basically only 1 and 2 would work because 3 is the stopping point (1 < 3).

If that doesnā€™t do anything, it could possibly be this part of your script.

RunService.Heartbeat:Connect(function(DeltaTime)
	ticks += 1
	if ticks < 2 then return else ticks = 0 end

oh yea i forgot to mention the for 3 loop and the ticks things are intentional because

  • The for loop is just to test to make sure that 3 enemies can successfully spawn with a 3 second interval in betwen
  • And the ticks are also intentional because I want their to be enough updating for it to be accurate but it shouldnā€™t be too performance-heavy

To figure out which part of the script isnā€™t triggering 3 times, I suggest putting a print("Hello") in the client script. Possibly before or after workspace:BulkMoveTo(enemyHRPs, enemyCFrames) because Iā€™m guessing this is what makes the enemies move?

yeaaahhh for some reason some times it just decides to remove a cframe because the third one doesnā€™t happen. for some reason the cframe and hrp table just decide to remove the third oneā€¦ i have no clue whatā€™s going on

Do the scripts print out any errors? I mean it seems like the scripts should work, it could just be a Studio bug. I know for some of my games, the scripts I just put in stopped working even though they should. So I wouldnā€™t be surprised if itā€™s not the script, itā€™s Studio. And try putting a print("Hello") in the server script hereā€¦

if not player then continue end
			enemies = RenderEnemies:InvokeClient(player, enemies, enemies["CFrames"])
		end

To make sure itā€™s triggering 3 times.

And another question, do they all use the same CFrames to move or have their own paths?

basically the enemies each have their own hrp and cframe, and these two are stored in two different tables, enemyCFrames and enemyHRPs ā† (only on the client) and the enemies themselves (well, their data because the server canā€™t directly get the enemies) is stored in a table called enemies. this is what happens between the two:

Server

The server can add or remove enemies from the enemies table. Ever two ticks, the server then calculates the cframe of the each enemy in the enemies using the BezierPath module and updates them to the enemyCFrames table accordingly before sending this data to the client via RemoteFunction. Once the client is finished visualizing the enemy movement, the server recieves a new enemy list from the client (because only the client can calculate when the enemy has reached the end so it can remove them from the enemy table and also delete the actual enemy model)

Client

Whenever the server fires a certain remotefunction, the client recieves this remote function along with two arguements for the enemies (the table /w data) and enemy cframes (the table /w enemy hrp cframes) the client then makes sure that if an enemy has reached the end, remove them from the table and delete their visual model. The client then (tries) to remove any cframes that donā€™t have a hrp to go along with them (because the enemy got deleted). And finally, the client moves all the models forward using workspace:BulkMoveTo(enemyHRPs, enemyCFrames) before returning the new enemy table to the server (because an enemy couldā€™ve been deleted on the client side).

sorry for the yap fest :sweat_smile:

As the video shows, the script does print errors sometimes involving the fact that the hrp table and cframe table donā€™t match up when using workspace:BulkMoveTo(enemyHRPs, enemyCFrames). I added a print statement whenever an enemy is created and it works, itā€™s just that the 3rd enemy doesnā€™t always move for some reason (even though they are all 3 tables). The error happens whenever the enemy reaches the end as the hrp is removed from the table but the cframe isnā€™t for some reason.

So itā€™s an inconsistency issue with the script. Which video did you use so I can test it out in my own game?

wait thereā€™s supposed to be a video in the initial post. can it not be seen?

Oh sorry, I thought you meant you used a tutorial video. My apologies.

oh ok this is just to make sure thirty letter

any updates on ur testing? im also trying to try out some new things as well

EDIT: after two hours of debugging, iā€™ve figured out that whatā€™s causing the error once an error reaches the end is that it only removes from the hrptable and not the cframetableā€¦ will research further

1 Like