Hello! I’ve been working on a TD game for the past few days. The Enemy Movement System is the most significant issue I have faced in this project. I’ve struggled to make my system efficient and handle 400+ enemies at a stable 60 FPS with other methods. So, I flocked to using AlignPosition & AlignOrientation.
So, I wrote a simple movement system that creates & moves the Enemy on the server, which worked well. Of course, rendering the enemies on the server would mean a drop in Performance & insane lag, so I started recoding the system to use client-side rendering. Then I hit a roadblock. How would I render all enemies for all clients while keeping track of their positions, nodes, speed an enemy should move at, etc, on server using AlignPosition & AlignOrientation.
Here is the system I wrote for initial testing on the server. (I’m aware that the code looks crap, just lil smth I cooked up.)
--[SERVER]--
function EnemyService:SpawnEnemy(EnemyData)
local Health = EnemyData.Health
local Node = EnemyData.Node
local MovingTo = EnemyData.MovingTo
local SpawnId = EnemyData.spawn_id
local Speed = EnemyData.Speed
local ID = EnemyData.id
local YOffset = EnemyData.Y_Offset
local AnimationType = EnemyData.Animation_Type
print(EnemyData)
local EnemyModel = game.Workspace._Enemies:FindFirstChild(SpawnId)
if not EnemyModel then
local model = ReplicatedStorage.Assets.Enemies[MapName]:FindFirstChild(ID)
if not model then return end
local Enemy = model:Clone()
Enemy.Name = SpawnId
Enemy.PrimaryPart.CFrame = Nodes[Node].CFrame + Vector3.new(0, YOffset, 0)
local AP = ReplicatedStorage.AlignPosition:Clone()
local AO = ReplicatedStorage.AlignOrientation:Clone()
local Att = Instance.new('Attachment')
AP.Parent = Enemy.PrimaryPart
AO.Parent = Enemy.PrimaryPart
Att.Parent = Enemy.PrimaryPart
Enemy.Parent = game.Workspace._Enemies
local AlignPosition: AlignPosition = Enemy.PrimaryPart.AlignPosition
local AlignOrientation: AlignOrientation = Enemy.PrimaryPart.AlignOrientation
AlignOrientation.Attachment0 = Att
AlignPosition.Attachment0 = Att
AlignPosition.MaxVelocity = Speed * YOffset
task.spawn(function()
for _, o in pairs(Enemy:GetDescendants()) do
local part: BasePart = o
if part:IsA('BasePart') then
part.CollisionGroup = 'Enemies'
end
end
end)
local function PlayAnimation()
local Animator = Enemy.Humanoid:FindFirstChild('Animator') or Instance.new('Animator', Enemy.Humanoid)
local AnimType = string.split(AnimationType, '_')[2]
local AnimationRaw = ReplicatedStorage.Assets.Animations.Enemies:FindFirstChild(AnimType)
if not AnimationRaw or not AnimationRaw:IsA('Animation') then return end
local Animation: AnimationTrack = Animator:LoadAnimation(AnimationRaw)
Animation:Play()
Animation:AdjustSpeed(EnemyData.Speed / 2)
end
PlayAnimation()
print(`Created: {SpawnId}`)
return Enemy
else
return EnemyModel
end
end
function EnemyService:CreateEnemy(EnemyName)
local enemyData = Enemies[MapName][EnemyName]
if not enemyData then return end
local enemyInfo = {
['spawn_id'] = HttpService:GenerateGUID(false),
['id'] = enemyData.id,
['Name'] = enemyData.name,
['Health'] = enemyData.health,
['Node'] = 0,
['MovingTo'] = 1,
['Speed'] = enemyData.speed,
['Animation_Type'] = enemyData.animation_id,
['Y_Offset'] = enemyData.y_offset
}
table.insert(self.EnemiesInGame, enemyInfo)
end
function EnemyService:EnemyMovement()
for index, EnemyData in pairs(self.EnemiesInGame) do
local Health = EnemyData.Health
local Node = EnemyData.Node
local MovingTo = EnemyData.MovingTo
local SpawnId = EnemyData.spawn_id
local Speed = EnemyData.Speed
local ID = EnemyData.id
local YOffset = EnemyData.Y_Offset
local AnimationType = EnemyData.Animation_Type
local EnemyModel = game.Workspace._Enemies:FindFirstChild(SpawnId)
if not EnemyModel then
EnemyModel = self:SpawnEnemy(EnemyData)
end
local HRP = EnemyModel:FindFirstChild('HumanoidRootPart')
local NextNodeAvailable = Nodes:FindFirstChild(MovingTo)
if not NextNodeAvailable then
self:OnBaseReached(EnemyData, index)
continue
end
local NodePosition = Nodes[Node + 1].Position + Vector3.new(0, YOffset, 0)
HRP.AlignPosition.Position = NodePosition
HRP.AlignOrientation.CFrame = CFrame.lookAt(HRP.Position, NodePosition)
if (HRP.Position - NodePosition).Magnitude <= .1 then
print(`Reached Node: {MovingTo}`)
EnemyData.Node += 1
EnemyData.MovingTo += 1
end
end
end
RunService.Heartbeat:Connect(function()
self:EnemyMovement()
end)
I would appreciate any advice or suggestions going around starting this!