Need help in making enemy system for my td game

Hello, I am right now trying to make an entity System for my td: game. I just came to know that MoveTo and pathfindingService are very bad methods for handling movement.

I tried looking for solutions in devorum, but they are too complicated for my small brain to handle

The code below is something i tried, and in the main script I make new enemies with the enemy.new and give the info as needed. I also give the table of mobs with all the info to the client to tween the positions.

I dont know how the server will detect the client’s enemies, Any solutions?

Module script:

local enemy = {}

enemy.__index = enemy

local map = workspace["Crash Test Zone"]
local ps = game:GetService("PhysicsService")

local cs = game:GetService("CollectionService")

local takenIDS = {}

local count = 0

function enemy:Die()
	self.HP = 0
end

function enemy.new(info)
	local newEnemy = {}
	
	newEnemy.Name = info["Name"]
	newEnemy.Speed = info["Speed"]
	newEnemy.ID = info["ID"]
	newEnemy.CFrame = CFrame.new()
	newEnemy.CurrentWaypoint = 1
	newEnemy.Health = info["Health"]
	newEnemy.MaxHealth = info["Health"]
	newEnemy.State = "Moving"
	newEnemy.Instance = game.ReplicatedStorage.Enemies:FindFirstChild(info["Name"])
	
	setmetatable(newEnemy, enemy)
	
	return newEnemy
end

return enemy

Summoning enemies script:

local enemy = {}

local mobs = require(script.Parent.Mob)

local takenIDS = {}
local runService = game:GetService("RunService")

local count = 0

local map = game.Workspace["Crash Test Zone"]

local mobss =  {}

function generateCode()
	local code = math.random(1, 1000000)
	-- now we check if it exists
	if table.find(takenIDS, code) then
		task.wait()
		generateCode() -- call the function again
	else
		table.insert(takenIDS, code)
		return code
	end
end

function enemy.WalkToWayPoint(thing, timeEachCooldown, position)
	if thing.State == "Dead" then return end
	if thing.CurrentWaypoint + 1 > #map.Waypoints:GetChildren() then return end

	local nextWaypoint = map.Waypoints[thing.CurrentWaypoint + 1]

	thing.CFrame = thing.CFrame.lookVector * (thing.Speed * timeEachCooldown)
	if (position - nextWaypoint.Position).Magnitude < 1 then
		thing.CFrame = CFrame.new(position, nextWaypoint.Position)
		thing.CurrentWaypoint += 1
	end
end

function enemy.spawnEnemy(enemy, amount)
	enemy = string.lower(enemy)
	local mob = game.ReplicatedStorage.Enemies:FindFirstChild(enemy)
	
	local code = generateCode()
	local info = {
		["Name"] = enemy,
		["Speed"] = mob:GetAttribute("Speed"),
		["Health"] = mob:GetAttribute("Health"),
		["ID"] = code
	}
	for i = 1, amount do
		local new = mobs.new(info)
		new.CFrame = map.Waypoints:FindFirstChild("1").CFrame
		new.CFrame = new.CFrame.Position
		table.insert(mobss, new)
	end
end

runService.Heartbeat:Connect(function()
	count += 1
	if count >= 10 then
		count = 0
		for i,v in pairs(mobss) do
			if v.CFrame then
				local dist = (v.CFrame - map.Waypoints[v.CurrentWaypoint].Position).Magnitude
				enemy.WalkToWayPoint(v, dist/v.Speed, v.CFrame)
			else
				continue
			end	
		end
		
		game.ReplicatedStorage.Events.Render:FireAllClients(mobss)
	end
end)

return enemy

Client:

local ts = game:GetService("TweenService")

function check(mob)
	for i,v in pairs(workspace.Enemies:GetChildren()) do
		if v:GetAttribute("ID") == mob.ID then
			return false
		else
			continue
		end
	end
	return true
end

game.ReplicatedStorage.Events.Render.OnClientEvent:Connect(function(enemy)
	for i,v in pairs(enemy) do
		if check(v) then
			local clone = v.Instance:Clone()
			clone:SetAttribute("ID", v.ID)
			clone.Parent = workspace.Enemies
			clone:WaitForChild("HumanoidRootPart").CFrame = workspace["Crash Test Zone"].Waypoints[1].CFrame
			
			v.Instance = clone
		end				
		local clone = v.Instance
		
		local goal = {}
		goal.Position = workspace["Crash Test Zone"].Waypoints[v.CurrentWaypoint].Position
		
		local distance = (clone:WaitForChild("HumanoidRootPart").Position - workspace["Crash Test Zone"].Waypoints[v.CurrentWaypoint].Position).Magnitude
		local times = distance/clone:GetAttribute("Speed")
		
		ts:Create(clone:WaitForChild("HumanoidRootPart"), TweenInfo.new(times), goal)
	end	
	
	return enemy
end)

Please don’t make fun of my small brain code.

6 Likes

It’s interesting how you generate ids, why bother getting a random number when you can just acquire a unique id by incrementing a id variable every time an entity is created?

4 Likes

True tho, I will i guess make your idea

5 Likes

Also, in your client side you are looping over all entities to get an entity with a matching id. A better and efficient approach would be to just store the entity with its id as the index in a dictionary named EntityMap so you can easily get the entity from the dictionary by doing EntityMap[enemy.ID).

4 Likes

Thats actually a very good idea, Thank you for that. But I will still need some info on connecting the server and client together and all

3 Likes

Also this is the post i want you to like make it simple for me to understand, I am kinda of new to scripting:

3 Likes

Well, the best approach is something like this afaik

  • Server creates entity with unique id and fires a remote event to client signaling that a new entity has been created
  • Client registers the entity in EntityMap and creates a clone in workspace
  • Server runs movement calculations every 1/10th of second and sends the movement data of all entities to client.
  • Client interpolates their client side entities to the resulting cframe
5 Likes

The entity map should be in the client or the server?

Also shouldn’t we calculate all the things in the client to reduce the server bandwidth, just saying

2 Likes

The system will become more exploitable if calculations are done on the client. The client only handles the visuals, the logic part is done by the server.

Client. You can have it in the server too if you want.

3 Likes

Alright thank you I might ask further questions if I reach home and try to do it but I will mark your answer as a solution, tysm

1 Like

Also I want to save the exact position of the enemy for the towers to detect in server, any ideas?

I currently use nodes and cframe

I didn’t get it…wdym

OK so the server will do calculations and the client gets the info and tweens, but that doesn’t exactly say where the enemy is currently (eg: he might be 1/4 the way to the way point), if you see in the info there’s only cframe for every waypoint unless I am mistaken

It’s for the towers to know where the enemy is since they are done by server

i’m not well versed with this topic, so the info I provide henceforth may not be the best

Ig you could get an approximate from the CFrame of the enemy table, but I need more info on how you want to handle towers.

Also your calculations seem weird???

function enemy.WalkToWayPoint(thing, timeEachCooldown, position)
	if thing.State == "Dead" then return end
	if thing.CurrentWaypoint + 1 > #map.Waypoints:GetChildren() then return end

	local nextWaypoint = map.Waypoints[thing.CurrentWaypoint + 1]

	thing.CFrame = thing.CFrame.lookVector * (thing.Speed * timeEachCooldown) -- won't this error cuz you are setting CFrame to Vector3???
	if (position - nextWaypoint.Position).Magnitude < 1 then
		thing.CFrame = CFrame.new(position, nextWaypoint.Position)
		thing.CurrentWaypoint += 1
	end
end

Basically I will have a module script connecting to the main script where all enemies spawn and get passed to summon script, the module script towers will handle placing and shooting, shooting is infinite until the tower isn’t there and it’s handled by the server as you can see

I kinda copied that part since I didn’t know what to do to calculate it

I thought it gets the exact position but when I read it I think it’s only waypoint