Tower Defense Custom Enemy Movement

hello!

i’m working on a tower defense game, and i just finished writing a custom NPC handler.


what you’re seeing right now is 50 npcs being rendered and lerped on the client.
NPCs can only move when their position changes and if they’re in view of the camera.
there are no instances on the server; everything is pure data.

right now i’m trying to make it so npcs can move through these nodes (ascending to descending). however, i encountered a problem:

what the NPC is supposed to do is to first spawn on that red part and then traverses through the white parts (nodes). instead, it spawns and moves to the first node and completely stops.

(all the provided code is server-side)

here is my custom MoveTo function:

function Enemy:MoveTo(finishPosition, speed)
	
	local duration = (finishPosition - self.Properties.Position).Magnitude / speed
	local durationMultiplier = (1 / duration)
	
	while ((finishPosition - self.Properties.Position).Magnitude > 0)  do

		local alpha = (duration * durationMultiplier)
		self.Properties.Position = self.Properties.Position:Lerp(finishPosition, alpha)
                
        --we set this to true to let the server know the enemy is moving
        --and replicate the enemy's data to all clients

		self.States.PositionChanged = true

		task.wait()
	end
end

and here is how i handle the nodes and enemy movement:

local speed = self.Properties.EnemyStats.WanderSpeed
local nodes = {}
	
for _, node in ipairs(workspace.Nodes:GetChildren()) do
	nodes[#nodes + 1] = node
end
	
table.sort(nodes, function(nodeA, nodeB)
	local nodeNumberA = string.match(nodeA.Name, "%d+")
	local nodeNumberB = string.match(nodeB.Name, "%d+")
		
	return tonumber(nodeNumberA) < tonumber(nodeNumberB)
end)
	
task.spawn(function()
	for _, node in ipairs(nodes) do

		local nodePosition = V3(node.Position.X, 0, node.Position.Z)

		local distance = (self.Properties.Position - nodePosition).Magnitude
		local duration = (distance / speed)

		self:MoveTo(nodePosition, speed)
		task.wait(duration)
	end
end)

thanks in advance.

2 Likes

What is going on with your duration calculations?? You calculate duration, then divide 1 by it, then times that product by the original sum which will always return 1.

I’m not sure if thats the issue with why your code is not running, its ambiguous as to what task.wait is, but if you’re calling task.wait() inside the task itself, its looks like the code is waiting on itself to finish.

Failing that, spam prints to see where its failing. It seems to me like the code is yielding before the next node

after some quick edits, it works for the most part.

however, i want the movement to be instantaneous. when i tried to implement it, this happens:

based on what i could gather from the above clip (and a bunch of tests), i think the enemy “silently” moves through the nodes until the server tracks its position using the self.States.PositionChanged, and replicates the enemy’s position to all clients.

here is my updated code:


--from my custom MoveTo() function
local distance = (finishPosition - self.Properties.Position).Magnitude
local duration = (distance / speed)
duration /= 1 * duration
	
while ((finishPosition - self.Properties.Position).Magnitude > 0) do
	self.Properties.Position = self.Properties.Position:Lerp(finishPosition, duration)
	self:SetState("PositionChanged", true)
		
	task.wait()
end
	
if self.States.PositionChanged then
   self:SetState("PositionChanged", false)
end


--main code
local speed = self.Properties.EnemyStats.WanderSpeed

self.States.IsWandering = true
	
local nodes = {}
	
for _, node in ipairs(workspace.Nodes:GetChildren()) do
	nodes[#nodes + 1] = node
end
	
table.sort(nodes, function(nodeA, nodeB)
	local nodeNumberA = string.match(nodeA.Name, "%d+")
	local nodeNumberB = string.match(nodeB.Name, "%d+")
		
	return tonumber(nodeNumberA) < tonumber(nodeNumberB)
end)
	
	
for _, node in ipairs(nodes) do
		
	local nodePosition = V3(node.Position.X, 0, node.Position.Z)
		
	local distance = (self.Properties.Position - nodePosition).Magnitude
	local duration = (distance / speed)
	duration /= 1 * duration
		
	--print(node)
	--print(duration)
		
	self:MoveTo(nodePosition, speed)
end

bump, any help is greatly appreciated.

I don’t know how exactly you’re replicating the enemy position to client, the only thing I can think of at the moment is that you shouldn’t set PositionChanged to false in MoveTo function after while but after for loop because you want enemy to move without stopping.
Like this:

--from my custom MoveTo() function
local distance = (finishPosition - self.Properties.Position).Magnitude
local duration = (distance / speed)
duration /= 1 * duration
	
while ((finishPosition - self.Properties.Position).Magnitude > 0) do
	self.Properties.Position = self.Properties.Position:Lerp(finishPosition, duration)
	self:SetState("PositionChanged", true)
		
	task.wait()
end

--main code
local speed = self.Properties.EnemyStats.WanderSpeed

self.States.IsWandering = true
	
local nodes = {}
	
for _, node in ipairs(workspace.Nodes:GetChildren()) do
	nodes[#nodes + 1] = node
end
	
table.sort(nodes, function(nodeA, nodeB)
	local nodeNumberA = string.match(nodeA.Name, "%d+")
	local nodeNumberB = string.match(nodeB.Name, "%d+")
		
	return tonumber(nodeNumberA) < tonumber(nodeNumberB)
end)
	
	
for _, node in ipairs(nodes) do
		
	local nodePosition = V3(node.Position.X, 0, node.Position.Z)
		
	local distance = (self.Properties.Position - nodePosition).Magnitude
	local duration = (distance / speed)
	duration /= 1 * duration
		
	--print(node)
	--print(duration)
		
	self:MoveTo(nodePosition, speed)
end

if self.States.PositionChanged then
   self:SetState("PositionChanged", false)
end