So in one of my games items are loaded in for like 5 different areas. obviously the player is in 1 area at a time, but the “items” are about 1500 parts so performance is quite the toll
What i need is someone to potentially recommend a way of doing something like stream instance but for 1 folder (the items are located in 1 folder in workspace)
i’ve tried searching around but ive heard that streaming enabled does rendering for all items in workspace.
I can’t link you to a specific post, but you might want to look into things like back-face and/or view-frustum culling; these are culling algorithms that stop things not in the player’s field of view from being rendered. Someone has probably made a module or something that can immediately be added to your game. You might also want to look into chunking, kind of like what you’re already doing except smaller; just load objects in chunks that are within a certain radius of the player (a great and very popular example of this is Minecraft.)
Just doing that doesn’t change very much (as far as I know), consider unparenting them from the workspace and instead keeping unseen/unused chunks in ReplicatedStorage and then re-parenting them to workspace when they’re within a certain radius of the player.
-- Constants
local ITEMS_FOLDER = workspace:WaitForChild("Items")
local PLAYER = game.Players.LocalPlayer
local RENDER_DISTANCE = 20
local function moveToPartsFolder(partsFolder)
for _, part in ipairs(ITEMS_FOLDER:GetChildren()) do
if part:IsA("BasePart") then
part.Parent = partsFolder
part.Transparency = 1
part.CanCollide = false
end
end
end
local function updateVisibility()
local character = PLAYER.Character
if not character then return end
local playerPos = character:FindFirstChild("HumanoidRootPart") and character.HumanoidRootPart.Position
if not playerPos then return end
local partsFolder = game.ReplicatedStorage:WaitForChild("Parts")
for _, part in ipairs(partsFolder:GetChildren()) do
if part:IsA("BasePart") then
local distance = (part.Position - playerPos).Magnitude
if distance <= RENDER_DISTANCE then
part.Transparency = 0
part.CanCollide = true
part.Parent = ITEMS_FOLDER
else
part.Transparency = 1
part.CanCollide = false
part.Parent = partsFolder
end
end
end
end
PLAYER.CharacterAdded:Connect(function(character)
moveToPartsFolder(game.ReplicatedStorage:WaitForChild("Parts"))
end)
game:GetService("RunService").Stepped:Connect(updateVisibility)
now this works DECENTLY well, the parts are initially put in rs and then drawn as the player appproches. but when the player approaches their location it has a frame drop and then spawns in the parts. do I add some kind of delay?
Well, first I would get rid of both the part.Transparency and part.CanCollide calls on both the unrendering and re-rendering as doing that to an unloaded part doesn’t do anything but make it slower.
different areas spawn diff items. so im lowkey gonna get a table down for each area and when they change areas, unload all the other areas and load specific area.
You’re still going to have to implement a system that loads closer objects first as setting a folder full of potentially hundreds of parts to the workspace isn’t going to fare very well.
I would recommend using meshes made in 3d modeling software. Unions don’t count - they make performance worse most of the time. I understand that there could be a huge time investment in doing this and it might not be an option, but it is worth considering
You can do something like chunking to optimize, but something like occlusion culling might need some internal tool that the developer can’t access. But there is this tool Software Occlusion Culling | Roblox Vis Tools that advertises occlusion culling but I can’t confirm because it is a paid tool. Although this feature will be added soon, as long according to the roadmap.
Frustum Culling has already been implemented in Roblox.
I think you can just set the CFrame of the part somewhere that is far from the player, that way the engine won’t render it. Putting it in Replicate Storage is not be performant as doing part cache. Setting part.Transparency and CanCollide are not good because setting transparency is for some reason, very laggy.
I couldn’t find my old solution so I hastily made this:
-- // CONSTANTS \\ --
local RENDER_DISTANCE = 100
local player = game.Players.LocalPlayer
local character = game.Players.LocalPlayer.Character or player.CharacterAdded:Wait()
local humanoidRootPart = character:WaitForChild("HumanoidRootPart")
---------------------
-- // services \\ --
local runService = game:GetService("RunService")
--------------------
-- // folders \\ --
local unloadedObjectsFolder = game.ReplicatedStorage.UnloadedObjectsFolder
local loadedObjectsFolder = game.Workspace.LoadedObjectsFolder
-------------------
-- // main \\ --
-- Checks to make sure we have a BasePart object so we can compare positions to find the distance of the model.
local function FindObjectPrimaryPart(object)
if object:IsA("Model") then
local root = object
-- We add a pcall to stop the script from stopping if it errors
local success, error = pcall(function()
object = object.PrimaryPart or object:FindFirstChildWhichIsA("BasePart")
end)
if success then
return object, root
else
warn(error, "Model: "..object.Name.." does not have a valid child or PrimaryPart, please change/add a PrimaryPart to the model")
end
return object
end
return object
end
-- Do this everytime the player respawns to make sure objects outside of the range don't take up precious resources.
local function ResetLoadedObjects()
for _, object in pairs(loadedObjectsFolder:GetChildren()) do
if object:IsA("BasePart") or object:IsA("Model") then
object.Parent = unloadedObjectsFolder
end
end
end
local function CullObject(object, isUnloading)
local object, root = FindObjectPrimaryPart(object)
local distanceFromPlayer = (object.Position - humanoidRootPart.Position).Magnitude
local shouldCull = distanceFromPlayer > RENDER_DISTANCE
-- makes sure we're getting either render or unrender
if shouldCull == isUnloading then
-- makes sure that we're not setting the parent of just the part but instead the whole model (if there is one).
if root then
object = root
end
object.Parent = isUnloading and unloadedObjectsFolder or loadedObjectsFolder
end
end
-- Obviously the update loop.
local function Update(deltaTime)
-- rendering
for _, object in ipairs(unloadedObjectsFolder:GetChildren()) do
CullObject(object, false)
end
-- unrendering
for _, object in ipairs(loadedObjectsFolder:GetChildren()) do
CullObject(object, true)
end
end
runService.RenderStepped:Connect(Update)
player.CharacterAdded:Connect(function()
ResetLoadedObjects()
end)
----------------
It’s not the best, and in my haste to make this, I used some weird techniques to pick between the loaded folder and the unloaded folder, but it does work (although I can’t guarantee how much it fixes the lag).
There are most certainly some underlying problems, like the fact that looping through two tables that could have hundreds, if not thousands, of objects in each will be quite slow. So I would recommend splitting the map into chunks during runtime (via code), saving the chunks in a table, and only picking the chunk that the player is in (and maybe their neighboring ones) for the loop.
Edit: I just stress tested it;
With 4160 objects, I reached a script activity of 24.13% ~ 30.41%, which is quite high, but I experienced mostly 0 delay on my objects rendering/unrendering even with the full 4160 objects (unless I had a high render distance).
Video of test:
Sorry if it looks bad, it was more than 10MB so I had to compress it.
There is some minor lag when loading the parts at the start, but that is because it is 4000 parts, 2000 duped inside of each other.