Enemy path system not working

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?