Rendering models within a radius of a user efficiently?

A few people and myself are organizing another year of a community-based event that’s been going around since the early 2010s. Users submit stalls with their creations and they’re displayed in a purpose-built venue for the occasion for a few days before we close shop and prepare for the next year. This year, we got double the amount of submissions that we would’ve normally gotten as we started branching out into other parts of the wider community instead of a small sub-section. This has however, brought up problems with experience optimization especially with such a high number of submissions and varying levels of part counts and model sizes.

We set limits on the stalls such as width, length and height, as well as part limits (the maximum for this year was 1000 parts). Of course we can’t load all of the stalls all at once or that would mean hardly anybody would be able to visit let alone go around the venue as smoothly as possible. Usually we use radius-based methods where stalls within a certain radius of the user will load and stalls that leave this radius will be removed.

However this year we seem to be having significant issues with how people are able to play the experience. We’ve tried two methods to load stalls from the server to the client, however both have had issues that we’ve been unable to resolve.

The first method was to fetch the stall model from server storage, place them in a folder on the client and then proceed to place them in the workspace. This method would make unions and other assets load faster, however we discovered in testing that it would cause massive memory leaks on the server side causing crashes after a few minutes with about 10 players (not a great outcome for an event where hundreds are expected).

The second method would instead move the stalls folder from ServerStorage to ReplicatedStorage (not a great idea as it exposes all the stalls to the client which can make leaking much easier for bad actors), and follow a similar method to the first method. While this would prevent the memory leaks, stalls would take much longer to load for the user, or sometimes just not load at all on the client-end. This method is also susceptible to temporary short-lived lag spikes when moving between locations (halls). However with both methods, we could not establish a stable ping on either the Client or Server, both would have pings in the hundreds (or sometimes thousands) which is not ideal.

While last year the previous method was what we used (and worked quite well) this was because our stall count was significantly smaller than it is now. When we conducted a playtest earlier today, the second method that we quickly implemented after the memory-leak fiasco would be simply unviable at a large scale especially when we go public later this month for the event itself.

Why not use StreamingEnabled?
We’ve tried, and that’s resulted in elements breaking along with part of our GUI elements (mainly the main menu) meaning nobody is able to play, or takes significantly longer for anything to actually happen. StreamingEnabled also broke tests in studio, as it would only present us with a blank skybox and proceed to do nothing afterwards.

TLDR
Is there any method to efficiently load/unload stalls from the server to the client that doesn’t cause any significant performance hit to the client, or cause the server to go into meltdown and crash without using StreamingEnabled?

1 Like

Replication

I don’t know of any way to replicate Instances to certain clients and not to others without StreamingEnabled. Here’s some other ideas:

  1. Fix the memory leaks in your current method and use that. Memory leaks are specific kinds of bugs in code, unless you mean something other than memory leaks (I think this is the case).

  2. Place the stalls in a folder in ReplicatedStorage in the first place (instead of moving them there at runtime) and plop them in local workspace when you need 'em.

    Your joining load times will be long, but your gameplay shouldn’t suffer too badly and rendering should be fine. Just would use a bunch of RAM. Might be fine.

  3. Break the stalls up into a few different places and teleport players between them. Wrap it in some pretty doorways or something.

  4. One (hard) option could be to somehow serialize your stalls to a string, send the string over a remote, and deserialize on the client. Aside from the difficultly of serializing all your stalls (which you could do statically rather than at run-time, at least), the cost of deserialization might not be worth it. If the stalls include scripts, this method doesn’t work at all unless you deal with script replication separately.

  5. A fourth option is… use StreamingEnabled. Fix the bugs in your GUI and game.

My favorite is 3 because it’s easy.

Popping in/out

How are you determining “closeness to the player”?

There are fast ways, like octrees or just mapping regions to lists of stalls, and there are slow ways, like looping over all stall positions.

Make sure you’re using a fast way, because maybe that’s the real performance issue :wink:

We were using math.abs and Magnitude on the characters root part along with a part from the stall itself to give the player a radius. Currently this is done with while wait() which ik can be incredibly inefficient sometimes:

local frequency=1.5
while wait(frequency) do
	for _,stall in pairs(stalls) do
		if math.abs((character.HumanoidRootPart.Position-stall.PrimaryPart.Position).Magnitude)<script.maxd.Value then
			stall.Parent=WorkspaceStalls
		else
			stall.Parent=ReplicatedStalls
		end
	end
end

What are octrees?

This is our stopgap method that we’re currently using however takes way too long to load assets or some assets don’t load at all. (Plus people leaking the whole event giving us more headaches)

An octree is a tree data structure in which each internal node has exactly eight children. Octrees are most often used to partition a three-dimensional space by recursively subdividing it into eight octants.

Octree - Wikipedia

It’s a more efficient way to divide space up and perform lookups like “get the closest object to me”. It can do it without looping over all the objects.

@Quenty has an implementation here you could look at: NevermoreEngine/Modules/Shared/Region3/Octree at version2 · Quenty/NevermoreEngine · GitHub

Might smooth out some lag spikes.

Still though, I think breaking it up into a few different places and teleporting between them is a decent way to go.