Game optimisation for high part lag

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.

Would appreciate some help.

7 Likes

Do you perhaps consider combining parts as a union, or porting them to blender and simplify them as meshes?

3 Likes

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

1 Like

quite a lot are meshes / unions tbh. its like a 7/11 but a few different maps and they got like a bunch of items on the shelves

3 Likes

I have also tried making distant objects transparencies and can collides off. doesnt help

1 Like

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.

1 Like

indeed. putting them in replicatedstorage stops the parts from being drawn.

2 Likes

i actually did something similar but stored them in the character for some weird reason. let me try that

1 Like
-- 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?

3 Likes

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.

1 Like

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.

1 Like

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’m trying super hard to find an old place that I built a object loader thingy on.

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

2 Likes

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.

1 Like

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

Edit: I cleaned up the code a little bit.

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.

2 Likes