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)
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.
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.
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?
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
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.
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.
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.
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
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.
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.
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.