Help optimizing chunk system

I have code that stores objects in a chunk system. However, the game grows to over 10K objects that are being pushed through the chunk system. It adds a lot of lag. Any way I can optimize my code here?

local tentacleDetectionRange = 250
local eyeballDetectionRange = 100
local anim = script.TentacleAnim

local MyFolder = script.Parent:WaitForChild("Planets"):WaitForChild("Planet1"):WaitForChild("Planet"):WaitForChild("Aliens")
local storage = game.ServerStorage.UnloadedPlants

local function isPlayerNearby(subject, range)
	local players = game:GetService("Players"):GetPlayers()

	for _, player in ipairs(players) do
		local character = player.Character
		if character then
			local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
			if humanoidRootPart and (subject.Position - humanoidRootPart.Position).Magnitude <= range then
				return true, humanoidRootPart
			end
		end
	end

	return false
end

local TentaclesTable = {}

local function tentaclesAnimate(rig)
	local animator = rig.AnimationController

	if not TentaclesTable[rig] then
		TentaclesTable[rig] = { playing = false }
	end

	local playerNearby, HRP = isPlayerNearby(rig.TentacleRig, tentacleDetectionRange)

	if playerNearby and not TentaclesTable[rig].playing then
		TentaclesTable[rig].playing = true
		local animation = animator:LoadAnimation(anim)
		animation.Looped = false
		animation:Play()
		animation.Ended:Wait()
		TentaclesTable[rig].playing = false
	end

end

local function eyeballsAnimate(eye)

	local playerNearby, HRP = isPlayerNearby(eye, eyeballDetectionRange)

	if playerNearby then
		eye.CFrame = CFrame.lookAt(eye.Position, HRP.Position)
	end

end


game:GetService("RunService").Heartbeat:Connect(function(deltaTime)

	for _, Child in pairs(MyFolder:GetChildren()) do

		local isNearby, HRP = isPlayerNearby(Child.PrimaryPart, 500)

		if not isNearby then					
			Child.Parent = storage
		end

		if Child:FindFirstChild("Props") and Child.Props:FindFirstChild("Tentacles") then
			local tentacles = Child.Props.Tentacles:GetChildren()
			for _, tentacle in pairs(tentacles) do
				coroutine.wrap(tentaclesAnimate)(tentacle)
			end
		end

		if Child:FindFirstChild("Props") and Child.Props:FindFirstChild("Eyeballs") then
			local eyeballs = Child.Props.Eyeballs:GetChildren()
			for _, eyeball in pairs(eyeballs) do
				coroutine.wrap(eyeballsAnimate)(eyeball)
			end
		end
	end

	for _, Child in pairs(storage:GetChildren()) do

		local isNearby, HRP = isPlayerNearby(Child.PrimaryPart, 500)

		if isNearby then					
			Child.Parent = MyFolder
		end
	end

	for _, Child in pairs(storage:GetChildren()) do

		local isNearby, HRP = isPlayerNearby(Child.PrimaryPart, 250)

		if isNearby then					
			Child.Parent = MyFolder
		end
	end

end)


1 Like

You could enable streaming under workspace.

Or the better option imo make it client sided.

1 Like

How would I make it client sided?

1 Like

Copy the whole script and turn server storage into replicated storage. You still need to change some stuff, if you find errors I can help.

Edit: also if your game is multiplayer client sided is most definitely the way to go since it would mean all chunks are loaded for all players. Single player can still benefit too.

1 Like

ok… but wont it still be storing everything on the server, as its not being unrendered there?

1 Like

What do you mean? Yes the objects will be still on the server, but it will be in replicated storage so it won’t be rendered reducing the lag. Putting it in the client will put less strain on the server since it doesn’t have to render for all players and the server.

Up to you though.

1 Like

hmm ok. I will do that now. By the way, are you aware of any ways to improve performance on the looping? The problem is the thousands of objects that it has to loop through. Caching or something?

1 Like

Alright! I made the script client-sided which did improve performance a small amount.

local tentacleDetectionRange = 250
local eyeballDetectionRange = 100
local anim = script.TentacleAnim
local player = game.Players.LocalPlayer

local MyFolder = game.Workspace:WaitForChild("Planets"):WaitForChild("Planet1"):WaitForChild("Planet"):WaitForChild("Aliens")
local storage = game.ReplicatedStorage.UnloadedPlants
local character = player.Character
local HRP = character:WaitForChild("HumanoidRootPart")

local function isPlayerNearby(subject, range)
	if (subject.Position - HRP.Position).Magnitude <= range then
		return true
	end
	return false
end

local TentaclesTable = {}

local function tentaclesAnimate(rig)
	local animator = rig.AnimationController

	if not TentaclesTable[rig] then
		TentaclesTable[rig] = { playing = false }
	end

	local playerNearby = isPlayerNearby(rig.TentacleRig, tentacleDetectionRange)

	if playerNearby and not TentaclesTable[rig].playing then
		TentaclesTable[rig].playing = true
		local animation = animator:LoadAnimation(anim)
		animation.Looped = false
		animation:Play()
		animation.Ended:Wait()
		TentaclesTable[rig].playing = false
	end

end

local function eyeballsAnimate(eye)

	local playerNearby = isPlayerNearby(eye, eyeballDetectionRange)

	if playerNearby then
		eye.CFrame = CFrame.lookAt(eye.Position, HRP.Position)
	end

end


while task.wait(1) do

	for _, Child in pairs(MyFolder:GetChildren()) do

		local isNearby, HRP = isPlayerNearby(Child.PrimaryPart, 500)

		if not isNearby then					
			Child.Parent = storage
		end

		if Child:FindFirstChild("Props") and Child.Props:FindFirstChild("Tentacles") then
			local tentacles = Child.Props.Tentacles:GetChildren()
			for _, tentacle in pairs(tentacles) do
				coroutine.wrap(tentaclesAnimate)(tentacle)
			end
		end

		if Child:FindFirstChild("Props") and Child.Props:FindFirstChild("Eyeballs") then
			local eyeballs = Child.Props.Eyeballs:GetChildren()
			for _, eyeball in pairs(eyeballs) do
				coroutine.wrap(eyeballsAnimate)(eyeball)
			end
		end
	end

	for _, Child in pairs(storage:GetChildren()) do

		local isNearby, HRP = isPlayerNearby(Child.PrimaryPart, 500)

		if isNearby then
			Child.Parent = MyFolder
		end
	end

end


2 Likes

I think you should definitely try StreamingEnabled first; the sort of chunk system you’re trying to implement here is what Roblox devs used to do before streaming was a thing that did it for you automatically.

If that doesn’t work, and you do have a need to control things like whether or not to render nearby grass, then absolutely you want to do it client side. Right now, you’ve got it all server-side, which has a few problems: 1) If players are spread out, you have little to no optimization, since what’s visible is the union of what’s in range of each player, 2) Every change cause by each player’s movement has to replicate part changes to all the other players, 3) Your test loop is doing an O(MN) process literally every heartbeat, where M = number of players and N = number of plants, and it does all this work every frame, even when no one has moved. Obviously that’s wasteful.

Moving things client side means each person’s client is doing just O(N) tests, and you should not do all the chunk visibility updates on 10k things every frame, you should use some sort of spatial partitioning. Like defined a chunk area to be something like a 32x32 stud grid, and only bother updating visibility when a player’s avatar changes grid squares, not just every heartbeat.

Or, you can make it radius based: Each time you update the parts on a client, save where the player was (note: their camera position is probably what you actually care most about). Then, each frame, see if they are more than some threshold distance from where they were last time you recalculated, and if so, recalculate and update the saved position. This method does one simple player distance check each frame, which is negligible, and only does the real work if they’ve traveled at least some meaningful distance. You probably still want a grid system or octree though, because when you do need to update, you shouldn’t have to loop over all plants in the whole world, you should only have to care about the grid square the player is in, and the 8 squares around them, for example.

4 Likes

Adding on to @EmilyBendsSpace idea you could probably create invisible hit boxes and use zone plus. When they enter visualize, when they leave hide. Therefore, you wouldn’t even need a loop. Although I’m not sure how zone plus works.

Then again depending on how many hit boxes this could be a bad idea.

1 Like

Ok, first of all, I just want to say how much I appreciate you taking your time to write out that very informative response. Second of all, I want to say that I am relatively new to chunk systems and efficient loops/optimization. Up until recently I have been happy with my mediocre code, which I am trying to change now. Alright. As you can see, I did move everything over to the client and changed it from heartbeat to once a second. I do not really understand how I could partition the plants into 3D chunks, because they are spawning on a 3D planet, not a plane, which makes things a bit more complicated.

This seems more reasonable for my system, but I have no idea how to go about it. Anyways, thanks again for your time and valuable insight! I will take a closer look at the methods you mentioned.

Part-based zones are comparatively expensive, and are really only a good option when you need a few zones and they are irregular (not a uniform grid).

The normal way of putting grid coordinates on something is to just divide the world position and round it, e.g.:

local gridX = math.floor(part.Position.X / 32)
local gridZ = math.floor(part.Position.Z / 32)

That sort of thing is cheap, when all you really need is an axis-aligned, uniform size grid.

2 Likes

you could cache getchildren like this


local parent_to_children = { }

local function getchildren_cached(parent)
	local children = parent_to_children[parent]
	if children then
		return children
	end
	
	parent_to_children[parent] = parent:GetChildren()

	local children = parent_to_children[parent]
	
	local Added = parent.ChildAdded:Connect(function(Child)
		table.insert(children, Child)
	end)

	local Removing = parent.ChildRemoving:Connect(function(Child)
		table.remove(children, table.find(children, child))
	end)

	parent.Destroying:Once(function()
		Added:Disconnect()
		Removing:Disconnect()
	end)
	
	return children
end


game:GetService("RunService").Heartbeat:Connect(function(deltaTime)

	for _, Child in pairs(getchildren_cached(MyFolder)) do

		local isNearby, HRP = isPlayerNearby(Child.PrimaryPart, 500)

		if not isNearby then					
			Child.Parent = storage
		end

		if Child:FindFirstChild("Props") and Child.Props:FindFirstChild("Tentacles") then
			local tentacles = getchildren_cached(Child.Props.Tentacles)
			for _, tentacle in pairs(tentacles) do
				coroutine.wrap(tentaclesAnimate)(tentacle)
			end
		end

		if Child:FindFirstChild("Props") and Child.Props:FindFirstChild("Eyeballs") then
			local eyeballs = getchildren_cached(Child.Props.Eyeballs)
			for _, eyeball in pairs(eyeballs) do
				coroutine.wrap(eyeballsAnimate)(eyeball)
			end
		end
	end

	for _, Child in pairs(getchildren_cached(storage)) do

		local isNearby1 = isPlayerNearby(Child.PrimaryPart, 500)
		local isNearby2 = isPlayerNearby(Child.PrimaryPart, 250)


		if isNearby1 or isNearyby2 then					
			Child.Parent = MyFolder
		end
	end


end)

aside from that you should also implement what the others here suggested

2 Likes

You’re right that you can’t tile a sphere with a 2-grid. But you can still use a 3D grid, just voxelizing all of space.

You can still do distance-to-player checks on a large planet, so long as your “near me” radius is much less than the planet radius. If you need to handle really small planets, you’d probably switch to using a cone, where something is within the range of the player if (plant.pos - planetCenter).Unit:Dot(player.pos - planetCenter).Unit is above some threshold value (cosine of the cone half-angle). Dot products are the same cost as a distance-squared test, the comparison test is more costly than the actual calculation.

1 Like

I honestly think the best way to improve performance is to add multi-threading for chunk loading no?
and loading animations on the client is definitely preferred.

I was able to do multi-threading with a noise based generation system and the performance is crazy good.

also I think one thing you could do is just take player position and map each chunk/voxel to a dictionary
and just load that from the server with multithreading

personally ^ this is my solution in general
heartbeat > while
and of course there is always something to be improved with math / loading stuff into the world
but I would work on that after getting mapping and multithreading to work.

1 Like

Alright, this sounds really good, and I did some research, but I cant figure out how to do it. Could I see some example code or resources I could learn from?

No. Unfortunately, the kind of chunk loading we’re talking about here typically involves moving things (Parts, Models) around in the Workspace, or showing and hiding them. All of the relevant properties and methods for Parts and Models are Read Parallel, and not safe to write from multi-threaded code. If there was expensive logic required to figure out if parts are in or out of view, that aspect could potentially be parallelized, but this problem is usually solved much better by spatial partitioning to minimize how many calculations get done, rather than by brute forcing it by throwing more threads at the problem.

There are some obvious and unnecessary costs you can avoid though. Creating and destroying instances is super expensive; WAY more expensive than moving parts around in the world or hiding them. A chunkloader should try to re-use allocated memory as much as possible. If your chunkloader is calling Clone() and Destroy() as part of loading and unloading, you likely have HUGE room for performance improvement just by re-using and pooling. For example: if you want to show some grass or bushes in the vicinity of your character, when they move to a new area don’t Destroy() all the plants in the area they are leaving, just move them into the new area (re-use them). Make pools of things like plants and grass that you can simply move around.

It’s OK to hide something by moving it far away, if there is a place that is out of view. Just don’t make the mistake of trying to hide something at some big, negative Y value, forgetting that there is a kill plane set by Workspace.FallenPartsDestroyHeight Moving things far away with Model:PivotTo() can be faster than making things Transparency=1, especially if you have things made out of thousands of individual Parts you’ve need to loop through. You can also hide stuff by setting Parent to nil, just be aware that this has consequences for some types of things, like player characters and things with running scripts inside them, and anything you’re not holding any strong reference to from Lua code could get garbage collected if Parent is nil.

1 Like

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