Need help with optimization

So, recently, I tried to add a purely visual system that creates a bounding box in UI when player is near an object with assigned tag. It worked well with a couple of items, but in my game there could be hundreds of items with that tag. I knew that I would need optimization, but I was unable to decrease the script’s activity to less than 3%, and it peaks at 40% in actual gameplay.

So far I tried to get items with tags in a certain distance from the player on server, but that doesn’t update properly and firing a function every frame for 8 players doesn’t seem to benefit anything.

Here’s my code (in Local Script):

local camera = workspace.CurrentCamera
local player = game.Players.LocalPlayer
local ui = player.PlayerGui.BoundingBoxes
local originalFrame = ui.BoundingBoxOrig:Clone()

local rs = game:GetService("RunService")
local ts = game:GetService("TweenService")
local cs = game:GetService("CollectionService")
local d = game:GetService("Debris")

local createdFrames = {}
local createdAdornees = {}
local items = {}

local op1 = OverlapParams.new()
op1.FilterDescendantsInstances = {}
op1.FilterType = Enum.RaycastFilterType.Include

function removeFrame(f, pos)
	table.remove(createdFrames, pos)
	table.remove(createdAdornees, pos)
	
	ts:Create(f, TweenInfo.new(0.125), {ImageTransparency = 1}):Play()
	d:AddItem(f, 0.5)
end

function removeNotInWorld()
	for pos, add in pairs(createdAdornees) do
		if not add:IsDescendantOf(workspace) then
			removeFrame(createdFrames[pos], pos)
		end
	end
end

function removeBox(thing)
	local found = table.find(createdAdornees, thing)
	if found then
		removeFrame(createdFrames[found], found)
	end
end

rs.PreRender:Connect(function()
	removeNotInWorld()
	
	for _, thing in pairs(cs:GetTagged("HasBoundingBoxUI")) do
		if not thing:IsDescendantOf(workspace) then continue end
		
		local actdistance = thing:GetAttribute("BBActivationDistance")
		local usesphere = thing:GetAttribute("BBUseSphere")
		local color = thing:GetAttribute("BBColor")
		
		local screenPos
		local cframe
		local xdist
		local ydist 
		local zdist 
		
		if usesphere then
			op1.FilterDescendantsInstances = {thing}

			local sphere = workspace:GetPartBoundsInRadius(
				camera.CFrame.Position, actdistance, op1)

			if #sphere == 0 then
				removeBox(thing)
				continue
			end
		elseif (cframe.Position - camera.CFrame.Position).Magnitude > actdistance then
			removeBox(thing)
			continue
		end
		
		if thing:IsA("Model") then
			local bbframe, bbsize = thing:GetBoundingBox()
			screenPos = camera:WorldToScreenPoint(bbframe.Position)

			cframe = bbframe
			xdist = bbsize.X / 2
			ydist = bbsize.Y / 2
			zdist = bbsize.Z / 2
		else
			screenPos = camera:WorldToScreenPoint(thing.Position)
			
			cframe = thing.CFrame
			xdist = thing.Size.X / 2
			ydist = thing.Size.Y / 2
			zdist = thing.Size.Z / 2
		end
		
		local creating = true
		local frame = nil
		local found = table.find(createdAdornees, thing)
		
		if found then
			creating = false
			frame = createdFrames[found]
		end
		
		if creating then
			frame = originalFrame:Clone()
			frame.Parent = ui
			frame.Visible = true
			frame.ImageColor3 = color
			frame.ImageTransparency = 1
			frame.Name = "BoundingBox"
			ts:Create(frame, TweenInfo.new(0.125), {ImageTransparency = 0.5}):Play()
			
			table.insert(createdFrames, frame)
			table.insert(createdAdornees, thing)
		end

		-- Yea this is bad
		local verticies = {}
		table.insert(verticies, cframe:ToWorldSpace(CFrame.new( xdist, -ydist,  zdist)))
		table.insert(verticies, cframe:ToWorldSpace(CFrame.new( xdist,  ydist,  zdist)))
		table.insert(verticies, cframe:ToWorldSpace(CFrame.new(-xdist, -ydist,  zdist)))
		table.insert(verticies, cframe:ToWorldSpace(CFrame.new(-xdist,  ydist,  zdist)))
		table.insert(verticies, cframe:ToWorldSpace(CFrame.new( xdist, -ydist, -zdist)))
		table.insert(verticies, cframe:ToWorldSpace(CFrame.new( xdist,  ydist, -zdist)))
		table.insert(verticies, cframe:ToWorldSpace(CFrame.new(-xdist, -ydist, -zdist)))
		table.insert(verticies, cframe:ToWorldSpace(CFrame.new(-xdist,  ydist, -zdist)))
		
		-- Point 1, Point 2
		local boundingBox = {Vector2.new(screenPos.X, screenPos.Y), Vector2.new(screenPos.X, screenPos.Y)}
		local visibleVerticies = 0

		-- Yea this is also bad
		for _, v in pairs(verticies) do
			local vPos :Vector2, visible = camera:WorldToScreenPoint(v.Position)

			if vPos.X < boundingBox[1].X then
				boundingBox[1] = Vector2.new(vPos.X, boundingBox[1].Y)
			end
			if vPos.Y < boundingBox[1].Y then
				boundingBox[1] = Vector2.new(boundingBox[1].X, vPos.Y)
			end
			if vPos.X > boundingBox[2].X then
				boundingBox[2] = Vector2.new(vPos.X, boundingBox[2].Y)
			end
			if vPos.Y > boundingBox[2].Y then
				boundingBox[2] = Vector2.new(boundingBox[2].X, vPos.Y)
			end

			if visible then visibleVerticies += 1 end
		end

		if visibleVerticies > 4 then
			local pos = UDim2.fromOffset((boundingBox[1].X + boundingBox[2].X) / 2, (boundingBox[1].Y + boundingBox[2].Y) / 2)
			local size = UDim2.new(0.05, boundingBox[2].X - boundingBox[1].X, 0.05, boundingBox[2].Y - boundingBox[1].Y)

			frame.Position = pos
			frame.Size = size
			ts:Create(frame, TweenInfo.new(0.125), {ImageTransparency = 0.5}):Play()
		else
			ts:Create(frame, TweenInfo.new(0.125), {ImageTransparency = 1}):Play()
		end
	end
end)

Any help would be appreciated! (even if I would be recommended to throw this idea into the fire)

You are checking for this every frame. And you are doing a full scan every frame. What you need is some form of spatial acceleration structure.

Put simply, the only items that can activate would be ones near you, so you just need a table of every item that is close enough to you to have a chance at actually triggering so you are not just wasting a ton of compute each frame. Only considering the subset of tagged objects that may actually be close enough to you to trigger.

There are a lot of structures that do this, but one of the simplest approaches is to just divide the game into a grid and only consider grids your player is in or right next to in your search. This will probably be fine but will break down if you have too many in any given cell.

For more ideas on how to do some of these you can look into broadphase collision detection as this is one of the places these are used the most. These are called spatial acceleration structures though for a direct search of the concept.

Minor addition now that I’ve looked closer, you are also doing a spatial query for every object for every frame too. This is close to a really good solution since roblox does accelerate their spatial queries with similar structures as to my above post. But instead you should be putting everything in the includes list once outside of your render loop (every tagged item) and then just do a spatial query looping through every part it hit which will effectively be everything you are looking for in the cheapest fastest way possible.

How about reversing the logic and having an appropriately sized transparent touchable object on each object and firing the code as a touched event from the objects. You would need to test, but I assume roblox already optimises such checks for only near objects?

Okay so firstly you’re using the prerender event in runservice. It’s great for visual effects and updating the character/camera or whatever. However, alot of people dont realise that you should use the event sparingly. As outlined by the documentation, the engine can’t start to render the frame until the code within the event has finished executing.

https://create.roblox.com/docs/reference/engine/classes/RunService#PreRender

Now, an easy “fix” would be to just use a slower event that is fired less frequently. You could use runservice.Heartbeat. But this isn’t really a fix, sure heartbeat might seem much slower in comparison to prerender. But it’s still fired every 16ms (if a client is getting 240 FPS though then it would be 4ms which means the event would be fired even more frequently). So your script activity would still be quite high. Probably still around the 40% mark.

I had an idea, you could utilize ProximityPrompts to make your script much more optimized. Simply set the proximityprompt’s style property to custom (so that there’s no prompt UI shown). And then listen to the promptshown/prompthidden events. This will decrease script activity by alot. And your code will fire alot less frequently. However, at a cost of increased memory. But honestly, RBXScriptConnections barely take up any memory. Obviously it’s best to save memory and optimize wherever possible. But just to let you know, there are millions of games that have hundreds of RBXScriptConnections. But if there’s any way you are able to disconnect those connections try your best.

So really it’s up to you, you can go for the prompt option. Which fires much less frequently. or do a 1 minute change to heartbeat. Which will fire less frequently than PreRender but will still be very frequent regardless. But ultimately, roblox’s engine can handle alot. So I wouldn’t worry too much but its best to optimize wherever possible.

The first method really improved the performance! Turns out I tried that before and completely messed it up and it didn’t do much. Just to clarify, is this what you meant?
I run this code on Server.

local op = OverlapParams.new()
op.FilterDescendantsInstances = {}
op.FilterType = Enum.RaycastFilterType.Include

while task.wait(0.5) do
	op.FilterDescendantsInstances = cs:GetTagged("HasBoundingBoxUI")
	local distance = setting:GetAttribute("BBMaxDistanceCheck")
	
	for _, player in pairs(game.Players:GetPlayers()) do
		local char = player.Character
		if not char then continue end
		
		local items = {}
		local sphere1 = workspace:GetPartBoundsInRadius(
			char.PrimaryPart.Position, distance, op)
		
		for _, part in pairs(sphere1) do
			local model = part:FindFirstAncestorOfClass("Model") or part:FindFirstAncestorOfClass("Tool")
			part = model or part
			
			if not table.find(items, part) then 
				table.insert(items, part) 
			end
		end
		
		event2:FireClient(player, items)
	end
end

The local script just replaces the list with recieved one and runs through it.

1 Like

Yeah that looks to be what I meant.

I was gonna replace the PreRender event with RenderStepped as PreRender turned out to be more stuttery. Thanks for clarifying though!
And yea, I don’t think I will be using ProximityPrompts because they don’t work like ClickDetectors: they take the distance between part’s center and camera focus, instead of taking the distance between nearest point of part to the camera focus (as far as I know)

I’m not a fan of Touched event in general. It fires a lot of times and is messy to work with. And using invisible parts is bad because the bounding boxes of those would make debugging harder.

Fair enough.

Renderstepped and PreRender both function the same AFAIK. PreRender is a replacement for RenderStepped and RenderStepped is now deprecated and shouldnt be used. Even if they’re both fired at slightly different frequencies there should be no stutter because both of them are basically the fastest runservice events. If there was, it must be due to something in your code other than the actual event or the unlikely option is that the engine had to execute all your code in that event first and then begin rendering frames. Which caused stutter. Did you notice any lag on the client? Eg lower frame rates?

I thought you weren’t willing to use a while loop since the code would run way slower. But since you are willing, then yeah you should do that.

Side notes: Use a while true do task.wait(.5) end instead of while task.wait(.5) do end. Not only is it bad practice but you are yielding before any of the code in the loop has even ran to begin with.

And there’s no need to use pairs anymore when looping through a table. You can just do “in”. I’m assuming you’re a veteran scripter with like 4+ years of experience since you’re still using old ways of code.

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