Looping to find the closest part to player is causing lag

I have this script here to find the closest part to your player in a loop and it causes extreme lag. Is there any way to do this without causing lag?

Here is the part of my code that causes the lag.

local Obj = nil

local function findClosestPart(group, position)
    local closestPart
	local closestPartMagnitude = 15 -- Max Distance

    local tmpMagnitude -- to store our calculation 
    for i, v in pairs(group:GetDescendants()) do
		if v:IsDescendantOf(game.Players.LocalPlayer.Character) == false then
	        if closestPartMagnitude and (v:IsA("BasePart") or v:IsA("VehicleSeat") or v:IsA("Seat") ) then-- we have a part
				
				tmpMagnitude = (position - v.Position).magnitude
	
				-- check the next part
				if tmpMagnitude < closestPartMagnitude then
					closestPart = v
					closestPartMagnitude = tmpMagnitude 
				end
			end
		end
	end
	return closestPart, closestPartMagnitude
end

--< Loops
game:GetService("RunService").Heartbeat:Connect(function()
	local closest,mag = findClosestPart(workspace,game.Players.LocalPlayer.Character.HumanoidRootPart.Position)
	if closest ~= nil then
		if closest ~= Obj then
			Obj = closest
			print(closest)
		end
	end
end)

Suggestions, ordered from greatest impact to least:

  1. You should try just running the code on a longer wait than every 60th of a second, I don’t think you need to find the closest part every frame. I would think something like wait(1) would be good, but it depends on your situation.

  2. You should be lessening the pool of parts to search before looping through every descendant of workspace. The easiest way to do this is either putting all the important parts in a folder and using Folder:GetDescendants() or using CollectionService and looping through CollectionService:GetTagged( [[tag]] )

  3. VehicleSeats and Seats are BaseParts, so you only need to check if v:IsA("BasePart"), nothing else.

  4. You can store the tmpMagnitude as local to the loop, inside the if closestPartMagnitude... statement.

  5. You can put everything in the same if statement in your for loop:

if not v:IsDescendantOf(game.Players.LocalPlayer.Character) and
	closestPartMagnitude and v:IsA("BasePart")
then

	-- rest of code

end
  1. You can do the same in your run service loop:
if closest and closest ~= Obj then
	Obj = closest
	print(closest)
end
Disproved by Post #6!
  1. You can use a function like this to get the distance between vectors, which removes the need for the square root for magnitude:
local function GetDistance(v3,compareTo)
	local dist = v3.X*v3.X + v3.Y*v3.Y + v3.Z*v3.Z
	if dist < compareTo then
		return true
	else
		return false
	end
end
				-- ...
				-- in your code
				if GetDistance(position - v.Position,closestPartMagnitude) then
					-- ...
				end
			end
		end
	end

	-- also in your code
	return closestPart, math.sqrt(closestPartMagnitude)
end
--...
8 Likes

Just an idea, what if you created a new part, welded it to the HumanoidRootPart, then used TweenService to make it grow to your max distance number on all 3 axes. You could then have a function run when this part is touched that checks if the hit part meets any requirements (e.g. if hit:IsDescendantOf(character) == false), and then have it delete the growing part on contact and and do whatever is necessary with that part that it touched.

What problem exactly are you trying to solve by getting the closest part?

If you need to get closest of every part in workspace consider using workspace:FindPartsInRegion3() (Workspace | Documentation - Roblox Creator Hub) with a region sized around your character based on the min distance as a broad phase, then iterating through that.

1 Like

Thank you so much. This made it much better than it was. Still lags though.

1 Like

Be wary that this is actually slower than comparing the .Magnitude of a Vector3 due to the multiple indexing of object properties working out slower overall.

Proof

Using

local prevtick = tick()

for i = 1, 10^6 do
GetDistance(Vector3.new(math.random(), math.random(), math.random()), 1)
end

print(tick() - prevtick)

and comparing your GetDistance function with this:

local function GetDistance(v3,compareTo)
	if v3.Magnitude < compareTo then
		return true
	else
		return false
	end
end

For the squared vector3 magnitude function, it prints around 0.95903348922729 mostly whereas the normal Magnitude comparison prints around 0.63851451873779 mostly. The 33% reduction is fairly significant enough to default to normal magnitude testing.

That being said, it would be nice to have a MagnitudeSquared property if it could be made to calculate faster than Vector3.Magnitude.

Another decent speedup for the OP might be to group and store part positions in a grid (together with a reference to the part). Then, just calculate the grid cell that your character is in and get all parts in the surrounding grid cells (by pulling them out of the arrays storing parts within those grid cells). From there, do your findClosestPart function on the parts collected from those cells. There are much less parts iterated on this way. This requires the position of said parts to be somewhat static for there to be a beneficial speedup (otherwise, you would have to update the grid cells of changing parts).

5 Likes

I actually use this method to index coins for spinning animations in my game eg!

But, as you say in your answer, this requires the parts indexed to be static or to properly be re-indexed if they leave their cell. Workspace:FindPartsInRegion3() works of off roblox’s own internal spatial hashing, removing the need for you to setup your own, it properly handles moving parts to boot!

2 Likes

Yeah, and given that OP has

local closestPartMagnitude = 15 -- Max Distance

They could just iterate on parts in a Region3 centred at the HumanoidRootPart.Position which is 30x30x30 in size.

This would be something like:

local min, max = HumanoidRootPart.Position - Vector3.new(1, 1, 1)*closestPartMagnitude, HumanoidRootPart.Position + Vector3.new(1, 1, 1)*closestPartMagnitude
local partsToSearchThrough = Workspace:FindPartsInRegion3(Region3.new(min, max))
4 Likes

Just to add, FindPartsInRegion3WithWhitelist is a little bit better here if you already know the group of parts you want to look for.

In the second argument, provide a table of instances that you want to check the descendants of, for example if all your parts were in a model called MyParts in Workspace you’d put { game.Workspace.MyParts } as the second argument and all its descendants would be included in the whitelist.

So add break after this code.
closestPartMagnitude = tmpMagnitude
break --it must go here
what a lua break does is it stops the loop so it goes out of the loop.

Instead of putting it in a runservice put it in while loop with wait of one second.

1 Like

This helped a lot. Thanks, @goldenstein64, @Nimblz, @Quasiduck, and @BanTech for helping with my problem. It still lags but it’s not like I could fully get rid of it anyway.