Please Help With Optimization!

So my map is the recreation of the infinity castle. dynamic gravity, movement, and precedural generation using a seed (no 2 servers have the same layout)

The code for the map generation is optimized and the map takes 10 seconds to generate.

The gravity change script is optimized perfectly too using a chunk based system

The map uses these assets:

4 Different Walls
4 Different Doorways
2 Different Floors
1 Roof
2 Different Pillars
1 Fence
1 Item (Lantern)
1 Stair

This is used to generate the map itself

However its extremely laggy, Looking away from the map into the void gives me 230 Fps and looking upwards to the map from the bottom puts that down to 80-90 fps

Any tips on optimization?
I tried making my own chunk render system but it doesnt solve the issue

local MapControl = {}

local Update = game:GetService("RunService")
local Tween = game:GetService("TweenService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Collection = game:GetService("CollectionService")
local player = Players.LocalPlayer
local character = player.Character
local humanoid = character.Humanoid
local root = character.HumanoidRootPart
local remotes = ReplicatedStorage.Shared.Remotes
local handlers = script.Parent

local map = workspace:WaitForChild("Map")
local objects = ReplicatedStorage:WaitForChild("MapObjects")

local container = map:WaitForChild("Container")
local cubed_length = container.Size.Y

local room_length = 50


local rendered_chunks = {}

local grid = {}              -- chunk -> objects
local gridKeys = {}       
local batchedgridKeys = {}  -- array of chunk keys for batching
local rendered_chunks = {}

local batchSize = 35 -- number of chunks to process per frame
local batchIndex = 1   -- current position in gridKeys

MapControl.batchedObjects = {}
local objectupdateCheck = 0.1
local objectbatch = 1
local objectbatch_size = 25
local renderDistance = 5

function MapControl.RenderObjectByDistance()
	Update.PostSimulation:Connect(function()
		if #gridKeys == 0 then return end

		local pos = root.Position
		local cX,cY,cZ = math.floor(pos.X/room_length), math.floor(pos.Y/room_length), math.floor(pos.Z/room_length)

		for i = #batchedgridKeys, math.max(1,#batchedgridKeys-batchSize),-1 do
			local key = batchedgridKeys[i]
			local v = grid[key]
			if v then
				local split = string.split(key,":")
				local x,y,z = tonumber(split[1]), tonumber(split[2]), tonumber(split[3])
				if math.abs(x-cX) <= renderDistance and math.abs(y-cY) <= renderDistance and math.abs(z-cZ) <= renderDistance then
					if #v > 0 and not rendered_chunks[key] then
						rendered_chunks[key] = true
						coroutine.wrap(function()
						for _, v2 in ipairs(v) do
								if v2 and v2.Parent and v2.Parent ~= map and rendered_chunks[key] == true then
								v2.Parent = map
							end
						end
						end)()
					end
				else
					if #v > 0 and rendered_chunks[key] then
						rendered_chunks[key] = nil
						coroutine.wrap(function()
						for _, v2 in ipairs(v) do
								if v2 and v2.Parent and v2.Parent ~= ReplicatedStorage and rendered_chunks[key] == nil then
								v2.Parent = ReplicatedStorage
							end
						end
						end)()
				
					end
				end
			end
			table.remove(batchedgridKeys,i)
		end
		
		if #batchedgridKeys <= 0 then
			batchedgridKeys = table.clone(gridKeys)
		end

	end)
end

function MapControl.AddChunk(object)
	coroutine.wrap(function()
		task.wait()
		if object.PrimaryPart then
			local pos = object.PrimaryPart.Position
			local chunk = math.floor(pos.X/room_length)..":"..math.floor(pos.Y/room_length)..":"..math.floor(pos.Z/room_length)

			if not grid[chunk] then
				grid[chunk] = {}
				table.insert(gridKeys, chunk)
				rendered_chunks[chunk] = true
			end

			rendered_chunks[chunk] = true
		
			table.insert(grid[chunk], object)
			
		end
	end)()
end

function MapControl:BatchSorting()
	local timed = 0
	Update.PostSimulation:Connect(function(delta)

		timed += delta

		if timed >= objectupdateCheck then
			timed = 0
	
			for i = #MapControl.batchedObjects, math.max(1,#MapControl.batchedObjects-objectbatch_size), -1 do
				local object = MapControl.batchedObjects[i]
				if object and object.Parent then
					MapControl.AddChunk(object)
					table.remove(MapControl.batchedObjects,i)
				else
					table.remove(MapControl.batchedObjects,i)
				end
			end

		end

	end)
end

for _, object in pairs(Collection:GetTagged("DistancedRender")) do
	if object and object.Parent then
		table.insert(MapControl.batchedObjects,1,object)
	end
end

Collection:GetInstanceAddedSignal("DistancedRender"):Connect(function(object)
	table.insert(MapControl.batchedObjects,1,object)
end)

MapControl:BatchSorting()

MapControl.RenderObjectByDistance()

return MapControl

Replace parts that make a pattern with textures. Also make sure your streaming settings are set up correctly or make your own LOD system.

There’s also this post:

but I’m not sure how helpful that will be for you since your entire map is visible most of the times from the looks of it which means that you can’t really use a lot of optimization techniques.

not sure if this is good or not but my rendering system does this and improves the fps to 140, but it sacrifices the look

I wish i could maintain 200 fps though

Not sure what your specs are, but you gotta account for low end devices as well.

thats true, these are my specs

1 Like

Assuming meshes are used anywhere, setting the CollisionFidelity of meshes that players can’t or are not supposed to interact with to “Box”, things like walls and floors.

“Hull” can also work on some meshes the player can interact with. You can try using it for stuff like bridges.

Try to avoid precise as much as possible and use Default instead.

By the way, this looks incredible, you should totally let me try this

1 Like

Try:

  • Reducing triangles
  • Reduce draw calls (to do this, reuse meshes, textures, decals, with the same appearance configuration)
  • Reduce threads and/or scripts
  • Disable shadows, collisions, and touch events where not needed
  • Combine light sources to reduce lighting calculations
  • Disable transparency where unnecessary
  • Reduce collision geometry where complex geometry is unnecessary (@TimeFrenzied)

Doing any of these can improve performance by reducing GPU calls, CPU time, or memory.

Additionally, using the microprofiler could help you pinpoint code segments that are taking a lot of time. If you optimize those tasks, your game will run smoother!

1 Like

I dont know how exactly I could help with optimization, but I can list basic tips to increase perfomance.

  1. Use MicroProfile to manage your game render. Use debug libary to manage exact parts of code.

  2. Check collision fidelity. Change fidelity to Haul or Box of parts player dont interacte with. Its basic, but you could render fewer parts of instances that are far away from players character.

In your case, you might have also memory issue. Check memory usage in developer console by pressing f9

@Dexilont @avodey @TimeFrenzied

hey so for all the meshparts, collision fidelity is Box

test game:

1 Like

Use generic iteration over pairs/ipairs because next and inext are pretty badly made internally compared to newer alternative (generic iteration): for i,v in {} do end

Set meshes to have render fidelity to be Perfomance.
Use materials over textures
Don’t create a method like function module:Hello() if you never use self, rely on just functions.
Put --!optimize 2 (although it will still be forced to optimize 2 outside of Studio, it’s still worth enabling for playtesting) use parallel Luau for map generation and use native code generation ontop of parallel.

Try writing code with –!strict to ensure it is easily manageable and possibly more optimized.
Look at the microprofiler as to what is causing the most lag.

Avoid creating closures or “unneeded threads” like seen here:

the code is extremely optimized, link is shown above, seems to be the map itself, any scripting help i need would be in the form of creating a chunk render system

will @native help?

I joined the test place and the MicroProfiler tells me that something you’re doing in PreSimulation is taking up a big portion of frame time.

If I had to assume, it’s the constantly moving parts of the city. If you could, try using Workspace:BulkMoveTo() since it can provide some significant performance improvements. Though, outside of the code, check on your draw calls every once in a while:

From what I see, a majority of the performance issues come from just how much you’re rendering. I have a very beefy computer so it runs fine for me, but on low-end devices, they’d be choking. I’m consulting MrChickenRocket’s budget from his RDC 2024 talk:

While your scene draw count is under 500, you’re rendering more than 5 million triangles, which is literally 10x what MrChickenRocket suggests.

You need to figure out an LOD system that doesn’t sacrifice too much visual fidelity while making sure the game isn’t rendering millions of triangles at once (at second glance, I see that this was mentioned earlier but I skimmed the post, oops). And, if possible, see if you can check if any moving parts are within the view frustum and use that information to determine whether they will be moved via BulkMoveTo as suggested above.

2 Likes

hey, how else do you think i can achieve a lower tri count?

also how can i use BulkMoveTo to move models?

Instead of calling PivotTo on every model, instead make two tables, one with all the models you want to move on that frame and the other with all the CFrames you want to move them to like so

workspace:BulkMoveTo(models, cframes, Enum.BulkMoveMode.FireCFrameChanged)

Also, another suggestion, whenever you are loading and unloading pretty much anything, be that parts of the map or custom particles, instead of destroying the old ones and cloning new ones from a preset, add the old ones to a pool (just a table which holds all the old ones) and parent them to, for example, ReplicatedStorage, and load the new ones in from the pool if any applicable objects are in the pool.

So some pseudocode would look like this

local function Unload(model)
    model.Parent = ReplicatedStorage -- Parent the model to somewhere outside workspace to avoid rendering and physics
    table.insert(pool[model.Name], model) -- Add the model to the corresponding pool
end

local function Load(preset, cframe)
    local model
    local modelPool = pool[preset.Name] -- Get the corresponding pool
    if #modelPool > 0 then -- If there are any corresponding objects in the pool
        model = modelPool[#modelPool] -- Get the last one
        table.remove(modelPool, #modelPool) -- Remove the last one from the pool, since it is now in use
    else -- No object in the pool
        model = preset:Clone()
    end

    model:PivotTo(cframe) -- BulkMoveTo may be better here as well
    model.Parent = workspace
end

Pooling is, as far as I’m aware, always a better option for things like this, since it bypasses the overhead of creating and destroying new instances.

2 Likes

well my chunk system does do that if you read it, it improved performance by 40 fps, going from 90 - 130, but i still wish i could hit 200, are you sure BulkMoveTo works on models?, i thought it was only parts

1 Like

Absolutely, BulkMoveTo works on anything that you can call :PivotTo on.

1 Like

thanks man, i wish i could give multiple people the solution

i decided on lowering my tri count by using cubes and textures instead of detailed meshes, cutting down a walls tri count from 1050 to 12

Detailed Mesh:

Cube with Faced Textures:

Not only will i be doing this

But i will use BulkMoveTo to move my platforms

I wont give myself the solution so ill give the person i think deserved it the most

1 Like