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)