Need help optimizing map loader

Hello! I am working on a system that can load in huge maps for my game.
Currently it takes around 3:30 - 4:00 minutes to load in a full map which is a pretty long time.
I am wondering if anyone has any suggestions and ideas on how I can further optimize this.

The script first loads in the nature, structures then misc, all from their folders, at the end it will load the terrain in, here you have an image to visualize it.
image

code:

function MapFunctions.Load(map)
	local NatureFolder = map.Assets.Nature
	local StructureFolder = map.Assets.Structures
	local MiscFolder = map.Assets.Misc
	local TerrainRegion = map.RBLXterrain:FindFirstChildOfClass("TerrainRegion")
	
	local i = 0
	local MaxIterationsUntilWait = 100
	local StuffToLoad = NatureFolder:GetChildren()
	
	for _, v in ipairs(StuffToLoad) do
		if i >= MaxIterationsUntilWait then
			i = 0 
			RunService.Heartbeat:Wait()
		else
			i += 1
		end
		
		local clone = v:Clone()
		clone.Parent = WorkspaceMapFolder
		RunService.Heartbeat:Wait()
	end
	
	StuffToLoad = StructureFolder:GetChildren()
	i = 0
	for _, v in ipairs(StuffToLoad)  do
		if i >= MaxIterationsUntilWait then
			i = 0 
			RunService.Heartbeat:Wait()
		else
			i += 1
		end

		local clone = v:Clone()
		clone.Parent = WorkspaceMapFolder
	end
	i = 0
	
	StuffToLoad = MiscFolder:GetChildren()
	for _, v in ipairs(StuffToLoad) do
		if i >= MaxIterationsUntilWait then
			i = 0 
			RunService.Heartbeat:Wait()
		else
			i += 1
		end

		local clone = v:Clone()
		clone.Parent = WorkspaceMapFolder
	end
	
	

	local position = Vector3int16.new(
		-math.floor(TerrainRegion.SizeInCells.X / 2),
		-math.floor(TerrainRegion.SizeInCells.Y / 2),
		-math.floor(TerrainRegion.SizeInCells.Z / 2)
	)

	workspace.Terrain:PasteRegion(TerrainRegion, position, true)

	local waterProps = TerrainRegion:FindFirstChild("WaterProperties")
	if waterProps then
		LoadTerrainProperty(TerrainRegion, "WaterColor")
		LoadTerrainProperty(TerrainRegion, "WaterReflectance")
		LoadTerrainProperty(TerrainRegion, "WaterTransparency")
		LoadTerrainProperty(TerrainRegion, "WaterWaveSize")
		LoadTerrainProperty(TerrainRegion, "WaterWaveSpeed")
	end

	local materialColors = TerrainRegion:FindFirstChild("MaterialColors")
	if materialColors then
		for _,material in Enum.Material:GetEnumItems() do
			local colorVal = materialColors:FindFirstChild(material.Name)
			if colorVal then
				AttemptSetMaterialColor(material, colorVal.Value)
			end
		end
	end
end
1 Like

I recommend using octrees or some chunk system. With octrees, you can load what’s next to you and unload what’s far away. It’s very cheap to get the distance too because you can just check the parent nodes distance. If you do have to load everything try loading everything in parallel by splitting it into different parts and making each actor load those in. For example, have one actor load in nature, another structure, etc. I haven’t used parallel much so I can’t help you with it but I have seen people improve loading times for things like this with parallel. So maybe try experimenting with parallel.

function MapFunctions.Load(map)
	local NatureFolder = map.Assets.Nature
	local StructureFolder = map.Assets.Structures
	local MiscFolder = map.Assets.Misc
	local TerrainRegion = map.RBLXterrain:FindFirstChildOfClass("TerrainRegion")

	local StuffToLoad = map.Assets:GetDescendants()

	-- Iterate over the contents of all three folders
	for _, v in ipairs(StuffToLoad) do
		local clone = v:Clone(true)
		clone.Parent = WorkspaceMapFolder
		RunService.Heartbeat:Wait()
	end

	local position = Vector3int16.new(
		-math.floor(TerrainRegion.SizeInCells.X / 2),
		-math.floor(TerrainRegion.SizeInCells.Y / 2),
		-math.floor(TerrainRegion.SizeInCells.Z / 2)
	)

	workspace.Terrain:PasteRegion(TerrainRegion, position, true)

	local waterProps = TerrainRegion:FindFirstChild("WaterProperties")
	if waterProps then
		LoadTerrainProperty(TerrainRegion, "WaterColor")
		LoadTerrainProperty(TerrainRegion, "WaterReflectance")
		LoadTerrainProperty(TerrainRegion, "WaterTransparency")
		LoadTerrainProperty(TerrainRegion, "WaterWaveSize")
		LoadTerrainProperty(TerrainRegion, "WaterWaveSpeed")
	end

	local materialColors = TerrainRegion:FindFirstChild("MaterialColors")
	if materialColors then
		for _, material in Enum.Material:GetEnumItems() do
			local colorVal = materialColors:FindFirstChild(material.Name)
			if colorVal then
				AttemptSetMaterialColor(material, colorVal.Value)
			end
		end
	end
end

Improvements:

  • Used the :GetDescendants() method instead of :GetChildren() to get all the children of Nature Folder, Structure Folder, and Misc Folder.
  • Used a single for loop to iterate over all 3 folders instead of using 3 separate loops.
  • Used the :Clone() method with the Create = true option to create the clones on the server, which will be faster and more efficient than creating them on the client and then transferring them to the server.
  • Other minor not-notable improvements.

I think you can remove the 2nd heartbeat wait. MaxIterationsUntilWait should have a value of around 512-1024 depending on your preference, but I think 100 is too small.

In addition, you have many duplicates of this code. This is due to multiple folders being used. You can use GetDescendants to load everything. However, this might cause issues due to some of the map assets being models. This brings me to the question, why can’t you clone it all at once, without cloning each individual asset? That would probably be much more performant.

Hey!

Thanks for the feedback, the reason I have 3 different folders is to seperate the nature, structures and anything besides that in misc.

Hey I can’t really use :GetDescendants() because then it will put all the children of the house models outside the model.

One optimization that could be made is to use a multi-threaded approach to load in the assets. Instead of waiting for each iteration to complete before moving on to the next one, you could create multiple threads to handle loading in the different folders in parallel. This would allow the assets to be loaded in faster, as each thread would be able to work on its own set of assets concurrently.

Another optimization that could be made is to use a cache system to store the assets that have already been loaded. This way, if the same map is loaded multiple times, the assets will not need to be loaded from the folders again, as they will already be stored in the cache. This can significantly speed up the loading process, especially for large maps.

Finally, you could consider using a progress bar or some other visual indicator to show the progress of the map loading. This can help to keep the player informed about how long the process is taking, and can also provide a sense of progress to keep them engaged.

A multi threaded approach sounds interesting and I may actually use this. But won’t I have to lower the MaxIterationsUntilWait value since it could probably overflow and make the small delay useless?

If you are using a multi-threaded approach, you may need to lower the MaxIterationsUntilWait value to ensure that the thread does not become overloaded. This is because the MaxIterationsUntilWait value determines how many iterations of the loop the thread will run before it waits for a small amount of time. If the MaxIterationsUntilWait value is too high, the thread may become overloaded with too many iterations and may not be able to function properly.

However, you should be careful when lowering the MaxIterationsUntilWait value, as it may also increase the amount of processing power the thread uses. You should try to find a balance between the two to ensure that the thread is efficient and does not cause performance issues.

It is also important to note that the MaxIterationsUntilWait value is not directly related to the issue you are experiencing with the CFrame/Position of the parts not updating. It is simply a tool that can be used to improve the performance of a multi-threaded script.