Tower Defense Enemy Movement

Hey there!

So i am currently working on a Tower Defense Game. I am making the enemy movement and it really just doesn’t work. There are multiple bugs, when they go around a corner, they just go crazy and do weird stuff.

There is no errors and i don’t see the problem in my code, that’s why i’m here to get help.

The way my Enemy System Works:
So we got the Main Script calling functions from a module script to spawn the enemies.
Here we got the Function for spawning the Enemies:

local possibleEntityIds = {}

for index = 1, 1000, 1 do
	table.insert(possibleEntityIds, index)
end

function EntityService:GenerateEntityId()
	local random = math.random(1, #possibleEntityIds)
	
	local id = possibleEntityIds[random]
	
	table.remove(possibleEntityIds, random)
	
	return id
end

function EntityService:CreateEnemy(enemyName, position)
	local Id = EntityService:GenerateEntityId()
	
	local enemyData = {
		
		["Id"] = Id,
		["Enemy"] = enemyName,
		["Position"] = position,
		["Health"] = EnemyData.MainData[enemyName].Health,
		["Rotating"] = false,
		["Reverse"] = false,
		["Path"] = 1,
		["NodeProgress"] = 0
		
	}
	
	table.insert(EnemyData.CurrentEnemies, enemyData)
	
	spawn(function()
		while true do
			task.wait()
			
			if table.find(EnemyData.CurrentEnemies, enemyData) == nil then
				table.insert(possibleEntityIds, Id)
				
				break
			end
		end
	end)
end

Alright, so we got a Module Script containing all the enemy data. It contains the Enemy Data for each enemy, also the Currently Spawned Enemies.
Here the Module Script is:

local enemyData = {}

enemyData.CurrentEnemies = {}

enemyData.MainData = {
	
	["Flower"] = {
		
		["Health"] = 5,
		["Speed"] = 3,
		["Camoflage"] = false,
		["HipHeight"] = 1.75,
		["Damage"] = 3,
		["Reward"] = 5,
		["Animations"] = {
			["Walk"] = "rbxassetid://7327516095",
			["Death"] = "rbxassetid://7327594298"
		}
		
	},
	
	["Fast Rock"] = {
		
		["Health"] = 5,
		["Speed"] = 10,
		["Camoflage"] = false,
		["HipHeight"] = 1.75,
		["Damage"] = 2,
		["Reward"] = 5,
		["Animations"] = {
			["Walk"] = "rbxassetid://7573965164",
			["Death"] = "rbxassetid://7574044604"
		}
		
	},
	
}

return enemyData

So whenever a enemy spawns, it gets added to the module current enemies table. So the movement for all the enemies is handled by a Server Script, the rendering is on the client tho.
Here is the Enemy Handler Server Script:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local Modules = ReplicatedStorage:WaitForChild("Modules")
local Remotes = ReplicatedStorage:WaitForChild("Remotes")

local Events = Remotes:WaitForChild("Events")
local Functions = Remotes:WaitForChild("Functions")

local EnemyData = require(Modules:WaitForChild("EnemyData"))

local EnemyEventThreads = {}

RunService.Heartbeat:Connect(function()
	for index, enemyData in pairs(EnemyData.CurrentEnemies) do
		spawn(function()
			local CurrentNode = workspace.Map.Paths[enemyData.Path].Node
			local NextNode = nil
			local PreviousNode = nil
			
			if tonumber(enemyData.Path) == #workspace.Map.Paths:GetChildren() - 1 then
				NextNode = workspace.Map.Paths.End
			else
				NextNode = workspace.Map.Paths[tostring(tonumber(enemyData.Path) + 1)].Node
			end
			
			if tonumber(enemyData.Path) > 1 then
				PreviousNode = workspace.Map.Paths[tostring(tonumber(enemyData.Path) - 1)].Node
			end
			
			local LerpPosition = nil
			
			if enemyData.Reverse then
				if enemyData.NodeProgress > 0 then
					if enemyData.NodeProgress - ((0.01 / (CurrentNode.Position - NextNode.Position).magnitude) * EnemyData.MainData[enemyData.Enemy].Speed) < 0 then
						LerpPosition = CurrentNode.Position:Lerp(NextNode.Position, 1)
						
						enemyData.NodeProgress = 0
					else
						LerpPosition = CurrentNode.Position:Lerp(NextNode.Position, enemyData.NodeProgress - ((0.01 / (CurrentNode.Position - NextNode.Position).magnitude) * EnemyData.MainData[enemyData.Enemy].Speed)) + Vector3.new(0, EnemyData.MainData[enemyData.Enemy].HipHeight, 0)
						
						enemyData.NodeProgress = enemyData.NodeProgress - ((0.01 / (CurrentNode.Position - NextNode.Position).magnitude) * EnemyData.MainData[enemyData.Enemy].Speed)
					end
				else
					LerpPosition = CurrentNode.Position:Lerp(NextNode.Position, 0)
					
					enemyData.NodeProgress = 0
				end
			else
				if enemyData.NodeProgress < 1 then
					if enemyData.NodeProgress + ((0.01 / (CurrentNode.Position - NextNode.Position).magnitude) * EnemyData.MainData[enemyData.Enemy].Speed) > 1 then
						LerpPosition = CurrentNode.Position:Lerp(NextNode.Position, 1)
						
						enemyData.NodeProgress = 1
					else
						LerpPosition = CurrentNode.Position:Lerp(NextNode.Position, enemyData.NodeProgress + ((0.01 / (CurrentNode.Position - NextNode.Position).magnitude) * EnemyData.MainData[enemyData.Enemy].Speed)) + Vector3.new(0, EnemyData.MainData[enemyData.Enemy].HipHeight, 0)
						
						enemyData.NodeProgress = enemyData.NodeProgress + ((0.01 / (CurrentNode.Position - NextNode.Position).magnitude) * EnemyData.MainData[enemyData.Enemy].Speed)
					end
				else
					LerpPosition = CurrentNode.Position:Lerp(NextNode.Position, 1)
					
					enemyData.NodeProgress = 1
				end
			end
			
			--enemyData.Position = Vector3int16.new(math.floor(50 * enemyData.Position.X + (.5 * EnemyData.MainData[enemyData.Enemy].Speed)), math.floor(50 * enemyData.Position.Y + (.5 * EnemyData.MainData[enemyData.Enemy].Speed)), math.floor(50 * enemyData.Position.Z + (.5 * EnemyData.MainData[enemyData.Enemy].Speed)))
			enemyData.Position = LerpPosition
			
			if enemyData.Reverse then
				if enemyData.NodeProgress == 0 then
					if enemyData.Path > 1 then
						enemyData.Rotating = true
						
						spawn(function()
							task.wait(2 / EnemyData.MainData[enemyData.Enemy].Speed)
							
							enemyData.Rotating = false
						end)
						
						enemyData.Path -= 1
						enemyData.NodeProgress = 1
					else
						table.remove(EnemyData.CurrentEnemies, index)
					end
				end
			else
				if enemyData.NodeProgress == 1 then
					if enemyData.Path < #workspace.Map.Paths:GetChildren() - 1 then
						enemyData.Rotating = true
						
						spawn(function()
							task.wait(2 / EnemyData.MainData[enemyData.Enemy].Speed)
							
							enemyData.Rotating = false
						end)
						
						enemyData.Path += 1
						enemyData.NodeProgress = 0
					else
						table.remove(EnemyData.CurrentEnemies, index)
					end
				end
			end
			
			if EnemyEventThreads[enemyData.Id] == nil then
				table.insert(EnemyEventThreads, enemyData.Id)
				
				local clientSendData = enemyData
				
				task.wait(0.1)
				
				Events.EnemyData:FireAllClients(enemyData)
				
				table.remove(EnemyEventThreads, table.find(EnemyEventThreads, enemyData.Id))
			end
		end)
	end
end)

Also here’s the map in the Explorer:
image

So we got a Local Script rendering everything on the Client. It is located in the StarterPlayerScripts.
Here it is:

local TweenService = game:GetService("TweenService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PhysicsService = game:GetService("PhysicsService")

local Remotes = ReplicatedStorage:WaitForChild("Remotes")

local Events = Remotes:WaitForChild("Events")
local Functions = Remotes:WaitForChild("Functions")

local Modules = ReplicatedStorage:WaitForChild("Modules")
local Enemies = ReplicatedStorage:WaitForChild("Enemies")

local EnemyData = require(Modules:WaitForChild("EnemyData"))

local CurrentEnemies = {} 

local function GetOrientationOfCFrame(Cframe)
	local Orientation = Vector3.new(math.floor(math.deg(Vector3.new(Cframe:ToOrientation()).X)), math.floor(math.deg(Vector3.new(Cframe:ToOrientation()).Y)), math.floor(math.deg(Vector3.new(Cframe:ToOrientation()).Z)))
	
	return Orientation
end

Events:WaitForChild("EnemyData").OnClientEvent:Connect(function(enemyData)
	local CurrentNode = workspace.Map.Paths[enemyData.Path].Node
	local NextNode = nil
	
	if tonumber(enemyData.Path) == #workspace.Map.Paths:GetChildren() - 1 then
		NextNode = workspace.Map.Paths.End
	else
		NextNode = workspace.Map.Paths[tostring(tonumber(enemyData.Path) + 1)].Node
	end
	
	local EnemyPosition = CFrame.lookAt(enemyData.Position, NextNode.Position + Vector3.new(0, EnemyData.MainData[enemyData.Enemy].HipHeight, 0))
	
	if workspace.Map.EnemiesFolder:FindFirstChild(tostring(enemyData.Id)) then
		local EnemyModel = workspace.Map.EnemiesFolder[tostring(enemyData.Id)]
		
		if enemyData.Rotating then
			TweenService:Create(EnemyModel.HumanoidRootPart, TweenInfo.new(0.1, Enum.EasingStyle.Linear, Enum.EasingDirection.In, 0, false, 0), {Position = EnemyPosition.Position}):Play()
			TweenService:Create(EnemyModel.HumanoidRootPart, TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false, 0), {Orientation = GetOrientationOfCFrame(EnemyPosition)}):Play()
		else
			TweenService:Create(EnemyModel.HumanoidRootPart, TweenInfo.new(0.1, Enum.EasingStyle.Linear, Enum.EasingDirection.In, 0, false, 0), {CFrame = EnemyPosition}):Play()
		end
	else
		local EnemyModel = Enemies:FindFirstChild(enemyData.Enemy):Clone()
		EnemyModel.Name = tostring(enemyData.Id)
		EnemyModel:SetPrimaryPartCFrame(EnemyPosition)
		EnemyModel.Parent = workspace.Map.EnemiesFolder
		
		for index, object in pairs(EnemyModel:GetDescendants()) do
			if object:IsA("BasePart") then
				PhysicsService:SetPartCollisionGroup(object, "Entity")
			end
		end
	end
end)

Don’t mind the UI, The Map or even the Enemies, it is all just for testing and will definitely be deleted

Here is a Video of what happens when i play the game:

Also the Performance is so bad, it literally uses 1.6GB Memory and the Received is also at like 85KB with just 5 Enemies bro. Pls help what did i do wrong?

Sorry for not explaining all the scripts, go through them and hopefully you will understand what i am trying to do.

I have tried to get help on Discord Servers but didn’t get much help, hopefully i will here.

Every response is absolutely appreciated!

2 Likes

i dont know about the script since im a animator but maybe check if the mobs get destroyed at lets say the 1st waypoint. that would make sense why there would be no error because if you accidentally set the destroy to the 1st waypoint that could be the problem. about the crazy thing when they go around corners i suggest making everything in your mob cancollide off, perhaps even make a collision group to fix it :smiley: i hope i can help

Maybe research? How to make a Tower Defense Game - #1 Path Navigation - YouTube
It’s a whole series start to end of how to make a ttd. Maybe look at that and next time do your research

Thanks for trying to help, tho i am looking for a lot more advanced method that is a lot better, not the extremely basic td game.

Thank you alot for trying to help, especially since you’re an Animator and don’t know much about scripting. I’ll check the place they get removed again, might be the fix for the disappearing. I have already made my mobs cancollide off and they got their own collision group, so still confused about that.

None the less, thank you.