Need help with optimizing world rendering

I made world generation, and now I need to render it.
The issue I encountered is that rendering the entire world at once is going to be painfully slow, plus this times out the script. My map is 200x200, but only because it is in test version.
Then, I decided to make world rendering be similiar to minecraft, instead of rendering the entire world, I will slowly load the world in as the player walks around. Though this causes lag spikes.


I wonder how can I fix it, or maybe I need another way of rendering the world.

My script:

local Players = game:GetService("Players")
local SS = game:GetService("ServerStorage")
local RunService = game:GetService("RunService")

local blocksFolder = SS:WaitForChild("Blocks")
local blocks = {
	Grass = blocksFolder:WaitForChild("Grass"),
	Dirt = blocksFolder:WaitForChild("Dirt"),
	Stone = blocksFolder:WaitForChild("Stone"),
	Bedrock = blocksFolder:WaitForChild("Bedrock")
}
local nearestBlockPosses = {}
local blockSize = 3
for i = 0, 9 do
	table.insert(nearestBlockPosses, i*blockSize)
end

local blocksWorkspace = workspace:WaitForChild("Blocks")

local X = 200
local Z = 200
local smoothness = 25

local grid = {}

for x = -(X/2), X/2 do
	grid[x] = {}
	task.spawn(function()
		for z = -(Z/2), Z/2 do
			local y = math.floor(math.noise(x/smoothness, z/smoothness) * 15)
			y += blockSize*10
			repeat y -= blockSize*10 until y < blockSize*10
			local nearestPos = {0, math.huge}
			for i = 1, #nearestBlockPosses do
				local n = nearestBlockPosses[i]
				local difference = math.abs(math.abs(y)-n)
				if difference < nearestPos[2] then
					nearestPos[1] = n
					nearestPos[2] = difference
				end
			end
			y = nearestPos[1]
			grid[x][z] = y
		end
	end)
end

local generatedCords = {}
local function generateAroundPos(position:Vector3, radius:number)
	for x = (position.X - math.round(radius/2)), (position.X + math.round(radius/2)) do
		task.spawn(function()
			for z = (position.Z - math.round(radius/2)), (position.Z + math.round(radius/2)) do
				local xgrid = grid[x]
				if not xgrid then continue end
				local y = xgrid[z]
				if not y then continue end
				if table.find(generatedCords, tostring(x).."|"..tostring(z)) then continue end

				local block = blocks["Grass"]:Clone()
				block.Position = Vector3.new(x*blockSize, y, z*blockSize)
				block.Parent = blocksWorkspace
				task.spawn(function()
					for i = y-blockSize, -(blockSize*3), -blockSize do
						local dirt = blocks["Dirt"]:Clone()
						dirt.Position = Vector3.new(x*blockSize, i, z*blockSize)
						dirt.Parent = blocksWorkspace
					end
					for i = -(blockSize*4), -(blockSize*6), -blockSize do
						local stone = blocks["Stone"]:Clone()
						stone.Position = Vector3.new(x*blockSize, i, z*blockSize)
						stone.Parent = blocksWorkspace
					end
					local bedrock = blocks["Bedrock"]:Clone()
					bedrock.Position = Vector3.new(x*blockSize, -(blockSize*7), z*blockSize)
					bedrock.Parent = blocksWorkspace
				end)
				table.insert(generatedCords, tostring(x).."|"..tostring(z))
			end
		end)
	end
end
generateAroundPos(Vector3.new(0, 0, 0), 50)

local last_position = {}
local function onPlayer(plr:Player)
	last_position[plr] = Vector3.new(0, 0, 0)
end

Players.PlayerAdded:Connect(onPlayer)

for i, v in ipairs(Players:GetChildren()) do
	onPlayer(v)
end

RunService.Stepped:Connect(function(t, dt)
	for i, plr:Player in ipairs(Players:GetChildren()) do
		local char = plr.Character
		if not char then continue end
		local root:BasePart = char:FindFirstChild("HumanoidRootPart")
		if not root then continue end
		local last_pos = last_position[plr]
		if not last_pos then continue end
		
		local pos = Vector3.new(math.round(root.Position.X/blockSize), 0, math.round(root.Position.Z/blockSize))
		if (pos - last_pos).Magnitude > blockSize then
			last_position[plr] = pos
			task.spawn(function()
				generateAroundPos(pos, 50)
			end)
		end
	end
end)

Thanks in advance.

3 Likes

This sounds like it’d be relevant. Sorry I couldn’t provide much help.

2 Likes

It still is like hella laggy. Even worse than before.


edit: It is impossible to see on the video itself, but FPS drops significantly. You can also notice that video occassionally speeds up.

Global Script:

local Players = game:GetService("Players")
local SS = game:GetService("ServerStorage")
local RunService = game:GetService("RunService")

local RS = game:GetService("ReplicatedStorage")
local events = RS:WaitForChild("RemoteEvents")
local render = events:WaitForChild("Render")

local chunks = SS:WaitForChild("Chunks")
local blocksFolder = SS:WaitForChild("Blocks")
local blocks = {
	Grass = blocksFolder:WaitForChild("Grass"),
	Dirt = blocksFolder:WaitForChild("Dirt"),
	Stone = blocksFolder:WaitForChild("Stone"),
	Bedrock = blocksFolder:WaitForChild("Bedrock")
}
local nearestBlockPosses = {}
local blockSize = 3
for i = 0, 9 do
	table.insert(nearestBlockPosses, i*blockSize)
end

local blocksWorkspace = workspace:WaitForChild("Blocks")

local X = 200
local Z = 200
local smoothness = 25

local grid = {}

for x = -(X/2), X/2 do
	grid[x] = {}
	task.spawn(function()
		for z = -(Z/2), Z/2 do
			local y = math.floor(math.noise(x/smoothness, z/smoothness) * 15)
			y += blockSize*10
			repeat y -= blockSize*10 until y < blockSize*10
			local nearestPos = {0, math.huge}
			for i = 1, #nearestBlockPosses do
				local n = nearestBlockPosses[i]
				local difference = math.abs(math.abs(y)-n)
				if difference < nearestPos[2] then
					nearestPos[1] = n
					nearestPos[2] = difference
				end
			end
			y = nearestPos[1]
			grid[x][z] = y
		end
	end)
end

local function createChunk(position:Vector3, radius:number)
	local existingChunk = chunks:FindFirstChild(`chunk#{tostring(position.X)}|{tostring(position.Z)}`)
	if existingChunk then
		warn(`Chunk with position {tostring(position)} already exists`)
		return existingChunk
	end
	local chunk = Instance.new("Folder")
	chunk.Name = `chunk#{tostring(position.X)}|{tostring(position.Z)}`
	chunk.Parent = chunks
	for x = (position.X - math.round(radius/2)), (position.X + math.round(radius/2)) do
		for z = (position.Z - math.round(radius/2)), (position.Z + math.round(radius/2)) do
			local xgrid = grid[x]
			if not xgrid then continue end
			local y = xgrid[z]
			if not y then continue end

			local block = blocks["Grass"]:Clone()
			block.Position = Vector3.new(x*blockSize, y, z*blockSize)
			block.Parent = chunk
			task.spawn(function()
				for i = y-blockSize, -(blockSize*3), -blockSize do
					local dirt = blocks["Dirt"]:Clone()
					dirt.Position = Vector3.new(x*blockSize, i, z*blockSize)
					dirt.Parent = chunk
				end
				for i = -(blockSize*4), -(blockSize*6), -blockSize do
					local stone = blocks["Stone"]:Clone()
					stone.Position = Vector3.new(x*blockSize, i, z*blockSize)
					stone.Parent = chunk
				end
				local bedrock = blocks["Bedrock"]:Clone()
				bedrock.Position = Vector3.new(x*blockSize, -(blockSize*7), z*blockSize)
				bedrock.Parent = chunk
			end)
		end
	end
	return chunk
end

local function generateChunk(position:Vector3)
	if blocksWorkspace:FindFirstChild(`chunk#{tostring(position.X)}|{tostring(position.Z)}`) then
		warn(`Chunk with position {tostring(position)} is already rendered`)
		return
	end
	local renderingChunk = chunks:FindFirstChild(`chunk#{tostring(position.X)}|{tostring(position.Z)}`)
	if not renderingChunk then
		warn("Failed to render in chunk: no chunk with position "..tostring(position))
		return
	end
	local copy = renderingChunk:Clone()
	copy.Parent = blocksWorkspace
end

createChunk(Vector3.new(0, 0, 0), 20)
generateChunk(Vector3.new(0, 0, 0))

local last_position = {}
local function onPlayer(plr:Player)
	plr.CharacterAppearanceLoaded:Connect(function(char)
		local rootPart:BasePart = char:WaitForChild("HumanoidRootPart")
		last_position[plr] = Vector3.new(
			math.round(rootPart.Position.X/blockSize),
			0,
			math.round(rootPart.Position.Z/blockSize)
		)
	end)
end

Players.PlayerAdded:Connect(onPlayer)

for i, v in ipairs(Players:GetChildren()) do
	onPlayer(v)
end

local chunkSize = 16
RunService.Stepped:Connect(function(t, dt)
	for plr:Player, pos:Vector3 in last_position do
		local char = plr.Character
		if char then
			local root:BasePart = char:FindFirstChild("HumanoidRootPart")
			if root then
				local currPos = Vector3.new(
					math.round(root.Position.X/blockSize),
					0,
					math.round(root.Position.Z/blockSize)
				)
				if (currPos - pos).Magnitude > blockSize then
					last_position[plr] = currPos
					local createdChunk = createChunk(currPos, chunkSize)
					local chunkCopy = createdChunk:Clone()
					chunkCopy.Parent = plr.Backpack
					render:FireClient(plr, chunkCopy)
					render.OnServerEvent:Wait()
				end
			end
		end
	end
end)

Local Script:

local Players = game:GetService("Players")
local player = Players.LocalPlayer

local RS = game:GetService("ReplicatedStorage")
local events = RS:WaitForChild("RemoteEvents")
local render = events:WaitForChild("Render")

local blocks = workspace:WaitForChild("Blocks")

local function fireBack(event:RemoteEvent)
	event:FireServer()
end

render.OnClientEvent:Connect(function(chunk:Folder)
	for i, v in ipairs(blocks:GetChildren()) do
		if v:IsA("Folder") then
			v:Destroy()
		end
	end
	chunk.Parent = blocks
	fireBack(render)
end)
1 Like

I’m not too experienced in loading maps and stuff, but some things you could do which i think could help would be

  • Chunk loading
    Loading the maps in chunks like Minecraft does it could probably help. It would still cause a lag spike the first time you load new chunks but you can probably not do anything about that and keep it efficient.

  • Unloading chunks
    Loading them in chunks will also obviously allow you to unload chunks too far away

  • Unloading invisible blocks
    Unload blocks that aren’t visible, you still generally wanna keep a layer of blocks that aren’t visible to keep the lighting working properly, but anything more could probably be safely unloaded.

these are the most important things i could come up with. Wish you luck

I’m a little confused why you parent the chunks to the player’s backpack.

I’m not really experienced in loading maps, but you should parent any unloaded chunks to replicated storage. Also, are you generating the chunks on the server? If so, maybe consider doing it on the client so it decreases the workload the server would have to do. Sorry if this didn’t help.

I guess I am just loading chunks improperly. Instead of individually creating specific chunk for a specific player, I should divide the world into chunks, not create them on place.
I will try to do something about it and maybe will upload the result here, if I will have it.

Simple question, but have you tried utilizing StreamingEnabled? It’s designed to handle large amounts of instances loading and unloading, such as cases like this.

Besides that, something like occlusion culling may be worth reading into.

1 Like

Hi, this could solve your issue with World Generation using Actors.
Look at this Documentation about Parallel Luau in World Generation, it’s a new feature.

2 Likes

I recommend the answer given by @FranKO_Dev

Additionally, I recommend using StreamingEnabled for a better experience.

2 Likes