Voxel World Generation is Lagging

Provide an overview of:

  • What does the code do and what are you not satisfied with?
  • What potential improvements have you considered?
  • How (specifically) do you want to improve the code?

This code generates a Minecraft type world for my game. Works perfectly, except that it lags. On mid range devices its playable, but on low range devices it will be extremely laggy and aunplayable. Also, my grid system will only update after a yield. I don’t want to add more yields as everything will spawn in slowly. The main problem is how many parts I am generating. Is there any way to make this more efficient?
Image for the scale of parts i am generating:

wait()
local rs = game:GetService("RunService") -- Using Heartbeat as a yield function
local seed = math.random(-1000000000, 1000000000) --Get the seed
local Generated = 0 -- Name chunks
local pCache = require(game.ReplicatedStorage.PartCache)
local function fractalNoise(x, y, octaves, lacunarity, persistence, scale, seed) -- Create the noise map
	local value = 0 

	local x1 = x 
	local y1 = y

	local amplitude = 1

	for i = 1, octaves, 1 do
		value += math.abs(math.noise(x1 / scale, y1 / scale, seed)) * amplitude

		y1 *= lacunarity
		x1 *= lacunarity

		amplitude *= persistence
	end
	value = value ^ 2
	return value
end
local colorTable = { -- Database for colors and materials
	{
		Color = Color3.fromRGB(0, 0, 0), -- Black
		NoiseScale = nil,
		StartHeight = -600,
		EndHeight = -159,
		UseNoise = false,
		Threshold = nil,
		Material = Enum.Material.Slate,
	},
	{
		Color = Color3.fromRGB(95, 97, 95), -- Gray
		NoiseScale = 1,
		StartHeight = -159,
		EndHeight = -12,
		UseNoise = true,
		Threshold = 0.1,
		Material = Enum.Material.Slate,
	},
	{
		Color = Color3.fromRGB(42, 86, 62), -- Green
		NoiseScale = 3,
		StartHeight = -8,
		EndHeight = 25,
		UseNoise = true,
		Threshold = 0.4,
		Material = Enum.Material.Grass,
	},
	{
		Color = Color3.fromRGB(95, 97, 95), -- Gray
		NoiseScale = 4,
		StartHeight = 20,
		EndHeight = 40,
		UseNoise = true,
		Threshold = 0.5,
		Material = Enum.Material.Slate,
	},
	{
		Color = Color3.fromRGB(255, 255, 255), -- White
		NoiseScale = 30,
		StartHeight = 45,
		EndHeight = 300,
		UseNoise = true,
		Threshold = 0.75,
		Material = Enum.Material.Sand,
	},
}
local function linearStep(a, b, c) -- Equation
	return math.clamp( (c - a) / (b - a), 0, 1)
end
function generate(x, y, z, height, color, material, folder) --Create the parts
	y = math.floor(y)
	if y%4 ~= 0 then -- Lock parts to a grid
		y+=1
		if y%4 ~= 0 then
			y+=1
			if y%4 ~= 0 then
				y+=1
				if y%4 ~= 0 then
					y+=1
				end
			end
		end
	end
	if not game.ReplicatedStorage.ChunkHolder:FindFirstChild(folder) then -- Create the folder, hold parts in replicated storage before putting them in the game
		local newfolder = Instance.new("Folder", game.ReplicatedStorage.ChunkHolder)
		newfolder.Name = folder
	end
	local part = game.ReplicatedStorage.Part:Clone()
	part.Parent = game.ReplicatedStorage.ChunkHolder:FindFirstChild(folder)
	part.Position = Vector3.new(x, math.floor(y), z)
	part.Color = color
	part.Material = material
	y-=4
	for i = math.floor(y), -160, -4 do -- Make underground parts
		for _, colorData in ipairs(colorTable) do
			if i >= colorData.StartHeight then
				if colorData.UseNoise then
					local colorValue = math.noise(x / colorData.NoiseScale, y/ colorData.NoiseScale, seed * 123)
					colorValue = (colorValue + 1) / 2

					local stepResult = linearStep(colorData.StartHeight, colorData.EndHeight, y)
					colorValue = (1 - stepResult) * colorValue + stepResult

					if colorValue > colorData.Threshold then
						color = colorData.Color
						material = colorData.Material
					end

				else
					color = colorData.Color
					material = colorData.Material
				end
			end
		end
		if color == Color3.fromRGB(42, 86, 62) then -- Create dirt if there should be grass
			color = Color3.fromRGB(144, 84, 54)
            material = Enum.Material.Sand
		end
		local part = game.ReplicatedStorage.Part:Clone()
		part.Parent = game.ReplicatedStorage.ChunkHolder:FindFirstChild(folder)
		part.Position = Vector3.new(x, i, z)
		part.Color = color
		part.Material = material
	end
	local chunkPart = Instance.new("Part") -- Create an unbreakable part so the game knows to not generate anything here
	chunkPart.Name = "ChunkPart"
	chunkPart.Transparency = 1
	chunkPart.Anchored = true
	chunkPart.Size = Vector3.new(4, 100000000, 4)
	chunkPart.Position = Vector3.new(x, 0, z)
	chunkPart.Parent = game.ReplicatedStorage.ChunkHolder:FindFirstChild(folder)
	chunkPart.CanCollide = false
	game.ReplicatedStorage.ChunkHolder:FindFirstChild(folder).Parent = workspace.World
	Generated+=1
end
local function generateChunk(xmin, xmax, zmin, zmax, name) -- Calculations for every part
	local xcount = 4
	local zcount = 4
	if xmax < xmin then xcount = -4 end
	if zmax < zmin then zcount = -4 end
	for x=xmin, xmax,xcount do
		rs.Heartbeat:Wait()
		for z=zmin, zmax, zcount do
			rs.Heartbeat:Wait()
			local y1 = math.noise(x*0.35/100, z*0.35/100, seed)
			local y2 = math.noise(x*0.35/100, z*0.35/100, seed)
			local y3 = math.noise(x*0.35/100, z*0.35/100, seed)
			local y= (y1*y2*15*15)
			local height = fractalNoise(x, z, 10, 1, 0.35, 100, seed) * 5 -- Multiplying changes how high up it will be, useful for mountain biomes
			local color = nil
			local material = nil
				for _, colorData in ipairs(colorTable) do -- Get the color
					if height >= colorData.StartHeight then
						if colorData.UseNoise then
							local colorValue = math.noise(x / colorData.NoiseScale, z/ colorData.NoiseScale, seed)
							colorValue = (colorValue + 1) / 2

							local stepResult = linearStep(colorData.StartHeight, colorData.EndHeight, height)
							colorValue = (1 - stepResult) * colorValue + stepResult

							if colorValue > colorData.Threshold then
								color = colorData.Color
								material = colorData.Material
							end

						else
							color = colorData.Color
							material = colorData.Material
						end
					end
				end
			generate(x, height, z, y, color, material, name)
		end
	end
end
local function generateGrid(c) -- Create grid around players for world generation
	for i, part in pairs(workspace.GridParts:GetChildren()) do
		c:ReturnPart(part)
	end
	rs.Heartbeat:Wait()
	for i, plr in pairs(game.Players:GetPlayers()) do
		rs.Heartbeat:Wait()
		local char = plr.Character or plr.CharacterAdded:Wait()
		local xpos = char:GetBoundingBox().p.X
		local zpos = char:GetBoundingBox().p.Z
		xpos = math.round(xpos)
		zpos = math.round(zpos)
		if xpos%4 ~= 0 then
			xpos +=1
			if xpos%4 ~= 0 then
				xpos +=1
				if xpos%4 ~= 0 then
					xpos +=1
					if xpos%4 ~= 0 then
						xpos +=1
					end
				end
			end
		end
		if zpos%4 ~= 0 then
			zpos +=1
			if zpos%4 ~= 0 then
				zpos +=1
				if zpos%4 ~= 0 then
					zpos +=1
					if zpos%4 ~= 0 then
						zpos +=1
					end
				end
			end
		end
		for x = xpos - 32, xpos + 32, 4 do
			for z = zpos - 32, zpos + 32, 4 do
				local gridPart = c:GetPart()
				gridPart.Parent = workspace.GridParts
				gridPart.Position = Vector3.new(x, 15, z)
			end
		end
	end
end
local function findChunk(origin, direction) -- Check if there is already a chunk
	local params = RaycastParams.new()
	params.FilterType = Enum.RaycastFilterType.Blacklist
	local Result = workspace:Raycast(origin, direction, params)
	if Result then
		return(true)
	else
		return(false)
	end
end
local function generateChunksfromGrid(gen) -- Create the chunks from the grid
	for i, gridPart in pairs(workspace.GridParts:GetChildren()) do
		local hasChunk = findChunk(gridPart.Position, Vector3.new(0, -10000, 0))
		if not hasChunk then
			generateChunk(gridPart.Position.X,gridPart.Position.X,gridPart.Position.Z,gridPart.Position.Z, "Chunk"..tostring(gen))
		end
	end
end
local newCache = pCache.new(game.ReplicatedStorage.GridPart, 300, script.Cache) -- PartCache, less lag
while wait() do
	generateGrid(newCache)
	generateChunksfromGrid(Generated)
end

If you want to know how it works, this person made a great tutorial.

I did similar things before. Are you generating all the blocks, even the ones that are not visible to the player? If so that is the issue. You should only generate the blocks that the camera can see and when the player breaks the top block then generate the one underneath. That is how Minecraft has solved this issue, as well as many other similar games.

The way some people do it is to keep every block in a folder in replicated storage. Then find the closest blocks to the camera and make them go in the workspace, then put them back when they are too far. You can also change this using a RenderDistance setting in your game.

Make sure that you have a folder for each chunk and that your blocks will go back to the right chunk folder so it’s easier to keep track of. But if you don’t want chunks you can just keep every block in one folder.

The problem with this is that anything handled locally with simulation wont work. Which is why you should keep everything generated on the server and handle rendering on the client. If you have mobs or other players, they shouldn’t fall down because of this.

task.wait()

use the and.

Once again, task.wait…