Proximity-based interaction - Help determining performant practice

At the moment, I’m currently trying to create an interaction system which allows players to interact with the world environment depending on their proximity to an interaction-based item. I have the interaction system all pat down, but the one thing I don’t have down right now is which method I should use to determine player distance: a Region3 with a whitelist containing interaction items, or using Player::DistanceFromCharacter against all of those items.

I have a feeling like a Region3 with a whitelist may be more performant since it’s only checking the area around the player instead of all items at all times and it’s not necessarily expensive to run in a rapid loop, but what do you guys think? Which one should I be going for?

4 Likes

Region3 with CollectionService tagged whitelist. Then loop thru all results for closest magnitude between character and object.

Depending on the size of your game you could skip the region3 step, but it would be the most performance friendly option, imo.

Another thing you could do is update “nearby interactions” every 3-5-10 seconds (using a region3) and then skip the region3 step when distance checking.

6 Likes

Basically what I was about to get started on, but had a hint of doubt in me about using the Player::DistanceFromCharacter method over this. That’ll do for an answer.

Just one thing - no to the closest magnitude thing. Region3 will handle that for me.

Just check the items magnitude when the mouse hovers over an item. Region3 checking is a lot more expensive than just using ‘distance from character’. Magnitude just gets a variable from two vector3 points.

OP states explicitly that I’m attempting to create a proximity-based interaction system. That doesn’t involve the use of the mouse. If I had been using a system that acted similarly to ClickDetectors, I wouldn’t have made this post. Just check the mouse’s target and the distance to that target. That’s where Player::DistanceFromCharacter jumps in. I probably should’ve stated explicitly “proximity from character without the use of a mouse”.

Region3 checks are not expensive if

  • You aren’t running an expensive loop in the first place
  • You’re using the proper function for the proper scenario (note that I’m using whitelist Region3s)
  • Checking distance would probably be more expensive in my use case, over a Region3

While magnitude only involves the distance between two points, you also have to factor in the scenario for which it’s being used in (i.e. how big the map, how many interaction parts, the type of interactions, their specific functions, etc). You could probably create a pseudo whitelist system for mouse target checking but again, that’s not what I’m trying to achieve.

I just needed a bit of clarification for something I felt a bit of unease on.

Oh, okay. :grinning:

If a performant solution is desired, a generic solution will probably not be the answer. There are some general good principles through:

  1. divide and conquer (for example, divide your game into ‘regions’, ‘rooms’, ‘buildings’, ‘worlds’, or whatever to narrow down your choices. The ultimate generic form of this would be an Octree, although hand crafted and tailored systems will be better.)
  2. only perform work when needed (don’t test every frame, only when something has changed. A very good generic form of this is an even driven system, although Roblox sometimes doesn’t have events for what you want. Perhaps hit spheres around objects would work for you.)
  3. perform work where / when it is cheapest (In Roblox, this generally means push as much computation as you can off onto the C++ engine rather than Lua. Unfortunately adding two numbers in Lua takes on the order of microseconds, whereas C++ only takes nanoseconds (1,000 times faster). Considering that each frame has only 16 milliseconds to perform everything including rendering, networking, and memory allocations, and all the other game engine stuff including your code, you need every microsecond you can muster for those low end devices!)
1 Like
  1. The game is already divided like this, I’m not entirely sure what you mean by this point.
  2. The whole point of me running something every frame (loop) is so that I can check the area around the player and see if parts are in a white list. If not a loop, then there’s no real way to determine a player’s distance to something at all. I don’t think running the code every time something changes would be better than every frame - it could potentially be worse. Mind delving into hit spheres a bit more?
  3. I don’t entirely get what you mean by this either. How?

I smell premature optimization in this thread…

One big assumption that’s being made here is that iterating over all the objects is slow (which is where I assume the Region3 idea came from). I wrote a little test for this assumption:

repeat wait() until game.Players.LocalPlayer.Character

local CollectionService = game:GetService("CollectionService")

local function generatePart()
	local part = Instance.new("Part")
	part.Position = Vector3.new(math.random(-100, 100), math.random(-10, 10), math.random(-100, 100))
	part.Anchored = true
	part.Parent = game.Workspace
	CollectionService:AddTag(part, "interactive")
	return part
end

for i = 1, 10000 do
	generatePart()
end



local deltaTSum = 0
local i = 0

while wait(0.1) do
	i = i + 1
	local t0 = tick()
	for _, part in pairs(CollectionService:GetTagged("interactive")) do
		if game.Players.LocalPlayer:DistanceFromCharacter(part.Position) < 10 then
			-- do stuff
		end
	end
	local t1 = tick()
	deltaTSum = deltaTSum + (t1 - t0)
	print("average deltaT: " .. deltaTSum / i)
end

after running this for a little while the average delta T was about 38ms (for 10,000 elements!). my advice is that unless you plan on having tens of thousands of these interactive objects in your game, a simple iteration is a fine approach

edit: you can also shave off ~11ms by declaring the CollectionService:GetTagged("interactive") bit as a local variable outside the scope of the while loop, which I probably should have done in the first place

12 Likes

Not really sure it’s premature optimization, but it may be.

Certainly, one assumption I did make when I started this thread was that iterating over tons of objects would be slow and that it’d be better to check what’s within a player’s region rather than everything at all times. I didn’t run a code test so that’s probably something to note for next time.

I’ll be switching solutions to this, as it seems more relevant to the use case I’m attempting to apply. I don’t really have that many interaction objects and don’t see a reason why there’d need to be. I don’t think many games would need that many either. One thing though: if by some miracle I do have a colossal amount of interaction objects, should I settle for a Region3 instead?

1 Like

The short answer is… maybe.

There are other things that could be done with the example I posted to speed up the traversal (i.e. storing the positions of the parts in an array in a “pre-processing” step, and then traversing that array instead), but as that list of objects grows truly large there will be performance problems. Under these conditions (tens of thousands of objects) something like a repeated FindPartsInRegion3WithWhiteList call is probably faster

another edit: the fastest I was able to make that example (using the two improvements I’ve cited in this thread) was 16ms for 10,000 elements. Probably decent for general use

2 Likes

Once a player enter’s a section of your game, then you only have to check the distances between the player and the intractable objects in that section. For example if your game was a city and the player entered a room of a building, then you would only need to check how close the player is to the much fewer intractable objects in that room. This alone will drastically reduce performance consumption, and is likely the only optimization you will ever need to perform.

You’re right, if not in a loop then there is no way to determine a player’s distance to something. Luckily for us though, the collision system already runs in a loop. We can use this to determine when the player is a specific distance from a part by placing a sphere with the desired distance as the radius around the stationary object, a “hit sphere”. When it touches a player, we know that the player is within distance. This is performant because it is event driven and allows as much work as possible to be done where it is fastest - in C++.

By not performing the distance calculations in your Lua code, but allowing the built in C++ collision system to do it for you. The way to do this is described above as “hit spheres”.

1 Like

Thanks for the tips, though responses.

My game isn’t really that huge, so I don’t think I need to optimize for this. It’s actually quite a small map and has only a few maps. Though I understand where you’re coming from here.

I still don’t catch what you mean here. Explain it like I’m five years old. Are you suggesting the use of Touched events on certain parts around a range in which I have the interaction brick? I’d rather not do that.

1 Like

The TLDR is that, in a dense map and with not that may interactive points of interest (POI, hundreds), DistanceFromCharacter() is predictable and fast. ~0.7ms for 1000 POI on my laptop, and it’s what I typically use. FindPartsInRegion3WithWhitelist can beat it on a baseplate with a small region3 (tens of studs on a side), but in practice, cost can explode to many times the cost of the distance method as you increase the size of the region–both absolutely, but particularly in relation to the overall part density of the world. Where the region3 method clearly wins is for very large numbers of POI (tens of thousands) where the O(N) cost of distance checks exceeds available calculation time, and when you only care about things in a small area around the player. In this case, it scales better.

The full answer to this is still fairly straightforward, and the best choice will depend on a couple of factors. First, look at the components of cost for each option:

For DistanceFromCharacter, there is effectively just linear scaling with number of POI. The cost is a straightforward O(N), linear with the number of interactive items. Most of this is Lua loop overhead, since a Lua loop iteration is a lot more expensive than the DistanceFromCharacter call itself. This approach is completely invariant to the distances, distribution of the items, non-POI part-count and part-density of the world. So, in terms of runtime performance, this search has O(N) for it’s worst, average, and best cases. It’s steady, and you know what you’re gonna get. For < 1000 points to test, this method is sub-millisecond.

The cost situation with FindPartsInRegion3WithWhitelist is a lot more complex. Collision tests with the whitelisted parts are AABB checks, which is more expensive than just distance. But, all the parts are in an Octree, so if your region is small relative to the typical spacing between parts in the world (all parts, not just interactive ones), you end up testing very few parts, so pretty negligible. The end result is that your worse-case cost is roughly proportional to the volume of your region times part density of the most part-dense part of world. Best case runtime is super fast, when there are just a few parts in the region to check against the white list. Average case is cost of iterating over the average number of parts likely to be in the specified R3 volume. Worst case is if there is a place on the map that has thousands of parts, including ones on the whitelist, all within a volume that can be enclosed by the region3 size you’ve chosen. If you use a big region3, hundreds of studs on a side, encountering a part-dense area on your map could be cripplingly expensive. The upper bound is a lot harder to estimate than for the simple distance check case.

If you’re doing proximity checks on the server, for when you can’t trust clients, it’s rarely necessary to do the checks for all players, every frame. You can usually amortize this by checking a few players per heartbeat. It’s also worth noting that with the distance check, you might also have other game-specific information that hugely reduces how often you need to do your tests. For example, if you have an open-world exploration map where the player is on foot moving at up to 16 studs per second, and objectives don’t move, then if your distance checks find that the nearest objective is 160 studs away, you don’t even need to run the checks again for nearly 10 seconds, unless the player teleports or resets or does some other form of discontinuous jump. You can cull a lot of checks this way, and even partition objectives by minimum time it would take to reach them. But don’t do any of it until you need to. :slight_smile:

21 Likes