Tower Defense: Enemy System

Howdy,

I’m working on a tower defense game with one of my friends. The objective is the same as every other tower defense (with a twist :ohmy:)

However, there are two main issues me and my friend have noticed in the system:

  • Heavy Performance on device (Needs heavy optimization)
  • Does not stay how it should normally (photo below)

    image

Into

Is there a way to highly optimize it and keep the path “filled” look to it?

Any help is appreciated!

Code:

local workspace = game:GetService("Workspace")
local ServerStorage = game:GetService("ServerStorage")
local PhysicsService = game:GetService("PhysicsService")
local ServerScriptService = game:GetService("ServerScriptService")

local EntitiesFolder = ServerStorage:WaitForChild("Entities")
local EntityData = ServerScriptService:WaitForChild("Modules"):WaitForChild("EntityData")

local EntityService = {}

local MapName = nil

function EntityService.Move(Entity, EntityData)
	if not EntityData.humanoid then
		warn("Entity is missing Humanoid!")
		return
	end
	
	if MapName then
		local Map = workspace:FindFirstChild(MapName)
		if Map then
			local waypoints = Map:FindFirstChild("Waypoints")
			if waypoints then
				
				for i = 1, #waypoints:GetChildren() do
					local position = Map["Waypoints"][i].Position
					local spacing = EntityData.offsetSpacing/(i - 0.5)/1.35
					local targetPosition = position + Vector3.new(EntityData.offset.X * math.random(-spacing,spacing),0,0)
					
					EntityData.humanoid:MoveTo(targetPosition)
					EntityData.humanoid.MoveToFinished:Wait()
				end
				
				Entity:Destroy()
			else
				warn("Waypoints don't exist!")
				return "Waypoints don't exist!"
			end
		else
			warn("Map does not exist!")
			return "Map does not exist!"
		end
	end
end

function EntityService:Spawn(HordeData)
	for i, v in pairs(HordeData["Entities"]) do
		if EntitiesFolder:FindFirstChild(v) and EntityData:FindFirstChild(v) then
			local DataModule = require(EntityData[v])
			local Enemy = EntitiesFolder[v]:Clone()

			local Health = DataModule.Health
			local MaxHealth = DataModule.MaxHealth
			local isBoss = DataModule.Boss
			local walkSpeed = DataModule.Speed
			local isStealth = DataModule.Stealth

			print("Entity Name: " .. v)
			print("Health: " .. tostring(Health))
			print("Max Health: " .. tostring(MaxHealth))
			print("Is Boss: " .. tostring(isBoss))
			print("Walk Speed: " .. tostring(walkSpeed))
			print("Is Stealth: " .. tostring(isStealth))

			if MapName == nil then
				for _, child in pairs(workspace:GetChildren()) do
					if child:IsA("Folder") and child.Name ~= "Data" and child:FindFirstChild("Map") then
						print("Found a Map folder: " .. child.Name)
						MapName = child.Name
						break
					end
				end
			else
				print("Map name is not nil!")
			end

			Enemy.Humanoid.WalkSpeed = walkSpeed
			Enemy.Humanoid.MaxHealth = MaxHealth
			Enemy.Humanoid.Health = Enemy.Humanoid.MaxHealth

			Enemy:SetAttribute("Boss", isBoss)
			Enemy:SetAttribute("Stealth", isStealth)

			Enemy.Parent = game.Workspace.Data.Entities

			local spacing = 1.25 
			local maxOffset = 1.25
			local EntityData = {}
			EntityData.data = DataModule
			EntityData.humanoid = Enemy:WaitForChild("Humanoid")
			EntityData.offset = CFrame.new((math.random(-maxOffset * 100, maxOffset * 100) / 100) * spacing, 0, 0)
			EntityData.offsetSpacing = spacing
			EntityData.offsetMaxOffset = maxOffset

			Enemy.Humanoid.NameDisplayDistance = 0
			Enemy.Humanoid.HealthDisplayDistance = 0
			Enemy.Humanoid.DisplayName = " "
			Enemy.HumanoidRootPart.CFrame = game.Workspace[MapName].Start.CFrame * EntityData.offset-- game.Workspace.Map[map].Start.CFrame

			local success, err = pcall(function()
				for _, obj in pairs(Enemy:GetChildren()) do
					if obj:IsA("Part") or obj:IsA("MeshPart") then
						obj:SetNetworkOwner(nil)
						obj.CollisionGroup = "Entity"
					end
				end
			end)

			if success then
				print("Success!")
				
				local success, err = pcall(function()
					coroutine.wrap(EntityService.Move)(Enemy, EntityData)
				end)
				
				if success then
					print()
				else
					warn("Error:", err)
				end
				
			else
				warn("Nuh uh! Error mate:", err)
			end
			task.wait(HordeData["TimeBetween"])
		else
			print("Entity or Data not found for: " .. v)
		end
	end
end

return EntityService
5 Likes

Try disabling all of the humanoid states your enemies don’t use. That way roblox won’t be checking for, say, water terrain for each npc each frame for the swim state, for instance.

Another good optimization step would be to have all of your enemies just be invisible HRPs and have the clients clone the enemy rigs on top.

5 Likes

is this for the issue with collding?

1 Like

Is this all being handled on the server?

No, it’s to prevent roblox from running extra background checks to see whether or not an entity should have it’s state changed. Humanoids aren’t magic, for every state that is enabled (which is by default all of them) extra checks need to be done every frame.

This can noticeably slow performance down with a high enough volume of NPCs.

Yes. We plan on later (if possible) to convert it over to client side

Is there an exact way to disable this? Can’t find it in the Humanoid

Humanoid:SetStateEnabled([HUMANOID STATE ENUM], false)

That’s likely going to be your largest issue on performance. There’s a really good article
that explains how to do really good optimizations for a tower defense game here: How we reduced bandwidth usage by 60x in Astro Force (Roblox RTS)
The concepts and techniques this guy uses are really good in practice not just for tower defense games but in general.

image
Correct?

1 Like

Got it! Thanks. Do you however understand how to fix my issue with the enemies?

You should be able to disable all of them, I think roblox re-enables the core ones by default like running (unless you have some states you’re intending to use at which point you could do some micro optimizations by only enabling them as they are required)

are collisions enabled on the enemies?

That’s interesting. Definity will look into this. Never knew that existed.

No, some enemies are going to collide with each other. We have it so enemies cant collide with enemies or players.

image
is there a way to fix this?
image

You want some enemies to collide with other enemies or no enemies colliding with any enemies?

Enemies cannot collide with enemies or players.

Probably just some removed state. Just comment them out until you find the culprit(s)

To me in the first photo, it looks like the enemies are colliding with each other which is why their paths seem off