Optimizing moving multiple large chunks in a map

  1. What do you want to achieve?
    My goal is to create an infinite scrolling map that creates the illusion of movement on a train.

  2. What is the issue?
    Performance is struggling to keep up with moving these large chunks every frame and I’m trying to figure out how to reduce frame times as much as possible.

  3. What solutions have you tried so far?
    I’ve tried switching from :PivotTo() to :BulkMoveTo() and has given me negligible increase.

Frame times average 20.43ms, usually moving each chunk costs about 3-4ms and theres 3 chunks.

Chunks can vary in the content that they contain, some have forests, others are tunnels and some are just grass and tracks.

--... this is a snippet of the code I am using, most parts of this script are removed as they do not relate to the issue (probably)
RunService.RenderStepped:Connect(function(dt)
	worldOffset += speed * dt

	if worldOffset >= chunkLength then
		worldOffset -= chunkLength

		local first = table.remove(chunks, 1)
		table.insert(chunks, first)

		task.defer(function()
			debug.profilebegin("[CHUNK] Iterate Chunk")
			iterateChunk(first)
			debug.profileend()
		end)
	end

	debug.profilebegin("[CHUNK] Chunk RenderFrame")

	for i, chunk in ipairs(chunks) do
		local x = originX + (i - 1) * chunkLength - worldOffset
		local targetCF = CFrame.new(x, baseY, baseZ)

		local pivot = chunk:GetPivot()

		local parts = {}
		local cframes = {}

		for _, descendant in ipairs(chunk:GetDescendants()) do
			if descendant:IsA("BasePart") then
				table.insert(parts, descendant)

				-- preserve relative offset from pivot
				local relative = pivot:ToObjectSpace(descendant.CFrame)
				table.insert(cframes, targetCF * relative)
			end
		end

		workspace:BulkMoveTo(parts, cframes, Enum.BulkMoveMode.FireCFrameChanged)
	end

	debug.profileend()
end)

Thank you for your help!

1 Like

I don’t think there’s much here to optimize algorithmically, but you should definitely try these:

  1. Use native codegen (--!native or @native)
  2. Drop expensive table shifting operations in favor of a fixed layout with a wrapping index
  3. Drop ipairs
  4. Disable streaming (if enabled)
3 Likes

If your moving multiple chunks every frame, maybe try moving chunks in separate threads, and not in one single thread.

I haven’t used this stuff before since I’ve never needed too, so I don’t know exactly how to use it, but this page should explain how it works:
https://create.roblox.com/docs/scripting/multithreading

This should hopefully allow all of the chunks to move at the same time, instead of one moving after another.

I hope this helps!

1 Like

Why not just :PivotTo(targetCF) on the entire model? It looks like you are trying to move all the parts to a new place preserving the original offsets which is the same as just moving the model

If you have to do it that way then you should put all of the basepart descendants in a table that maps chunk to array of basepart when a chunk is made instead of recalculating them each frame, and now the loop inside of renderstepped can iterate through a clean array of parts without of checking :IsA or going through unecessary stuff

1 Like

Unfortunately multi-threading does not allow for the movement of objects as BulkMoveTo nor PivotTo is parallel safe.

Originally, that was what I was doing, and BulkMoveTo was my attempt to try and save on frame times (ended up contributing negligibly saving around ~0.5ms). I have attached that code for reference.

for i, chunk in ipairs(chunks) do
		local x = originX + (i - 1) * chunkLength - worldOffset

		chunk:PivotTo(CFrame.new(
			x,
			baseY,
			baseZ
			))
	end

As for now, I will mark xnlogical’s post as the solution as I do not believe there is anything more that can be done to optimize the code as is without changing the way my game works.

Thank you everyone for their help!

It should be noted that you’re doing chunk:GetDescendants() in your original loop, per chunk, every time you move them. The overhead of that is likely contributing to how slow this loop is; Instance:GetDescendants() does not cache, so it has to create a new array containing every instance within that chunk every single time.

I am aware this is solved, but have you considered turning your chunks into MeshParts? You can’t exactly move massive assemblies chock full of parts every frame and expect it to run very well, so the next best solution is to simply move less.
I’m unsure what your chunks are supposed to look like (or what happens in them), but you can likely cut down on how many instances you’re moving at once by exporting your chunk into gLTF or OBJ, merging whichever parts have the same material and color into the same mesh (e.g. some terrain or perhaps a lot of trees), then re-exporting it back to Roblox.
Technically, you’d be moving way less parts per frame, since they’d all be meshed into a few parts (kind of like unions, I suppose). While it’s unfortunately a little gross, you can’t really improve performance in any other way

2 Likes