Tower defence enemies lagging behind

As you can see in the footage, the enemies lag behind and teleport/go insanely fast to where they are supposed to be. How can I fix this, because when I have lots more enemies, they move across the corners and it looks super weird. I am using :MoveTo() with humanoids:

Edit: just noticed I recorded excess footage, just skip ahead a bit.

19 Likes

Change the network ownership to the server

BasePart:SetNetworkOwner(nil)
6 Likes

That’s a lot of Humanoids and a lot of MoveTo() which is most likely the cause of your lag.

  1. Are you making sure to disable HumanoidState’s that aren’t needed?

  2. You may consider removing the Humanoids or replacing them with an Animator (see link above) and using CFrame:Lerp() to move the models as needed.

    2.1 You could remove the full character Models for mobs and replace them with a SpecialMesh with the appearance of a character, when in reality it’s just a single BasePart. Unsure if using MeshParts for this would have the same reduction in computations.


    2.2 You might also have some success using TweenService under the caveat that you only create a TweenInfo object for each unique tween and not each mob, and make sure to delete the actual Tween object after the mob despawns.

  3. Network ownership as mentioned above:

5 Likes

I already set the network ownership to server. How would I use CFrame.Lerp? I tried it but it didn’t work for me. I’ll try the other optimizations, but can I add animations to special meshes.
@Niveum

5 Likes

Could you send your code? I can try to implement some sort of tweening/lerping. By the way, humanoids are terrible for performance.

3 Likes

Sure. I am going to be busy, so I can send it to tomorrow. Thanks!

2 Likes

heres the code:

--!native
local module = {}
local ServerStorage = game:GetService("ServerStorage")
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")
local PhysicsService = game:GetService("PhysicsService")

module.spawn = function(name:string, waypointfolder:Folder, exit, spawns:CFrame, boss:BoolValue)

	local model:Model = ServerStorage.Enemies:FindFirstChild(name):Clone()
	local offset = math.random(-1,1)
	model:PivotTo(spawns + Vector3.new(offset,0,0))
	if boss then
		ReplicatedStorage.AddHealthBar:FireAllClients(name,model.Health.Value,model.Health.Value)
	end
	
	model.Parent = Workspace:FindFirstChild(ReplicatedStorage.Map.Value).enemies
	--local health = model.health
	--health:GetPropertyChangedSignal("Value"):Connect(function()
	--	if health.Value == 0 then
	--		model:Destroy()
	--	end
	--end)
	for _,part in pairs(model:GetDescendants()) do
		if part:IsA("BasePart") then
			part:SetNetworkOwner(nil)
		end
	end
	for index = 1,#waypointfolder:GetChildren(),1 do
		model.Humanoid:MoveTo(waypointfolder:FindFirstChild(tostring(index)).Position + Vector3.new(offset,0,0))
		model.Humanoid.MoveToFinished:Wait()
	end
	model.Humanoid:MoveTo(exit.Position + Vector3.new(offset,0,0))
	model.Humanoid.MoveToFinished:Wait()
	
	ReplicatedStorage.BaseHealth.Value = math.clamp(ReplicatedStorage.BaseHealth.Value - model.Health.Value,0,ReplicatedStorage.MaxBaseHealth.Value)
	model:Destroy()
	
end




return module

@Katrist

5 Likes

I’ve implemented tweening:

--!native
local module = {}
local ServerStorage = game:GetService("ServerStorage")
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")
local PhysicsService = game:GetService("PhysicsService")

module.spawn = function(name:string, waypointfolder:Folder, exit, spawns:CFrame, boss:BoolValue)
	local offset = math.random(-1,1)

	local model: Model = ServerStorage.Enemies:FindFirstChild(name):Clone()
	model:PivotTo(spawns + Vector3.new(offset,0,0))

	if boss then
		ReplicatedStorage.AddHealthBar:FireAllClients(name,model.Health.Value,model.Health.Value)
	end

	model.Parent = Workspace:FindFirstChild(ReplicatedStorage.Map.Value).enemies
	--local health = model.health
	--health:GetPropertyChangedSignal("Value"):Connect(function()
	--	if health.Value == 0 then
	--		model:Destroy()
	--	end
	--end)

	model.PrimaryPart:SetNetworkOwner(nil)
	
	local enemySpeed = model.Humanoid.WalkSpeed

	local orientation, size = model:GetBoundingBox()

	for index = 1, #waypointfolder:GetChildren() do
		local node = waypointfolder[index]
		local distance = (node.Position + Vector3.new(offset, size.Y / 2, 0) - model.PrimaryPart.Position).Magnitude

		local movementTween = TweenService:Create(model.PrimaryPart, TweenInfo.new(distance / enemySpeed, Enum.EasingStyle.Linear), {
			CFrame = CFrame.lookAlong(node.Position + Vector3.new(offset, size.Y / 2, 0), model.PrimaryPart.CFrame.LookVector)
		})

		movementTween:Play()
		movementTween.Completed:Wait()

		local rotationTween = TweenService:Create(model.PrimaryPart, TweenInfo.new(0.5, Enum.EasingStyle.Linear), {
			CFrame = CFrame.lookAlong(model.PrimaryPart.Position, node.CFrame.LookVector)
		})

		rotationTween:Play()
		rotationTween.Completed:Wait()
	end

	model.Humanoid:MoveTo(exit.Position + Vector3.new(offset,0,0))
	model.Humanoid.MoveToFinished:Wait()

	ReplicatedStorage.BaseHealth.Value = math.clamp(ReplicatedStorage.BaseHealth.Value - model.Health.Value,0,ReplicatedStorage.MaxBaseHealth.Value)
	model:Destroy()
end

return module

By the way, to set the NetworkOwner of a model, you can just set the PrimaryPart’s NetworkOwner. This script also assumes you set up your orientation correctly, all of the nodes should be facing the direction that the enemy will face when they are on it.

Example:
image

To see the orientation, right click the part and click Show Orientation Indicator, then rotate your part.

5 Likes

the enemies just wont move. Here is the code i am using:

--!native
local module = {}
local ServerStorage = game:GetService("ServerStorage")
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")
local PhysicsService = game:GetService("PhysicsService")

local enemySpeed = 2

module.spawn = function(name:string, waypointfolder:Folder, exit, spawns:CFrame, boss:BoolValue)
	local model:Model = ServerStorage.Enemies:FindFirstChild(name):Clone()

	local offset = math.random(-1,1)

	model:PivotTo(spawns + Vector3.new(offset,0,0))

	if boss then
		ReplicatedStorage.AddHealthBar:FireAllClients(name,model.Health.Value,model.Health.Value)
	end

	model.Parent = Workspace:FindFirstChild(ReplicatedStorage.Map.Value).enemies
	--local health = model.health
	--health:GetPropertyChangedSignal("Value"):Connect(function()
	--	if health.Value == 0 then
	--		model:Destroy()
	--	end
	--end)

	model.PrimaryPart:SetNetworkOwner(nil)

	for index = 1, #waypointfolder:GetChildren() do
		print("startedloop")
		local distance = (waypointfolder[index].Position + Vector3.new(offset, 0, 0) - model.PrimaryPart.Position).Magnitude
		print("calculated distance")
		local moveinfo = TweenInfo.new(distance / enemySpeed, Enum.EasingStyle.Linear)
		print("createdmoveinfo")
		local movegoal = {
			CFrame = waypointfolder[index].Position + Vector3.new(offset,0,0)
		}
		print("created move goal")
		local movementTween = TweenService:Create(model.PrimaryPart, moveinfo, movegoal)
		print("created tween")
		movementTween:Play()
		print("moving")
		movementTween.Completed:Wait()

		local rotationTween = TweenService:Create(model.PrimaryPart, TweenInfo.new(0.5, Enum.EasingStyle.Linear), {
			CFrame = waypointfolder[index].CFrame + Vector3.new(offset, 0, 0)
		})

		rotationTween:Play()
		rotationTween.Completed:Wait()
	end

	model.Humanoid:MoveTo(exit.Position + Vector3.new(offset,0,0))
	model.Humanoid.MoveToFinished:Wait()

	ReplicatedStorage.BaseHealth.Value = math.clamp(ReplicatedStorage.BaseHealth.Value - model.Health.Value,0,ReplicatedStorage.MaxBaseHealth.Value)
	model:Destroy()
end

return module

and the model:
image

i found out the code start yielding when it tries to create the tween. @Katrist

3 Likes

You’re accessing ServerStorage so this is a server sided script. Some issues here:

  1. It’s costly to run Tween’s on the server. Tends to be a common culprit of lag.
    A possible alternative would be to use for loops combined with CFrame:Lerp() to achieve a similar effect of interpolating between two CFrames as I mentioned in my last post. Lerp works to interpolate a specified distance in a straight line between two objects.

    You call Lerp() on your starting CFrame, its first Argument is your goal CFrame, and its second argument is what fraction to interpolate from 0 - 1; where 0 is 0%, which is the same as your starting CFrame. A value of 1 is 100%, equivalent to your goal CFrame (argument 1). 0.123 would put you 12.3% of the way to the goal CFrame. In the code below I put it in a for loop that runs from 1 - 10, and call Lerp using 1/10th of the ’ i ’ value equivalent to calling 0.1 - 1.0.
local c0 = CFrame.new(0,0,0)
local c1 = CFrame.new(0,10,0)
local iterator = 1 -- You can use this to specify a frame rate (e.g. 1/30), although consider performance costs

for i = 1, 10, iterator do task.wait()
local i_CFrame = c0:Lerp(c1,i/10)
print (i_CFrame.Y) -- Will print 1 - 10
end

  1. You’re creating an entire set of new TweenInfo’s and Tweens each time you spawn a mob. Instead, you could make one TweenInfo for each type of mob and use that to create your Tweens. Additionally, once a tween finishes playing, you should destroying them.

    Alternatively- you could consider a system whereupon a mob dies, it is despawned rather than destroyed, and when the game looks to spawn a new mob it first checks the ‘despawned mob pool’ to see if there are any available mobs that can be respawned. This allows you to reuse both the model and its tweens rather than always creating a new one.

  2. Otherwise, consider:

  • Running the mobs and/or other intensive calculations on the Client rather than the sever.
  • Remove humanoids altogether and replace them with AnimationControllers.
  • Reducing the number of mobs you spawn. One example of this would be increasing the types of mobs such that instead of of spawning ten Lvl 1 mobs you spawn two Lvl 5’s or one Lvl 10. You could make this a performance setting available for players to set. Or you can automatically set it based on a Clients frame rate / ping / etc if you wanna be fancy.
3 Likes

I’ve edited my code, I forgot to add a CFrame.new. After it works, I’ll add a RemoteEvent to transfer the tween onto the client so it looks smooth.

1 Like

uh…

i think the offset isnt exactly working?
the rotation tween is playing to early.

here is the orientation.

1 Like

Could you unprivate the video?

2 Likes

updated the video to make it unlisted.

1 Like

Just edited my code, could you check it? I set the offset wrong. I’ve also made the speed adjust to the humanoid’s WalkSpeed.

2 Likes

okay, it works okay, but the rotating is a bit slow. Also it seems kinda laggy. With 100 enemies, my crappy laptop runs at 20-30 fps.

1 Like

Should send the tween through a remote? Because i saw that tweens are heavy on server.

2 Likes

Yes, that’s what I was going to move on to. If you know how to set it up, you should give it a try. Shouldn’t be too difficult.

Change the tweenInfo for the rotating from 0.5 to lower.

I would also like to see a video of what it looks like after you change the above.

2 Likes

heres the video of the new enemy system

1 Like

Make sure you set the end node position correctly, looks a tad bit high.

3 Likes