Client-Side Rendering Enemy Movement using AlignPosition & AlignOrientation

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! :smile:

1 Like

Bumping this. Need help with this.

1 Like