Tower Defense Enemy Movement System Glitchy

  1. What do you want to achieve? I want to achieve a smooth movement system where my enemy units can move forward towards the nodes on a dedicated path using a runservice system which updates their positions every tick.

  2. What is the issue? I’m working on an enemy movement system for a tower defense of mine for pure education and for some reason the current one I’m working on is slightly bugged and it has these position stutters which I will explain in the images I’ll attach below.

  3. What solutions have you tried so far?: I tried looking around for examples, different systems, and I decided to stick with the runservice heartbeat position updating, and then I decided to potentially combine it with tweenservice to rotate the parts when turning at the nodes instead of instantly clipping. I’m currently out of ideas really on what to do. Someone recommended I tried setting network ownership to nil but my parts are anchored and I can’t go around it.

Here’s the script for the movement:

game:GetService("RunService").Heartbeat:Connect(function(deltaTime)
	for _, npc in ipairs(game.Workspace.TowerDefense.Test.Mobs:GetChildren()) do
		local distance = (game.Workspace.TowerDefense.Test.Path[npc.Settings.GoalNode.Value].Position - npc:GetPivot().Position).Magnitude
		if distance < 0.5 then -- original 0.5
			if npc.Settings.GoalNode.Value ~= #game.Workspace.TowerDefense.Test.Path:GetChildren() then
				npc.Settings.CurrentNode.Value = npc.Settings.CurrentNode.Value + 1
				npc.Settings.GoalNode.Value = npc.Settings.GoalNode.Value + 1
				npc_library.Rotate(npc)
				npc:PivotTo(CFrame.new(game.Workspace.TowerDefense.Test.Path[npc.Settings.CurrentNode.Value].Position, game.Workspace.TowerDefense.Test.Path[npc.Settings.GoalNode.Value].Position))	
			else
				print("DONE!")
				npc:Destroy()
			end 
		else
			npc:PivotTo(npc:GetPivot() + npc:GetPivot().LookVector * 20 * deltaTime) -- original 20
		end
	end
end)

Any help will be appreciated.

2 Likes

Is this any better?

game:GetService("RunService").Stepped:Connect(function(deltaTime)
	for _, npc in game.Workspace.TowerDefense.Test.Mobs:GetChildren() do
		local distance = (game.Workspace.TowerDefense.Test.Path[npc.Settings.GoalNode.Value].Position - npc:GetPivot().Position).Magnitude
		if distance < 0.5 then -- original 0.5
			if npc.Settings.GoalNode.Value ~= #game.Workspace.TowerDefense.Test.Path:GetChildren() then
				npc.Settings.CurrentNode.Value = npc.Settings.CurrentNode.Value + 1
				npc.Settings.GoalNode.Value = npc.Settings.GoalNode.Value + 1
				npc_library.Rotate(npc)
				npc:PivotTo(CFrame.new(game.Workspace.TowerDefense.Test.Path[npc.Settings.CurrentNode.Value].Position, game.Workspace.TowerDefense.Test.Path[npc.Settings.GoalNode.Value].Position))	
			else
				print("DONE!")
				npc:Destroy()
			end 
		else
			npc:PivotTo(npc:GetPivot() + npc:GetPivot().LookVector * 20 * deltaTime) -- original 20
		end
	end
end)
1 Like

Hello,

Unfortunately no, it seems to create a new event or something related to that because it speeds up the movement of the cats each time they reach a new node, after the node the speed increases until they completely fly off the map.

I’ll try and play around with the different runservice events, that I haven’t tried yet.

EDIT: Nevermind, doesn’t seem like there are any events left over to play around with. I’m wondering if maybe it has something to do with the speed increasing every time.

1 Like

Try changing position values inside the enemies on the server and tweening the enemy models on the client
Slight lag will cause choppiness with the current system

1 Like

Is there a potential workaround this? I haven’t really explored how to filter stuff between server side and client side in regards to positions and what not.

That’s true that slight lag with cause problems with the current system but wouldn’t updating the positions on the server side do the same thing even with tweening on the client side?

Maybe the problem is with my with movement system, should I try some other ideas?

It shouldn’t lag as much if done right due to constantly being slightly behind
Try doing something like this to be more efficient than tweening:

--On the client

--This should be a CFrame value
local TweenPos = Enemy:WaitForChild("ClientPosition")
--This should be a Vector3 value
local ServerPos = Enemy:WaitForChild("ServerPosition")
local GoalCFrame = ServerPos.Value --Add rotations

--Adjust alpha if it's too fast/slow
TweenPos.Value = TweenPos.Value:Lerp(GoalCFrame, 0.01)

Enemy:PivotTo(TweenPos.Value)

“-- On the client” : Meaning this is supposed to be a local script? Local player script?

And then how would I update the position? Through events every single time?

Make a Stepped or RenderStepped event “loop” that updates through a local script

But how would that go about fixing the movement system tho? The entire problem starts with how the part is moving every heartbeat.

The backend / frontend model and lag problem I’ll try and solve later, I understand that lag might be an issue here but could there be other factors that could be the reason why this system is glitching out with different spacings?

Here’s how it looks:
This is the start from how they look when they spawn:

image

And this is how they look after they move through the nodes:
image3
image2
image1

This seems to be an unrelated issue but I think the easiest solution is by tweening the positions on the server which should cause minimal pauses which are probably causing the spacing issues.

Well, I’ll try that and see if it works.

Alright, I seem to have succeeded in making a sorta client sided - like server-sided system for moving these parts with lerp to attach the part to it every single time, now.

Should I make the part on the server transparent? Because I already have the client side model which I lerp to the actual moving part all the time.

Yeah I guess.
I don’t see why you can’t just store the server position/CFrame inside a value though.
It would be more efficient in the long term and it’s probably better to do it sooner than later.
If you aren’t going to have many enemies spawned at the same time this should work just fine though.

Wait you mean I shouldn’t be storing the server position as a part? Is this not what you meant I should do?

Maybe I didn’t really understand it quiet well so let me get it straight:

I shouldn’t have a server side part at all? How would I be storing the server position part? Inside the client sided part? Would I have to first spawn the client sided part and then inside it the position values? And then update one ofthe server sided values back and forth through some sort of an event / function on the server side to update the position of the model? Is this what you meant?

No, I meant just only have the enemy spawned in and have CFrame or Vector3 value instances as children for them storing the data.

These are what I was talking about:

Oh I think I get it, so I don’t actually “move” the part on the server right? The only thing being moved is the player / client sided model that lerps through the positions that are updated on the server side right?

Basically I store the values inside the enemy model but never actually perform any animation movement and just play around with the values of it?

Yeah, that’s what I meant.
Try to do the minimum on the server sided stuff and try to do all the visuals on the client if possible.
Using value instances make it much more simple because it cuts down a bunch of the event clutter that can build up on systems like this.

Wait but, my entire code depends on pivots and lookvectors, if I don’t actually move the part on the server, how would I know where it’s pivot at and look vector is? How would the part movement calculations actually work?

For movement it’s much more efficient to just use TweenService on the position values rather than relying on lookvectors and stuff.

On the server try doing something like this for each enemy:

local TS = game:GetService("TweenService")

--On enemy spawn
task.spawn(function()
	--Assuming the goals are numbered
	for _, GoalNode in ipairs(GoalNodes:GetChildren()) do
		local Distance = (EnemyPosVal.Value - 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(EnemyPosVal, EnemyTweenInfo, {Value = GoalNode.Position})
		PosTween:Play()
		
		PosTween.Completed:Wait()
		
		if not Enemy then --Check if the enemy still exists
			break --Stop the loop if it doesn't
		end
	end
end)

Also replace EnemyPosVal with the position value.

Wait are you sure you meant on the server? Don’t you mean on the client? Isn’t the client doing all the movement? Why would I be tweening it on the server if I’ll be lerping it on the client?

Maybe I don’t know how tweening exactly works but isn’t tweening for animation purposes? Or can I use tweening for just playing with values without having to actually animate a moving model?