Tower Defense Enemy Movement System Glitchy

In this case it’s just tweening the position value though it still won’t be perfectly smooth.
The reason you do smooth transitioning on the client is because if you did it only on the server it would be super choppy and also cut down on performance a lot.
The client is mostly only handling visuals anyway so it makes more sense to put most of the visual calculations and stuff on it.

Also if you don’t have any sort of smooth transitions on the server then the towers wouldn’t have as accurate of targeting.

Hey,

Alright so I think I got something to work, instead of working with Vector3 values I just decided to completely work with CFrame values instead because, reasons

Point is, I seem to be encountering an issue now, the “enemy part” is tweening its value from where it is to the node and once it arrives at the node it just stops counting, for some reason it doesn’t continue to the next node.

Here’s the code so far:

(Server Side Code)

wait(5)

local TS = game:GetService("TweenService")

local test = game.ReplicatedStorage:WaitForChild("Spawned")

game:GetService("RunService").Heartbeat:Connect(function(deltaTime)
	for _, npc in ipairs(game.Workspace.Mobs:GetChildren()) do
		--On enemy spawn
		task.spawn(function()
			for _, GoalNode in ipairs(game.Workspace.Nodes:GetChildren()) do
				local Distance = (npc.Settings.EnemyPosVal.Value.Position - GoalNode.Position).Magnitude
				local Speed = 2
				local EnemyTweenInfo = TweenInfo.new(Distance/Speed, Enum.EasingStyle.Linear, Enum.EasingDirection.Out, 0, false, 0)
				local PosTween = TS:Create(npc.Settings.EnemyPosVal, EnemyTweenInfo, {Value = GoalNode.CFrame})
				PosTween:Play()
				PosTween.Completed:Wait()
			end
		end)
	end
end)

function SpawnNPC(amount)
	print("Spawning " .. "NPC" .. " amount of " .. amount)
	for i=1, amount do
		task.wait(2) -- Spawn delay between each unit
		local npc = game:GetService("ServerStorage").ServerSide:Clone()
		npc.Parent = game.Workspace.Mobs
		npc:PivotTo(CFrame.new(game.Workspace.Nodes[npc.Settings.CurrentNode.Value].Position, game.Workspace.Nodes[npc.Settings.GoalNode.Value].Position))
		test:FireAllClients(npc)
		print("test")
	end
end

SpawnNPC(3)

(Client Side Code)

game:GetService("RunService").Heartbeat:Connect(function(deltaTime)
	for _, npc in ipairs(game.Workspace.Mobs:GetChildren()) do
		local TweenPos = npc.ClientSide.CFrame
		local ServerPos = npc.Settings.EnemyPosVal.Value
		
		--Adjust alpha if it's too fast/slow
		npc.ClientSide.CFrame = npc.ClientSide.CFrame:Lerp(ServerPos, 1)
	end
end)

First of all, so far I figured outthat the value starts from (0,0,0) because I’m not pasting the value of the CFrame of the enemy part into the EnemyPosVal as soon as it starts, it isnt supposed to start at 0,0,0 because I’m pivoting it to the first node and from there I want it to continue to other nodes.

Now in regards to the problem where it reaches the coordinate of the node but doesn’t continue to the next node, I don’t know why.

The reason I put “On enemy spawn” in there was because I meant for you to run everything inside of the task.spawn(function() when an enemy is spawned.
I’m surprised it didn’t break even more with the loops connected to a heartbeat.

Try this:

wait(5)

local TS = game:GetService("TweenService")

local test = game.ReplicatedStorage:WaitForChild("Spawned")

function SpawnNPC(amount)
	print("Spawning " .. "NPC" .. " amount of " .. amount)
	for i=1, amount do
		task.wait(2) -- Spawn delay between each unit
		local npc = game:GetService("ServerStorage").ServerSide:Clone()
		npc.Parent = game.Workspace.Mobs
		npc:PivotTo(CFrame.new(game.Workspace.Nodes[npc.Settings.CurrentNode.Value].Position, game.Workspace.Nodes[npc.Settings.GoalNode.Value].Position))
		test:FireAllClients(npc)
		print("test")

		--Start looping through nodes
		task.spawn(function()
			for _, GoalNode in ipairs(game.Workspace.Nodes:GetChildren()) do
				local Distance = (npc.Settings.EnemyPosVal.Value.Position - GoalNode.Position).Magnitude
				local Speed = 2
				local EnemyTweenInfo = TweenInfo.new(Distance/Speed, Enum.EasingStyle.Linear, Enum.EasingDirection.Out, 0, false, 0)
				local PosTween = TS:Create(npc.Settings.EnemyPosVal, EnemyTweenInfo, {Value = GoalNode.CFrame})
				PosTween:Play()
				PosTween.Completed:Wait()
			end
		end)
	end
end

SpawnNPC(3)

Oh, I see, wait so the HeartBeat event is basically useless in this case right? Was I just running the same function a bunch of times every frame? My bad. But yeah, it seems to working. Thank you.

I’m probably going to play around with it and try and add a bunch of checks to make sure the target is going towards the right nodes all the time and what not.

Would you recommend I add anything else or try and improve something? Any potential ideas?

For more optimization you could try converting the enemies (on the server) into tables w/ positions and stuff and just send the data to the client with an event but it might be a little difficult.

If the model is unachored, you could try setting the network owner. :SetNetworkOwner(player)

Interesting, what do you mean by converting the enemies on the server into tables? Currently the enemy is a simple part on the server, how should I exactly convert it into a table? Do you have an example by any chance?

Edit: Also, why did you use task.spawn in the script above?

By converting them into tables I meant to not clone the enemy part on the server and just store the CFrames/Positions inside of a table, spawning the enemy parts only on the client.

task.spawn makes it so that the code inside will run alongside everything else.
Basically using task.spawn is like creating a new script, it just allows the code to continue without waiting on that section of code (In this case, waiting for the node loop to end).

I see, but what do you mean exactly by storing the CFrame/Positions inside of a table? How would I know which position belongs to who in the table? By their index number?

Yeah.
Set the index to a string and instead of using table.remove use Enemies[index] = nil.
Using table.remove will cause the table’s indexing to change which would break everything but it won’t happen if you just set the table index to nil.

Also, if I wanted to rotate the part on the client side while the part is moving how exactly would I do it?

If Lerp is constantly being played: as in the following script:

game:GetService("RunService").Heartbeat:Connect(function(deltaTime)
	for _, npc in ipairs(game.Workspace.TowerDefense[MapName].Mobs:GetChildren()) do
		local TweenPos = npc.ClientSide.CFrame
		local ServerPos = npc.Settings.EnemyPosVal.Value
		
		--Adjust alpha if it's too fast/slow
		npc.ClientSide.CFrame = npc.ClientSide.CFrame:Lerp(ServerPos, 1)
	end
end)

Where would I stick the rotation of the part if I can’t stop the lerp to rotate it? Or are there other ways of doing this? Is it even possible playing multiple animations at the same time?

You just have to separate the rotation and position, lerp them separately and then make them a CFrame again.

I can’t really test this so tell me if it doesn’t work and I’ll try a possible fix I know.

game:GetService("RunService").Heartbeat:Connect(function(deltaTime)
	for _, npc in ipairs(game.Workspace.TowerDefense[MapName].Mobs:GetChildren()) do
		local TweenPos = npc.ClientSide.CFrame
		local ServerPos = npc.Settings.EnemyPosVal.Value
		
		local PosSpeed = 0.1 --(Speed * 100)% every heartbeat
		local RotSpeed = 0.04 --(Speed * 100)% every heartbeat

		local NewPos = TweenPos.Position:Lerp(ServerPos.Position, PosSpeed)
		local NewRot = TweenPos.Rotation:Lerp(ServerPos.Rotation, RotSpeed)

		npc.ClientSide.CFrame = CFrame.new(NewPos) * NewRot --CFrame with new position + new rotation
	end
end)

BTW setting the alpha (Percent) in a lerp to 1 is the same as setting the position normally.
Lerping a Vector3/CFrame sets the start value to x% of the goal value. In this case you were setting the enemy CFrame to 1 (100%) of the goal CFrame.

Lerp Examples



It seems to be working better now, in terms of the animation and moving is a lot more smooth but I don’t think they are rotating exactly.

EDIT: Yeah changing the rotation / orientation of the nodes seems to have been it.

Probably a magnitude check is required now because they are rotating to the while they are getting closer and not rotating when they are close to the part already or at the part.

image

Instead of rotating the nodes I suggest using CFrame.LookAt on the client so you don’t have to rotate everything manually.

Try replacing NewRot with this:

local NewRot = TweenPos.Rotation:Lerp(CFrame.LookAt(LastGoalNode.Value.Position, GoalNode.Value.Position).Rotation, RotSpeed)

Also GoalNode and LastGoalNode should be an ObjectValue inside the enemy

Change .Heartbeat to .RenderStepped on the client
Also try adjusting the PosSpeed to fix the early rotation issue

ObjectValue? I have it as an IntValue? What’s the difference?

If it works it works.
They should both work about the same for that if they can both hold instances for values.

Yep, that did it. They are rotating nicely towards the nodes, awesome. Thank you.

I tested the performance of the objects and it seems to be running 333 Objects at about 513 KB/s of Data, which is quite a lot I think, and like you said tables could be used for optimization.

I want to really optimize this well before I proceed further into the idea and I was wondering how can I use tables exactly?

I know you explained to me a bit above about how I could use tables but I don’t really get the idea, where would I be storing the table of enemies? In the main spawn script with the spawn function? And then just remove the index when the enemy reaches the end or gets killed by its index?

But then how is it going to work on the client side? How would the client know which lerping part belongs to who? I had some ideas like storing some int value on the client side which lets me know of their index without having to store it on the server side but I’m not too sure.

Maybe I should start off small? What should I try and do to experiment with tables before I can actually try and work with the tables in my game? Do you have any tips or sources or examples by any chance?

These are two big resources you can read to better understand tables:


Also:
For backtracking the enemies I suggest storing the enemies index into it’s data table and so the client can just use the index for tracking enemies.
This will also be good for the towers being able to know which enemy table to change the health in

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.