How to optimize this server-sided turret system?

Working on a Surface-to-Air Missile system for my game.

I have this below snippet of code from a module script, the main Init function where all the SAM turrets connect to:

runService.Heartbeat:Connect(function(dT: number) 
	for _,turret in pairs(turrets) do
		if not turret.hinge or not turret.hinge2 or not turret.status then return end
		if not turret.target and not turret.tracking then
			for _,p in pairs(players:GetPlayers()) do
				local char = p.Character 
				if char then
					local hum = char:FindFirstChildOfClass("Humanoid")
					--print(ignore[p])
					if hum.Health > 0 and turret.ignore[p] == nil then
						if math.abs((turret.model.PrimaryPart.Position - char.PrimaryPart.Position).Magnitude) <= turret.range then
							print(tostring(p))
							turret.target = char.PrimaryPart
							turret.targetPlr = p
							task.spawn(function()
								turret:toIgnore(p)
							end)
							break
						end
					end	
				end	
			end
		end	
		if turret.target and math.abs((turret.model.PrimaryPart.Position - turret.target.Position).Magnitude) <= turret.range then
			turret.tracking = true
			if not turret.model.holder.lockOn.Playing then
				turret.model.holder.lockOn:Play()
			end

			local angle = computeServoAngle(turret.hinge,turret.target.Position,true)
			turret.hinge.TargetAngle = angle
			--print(angle)

			local angle = computeServoAngle(turret.hinge2,turret.target.Position)
			turret.hinge2.TargetAngle = angle
			turret.timed += dT
			--print(angle)
			turret.model.status.BillboardGui.Enabled = true
			turret.model.status.BillboardGui.Frame.grow.Size = UDim2.new((math.min(turret.timeToLock,turret.timed)/turret.timeToLock),0,1,0)
			--
			if turret.timed >= turret.timeToLock then
				print("launch")
				print(tostring(turret.target))
				local missile = turret.model.missiles:FindFirstChild("missile")
				if missile then
					turret.model.PrimaryPart.launch:Play()
					task.spawn(function()
						rocket(missile,turret.target,turret.targetPlr.Character)
					end)
					turret.target = nil
					turret.targetPlr = nil
					turret.tracking = false
					turret.timed = 0
					turret.model.status.BillboardGui.Enabled = false
					print(tostring(turret.target))
				end
			end--
		else
			turret.target = nil
			turret.targetPlr = nil
			turret.tracking = false
			turret.timed = 0
			turret.model.status.BillboardGui.Enabled = false
		end
	end	
end)

function sam:toIgnore(plr:Player)
	self.ignore[plr] = true
	task.wait(10)
	self.ignore[plr] = nil
end

function sam.new(turret:Model)
	local newSam = {}
	setmetatable(newSam,sam)
	
	newSam.target = nil
	newSam.targetPlr = nil
	newSam.range = turret:GetAttribute("range") or 100
	newSam.timeToLock = turret:GetAttribute("timeToLock") or 4
	newSam.timed = 0
	newSam.tracking = false
	newSam.status = true
	newSam.ignore = {}
	newSam.hinge = turret:FindFirstChild("spin",true)
	newSam.model = turret
	newSam.hinge2 = turret:FindFirstChild("elevate",true)
	
	turret.ChildRemoved:Connect(function(child)
		if child.Name == "status" then
			newSam.status = false
		end
	end)
	
	local missiles:Model = turret:FindFirstChild("missiles")
	missiles.ChildRemoved:Connect(function()
		if #missiles:GetChildren() <= 0 then
			if turret:FindFirstChild("status") then
				turret:FindFirstChild("status").BrickColor = BrickColor.new("Really red")
			end
			newSam.status = false
		end
	end)
	
	table.insert(turrets,newSam)
	
	return newSam
end

I have a few concerns:

  • This runs on the server. I have tried to run it on the client, but given that the game uses StreamingEnabled, this is simply not possible, unless I have a million remote firings going on - which is again not optimal. At best, I can run this on the client when the player is nearby it, but given that these SAM turrets are placed at the bases/tycoons, and are meant to protect the base and teammates from raids, it would be useless if the client it is attached to were out of streaming range.
  • Is there a way to optimize this running on the server? Multiple SAM turrets running a RunService loop is definitely not good for performance… However this thing needs to be constantly running, especially with the way the system has a progression to locking (to give the target time to get out of range) that adding a debounce wouldn’t really work.

Does anyone have any ideas?

3 Likes

Instead of using a module try using a single server script with Collection service. Make a tag for the rocket launcher and then loop over all launchers.

1 Like

That is what I was thinking too, however; there are some important values that need to be stored outside of the loop
image

If I used CollectionService there really wouldn’t be any way to store these values.

Whenever the SAM turret model is deleted, because there is a script inside it that connects to the .new function, it will cleanup whatever is inside that - have this in the module script for cleaning up the other tables:

workspace.DescendantRemoving:Connect(function(descendant: Instance) 
	if turretModels[descendant] == true then
		turretModels[descendant] = nil
		for index,val in pairs(turrets) do
			if val.model == descendant then
				table.remove(turrets,index)
			end
		end
	end	
end)
1 Like

For the progression to locking, store last lockon begin time, then when lockon should be complete you check current time - last lockon time to see if you can confirm to fire. That would not require render stepped. I have a number of turrets in my game and I don’t have them scanning for targets each frame, I also keep a master table of targets that all turret can tap into.

2 Likes

How would I check for targets based on their distance without using a loop, like render stepped, then?

1 Like

What you should do is Use collection service and just keep the values in attributes, and for stuff like objectvalues use instances in folders. I recently wrote a whole modular projectile system with this method and its super performant can handle thousands of projectiles at once all with different parameters and behaviours.

From what i can see the issue is in the way the system is designed and not code itself. Only way to optimize this is to redesign how the system works and communicates, As i said attributes are super fast and collection service is the way.

You can also try lowering the fidelity if calculations to also squeeze out some cheap performance.

2 Likes

Fair enough. I’m more worried about just constantly looping it via RunService - but if you use your method for the same thing (including using RunService to track potential targets), then so be it.

I work on artillery system rn, and i use client-server system with OOP, you shouldn’t use streaming enabled, making custom system is the best way, overall lags are caused usually due to unnecesary collisions or shadows and code that stresses server

If you think your optimization makes your code very hard to write or impossible sometimes, don’t use it

The solution might just be running it on the client entirely.

Set ModelStreamingMode of your projectile model to Persistent to prevent StreamingEnabled from streaming it out when out of streaming range.

I’m not going to turn off StreamingEnabled just so it makes one tiny aspect of the game (an AI SAM turret) functional and less developmentally complicated.

Having StreamingEnabled in my game has done a lot in terms of performance. I have another, similar game where StreamingEnabled is turned off, and the memory usage is way higher and the frame rates significantly lower there than the game where StreamingEnabled is on.

FWIW, StreamingEnabled streams out parts that may have “unnecessary collisions” when they are out of range of players…

Already tried that. The issue becomes, what happens if the owner of the SAM turret is 1,000 studs away from the SAM turret when an enemy player approaches it? The SAM turret will still render for that owner, but the enemy player’s model will not (in traditional StreamingEnabled behavior), and as a result, the SAM turret code will either error (player’s character is nonexistent on the SAM turret owner’s client) and/or just be non-functional, unless the owner of the SAM turret is rendering the enemy player at the same time.

I supposed I could make a work-around to this by making all character models (upon spawn) “Persistent” instead of “Default” (or whatever it is) for StreamingBehavior…

options:

limit the frequency of the heartbeat i.e. return if accumulated dt does not exceed a var i.e 1/10 (thus every .1 seconds you do the contents.)

aggressively optimize where you have a zone module handling tracking of targets in a low set frequency and targeting module also under a low set frequency, both assess batches in a performant manner, where the turret only needs to receive a .target value change

Yeah i understand why you would be worried, i think it shoudlnt have that much of an impact on performance if its just for a few players, otherwise you can just do a normal while loop with a task.wait(1/30) which will run it every other tick. then just do some interpolation to make up for the fewer updates.

When i was writing my projectile system a HUGE optimization i did was moving ALL visuals onto the client, so any instancing happens on players clients instead of the server.
Server would calculate the physics and update the projectiles position and other parameters and once the projectile hit something a remote event would fire to all clients to play out the visuals.

Dont torture yourself about the optimization, Roblox itself is very badly optimized and we as developers cant really do much about it, try your best at optimizing but dont beat yourself to it.

That’s the problem. There are 11 bases in my game, so at maximum 11 people will have these things running. In addition, any sort of delay that is not “natural” or part of delta time, will result in the movement of the SAM turret looking pretty unnatural/crappy if there are delays in the calculations, vs. having it down every frame.

As mentioned above, the SAM turret will follow its target as it locks on, so that movement is crucial. I could definitely replicate this via a remote firing on all clients, however, the receiving stat for my game is relatively high, if I am firing remotes on all clients all the time as the SAM turret makes its changes to where its pointing at.

In this picture it’s low, but it can regularly bounce around from 40 kb/s to 150+ kb/s.

image

Other stats shown just for reference.

I try not to torture myself for optimization on ROBLOX, but I have a habit of making large games with even larger server sizes, so I gotta do what I gotta do

How could firing a couple remotes a second be more network intensive than replicating CFrames from server to client?

You shouldn’t be worried about that little of memory usage.

1 Like

I’m under the assumption that sending a fired client remote across all clients in a server of 22 every frame will be very intensive as well.

For a larger sized game with 22 player servers, yeah it’s not bad, but the “Receiving” stat will fluctuate from 30kb/s to upwards of 250kb/s - it will be that high for a few seconds before falling down to the lower spectrum again

You shouldn’t send it every frame. 10 FPS would be more than enough, and you could add interpolation on the client (as mentioned before) to make it look smooth. You need to remember that there will always be delay between the server and client, and having it update this frequently will only exacerbate this delay.

That’s the tradeoff with streamingenabled. Of course the recv stat will be high if you’re constantly loading and unloading assets. You should only keep streamingenabled on for constantly changing assets, and keep mostly persistent assets loaded at all times. Higher memory usage is better than higher network usage. RAM exists for a reason :slight_smile:

Yeah, I assumed that was because of StreamingEnabled. Staying in place gave an absurdly low Receiving rate (less than 30kb/s at times), moving around in vehicles resulted in an absurdly high one, temporarily. As long as it’s not permanent then, it should be fine.

This is fair, but I have a game that doesn’t use StreamingEnabled, and it not an uncommon occurrence for the Error Code 277 to occur when the memory runs out. I’ve checked Performance Stats, and it’s constantly Physics running up the memory usage; e.g. collisions with the terrain, meshparts on the other side of the map that a player on the opposite side won’t see anyway, etc.

I think I may just make it so when a player enters an aircraft/weapon I want to be vulnerable to the SAM turret, I make their character model “Persistent” so I can run the SAM turret (which would also be persistent) on the client (avoiding a lot of my concerns mentioned above).

And then obviously when said player hops out of the aircraft/weapon, their character model’s streaming mode is back to “default” or whatever

2 Likes

Here’s my proposal:

SERVER:

  • Calculate the target, then set it as an attribute of the turret.
  • Fire the projectile.

CLIENT:

  • For all turrets within reasonable range:
  • Run the animation/cframe/hinge stuff to make the turret look at the target

In this model, the only thing that needs to be replicated is the target attribute and the missile. (It is standard practice to run visuals on the client.)

1 Like

Yup, thats true. There will ALWAYS be some kind of delay, games like Deepwoken make up for it with Ping compensation and similiar methods which still are imperfect. There is literally no need to run the simulation every frame as noone will really notice it, Even if it were to run once per second you can still make it look smooth by using interpolations and splines / bezier curves. Its all about doing some clever maths and optimizations here and there.

1 Like