Hi, I’m currently making a voxel game with procedurally generated terrain.
However, i would like to know how to optimize it properly:
Chunks are currently generated and then placed inside a folder in workspace (which will replicate it to the client), and if the map size is too big, it will consume too much memory to the point where the Client can crash. And each chunk has an average of 3-7k+ voxels.
StreamingEnabled could fix this, but having StreamingEnabled on causes a huge framerate drop when a chunk gets streamed into the Client.
And since chunks get replicated to the Client while they are being generated, it also causes framerate drops. (and also causes the recently created parts/voxels to be counted as moving/awake?)
However the performance after the chunks have finished generated is completly fine and stable.
And also since i’ve ever implemented SharedTables to save voxel positions to avoid them being placed over one another, there is usually missing voxels on the surface (and possibly underground). I also cant seem to place any new voxels on these positions.
ChunkHandler module
local replicatedStorage = game:GetService("ReplicatedStorage")
local scriptService = game:GetService("ServerScriptService")
local rs = game:GetService("RunService")
local modules = replicatedStorage:WaitForChild("Modules")
local blockHandler = require(modules.BlockHandler)
local worldFolder = workspace:WaitForChild("World")
local selfModule = {}
function selfModule:GetChunks()
local chunks = {}
for i,v in pairs(worldFolder:GetChildren()) do
if v:IsA("Folder") then
table.insert(chunks, v)
end
end
return chunks
end
function selfModule:GetBlocks(chunkFolder: Folder)
local blocks = {}
for i,v in pairs(chunkFolder:GetDescendants()) do
if v:IsA("BasePart") then
table.insert(blocks, v)
end
end
return blocks
end
selfModule.newChunk = function(chunkX, chunkZ, chunkSize, SEED, HILL_SCALE, BLOCKSIZE, numDirtLayers, numStoneLayers, numCaveStoneLayers, numDeeprockLayers, CAVE_SCALE)
replicatedStorage.Values.ChunksInGeneration.Value += 1
local chunkFolder = Instance.new("Folder")
chunkFolder.Name = "Chunk_"..chunkX.."_"..chunkZ
chunkFolder.Parent = worldFolder
for x = 1, chunkSize do
for z = 1, chunkSize do
rs.Heartbeat:Wait()
local realX = (chunkX - 1)*chunkSize + x
local realZ = (chunkZ - 1)*chunkSize + z
local noise = math.noise(SEED, realX/12,realZ/12)*HILL_SCALE
local position = Vector3.new(realX * BLOCKSIZE, math.floor(noise)*BLOCKSIZE, realZ * BLOCKSIZE)
local grassBlock = blockHandler.newBlock(CFrame.new(position), chunkFolder, "Grass")
rs.Heartbeat:Wait()
local treeNoise = math.noise(SEED, realX/12,realZ/12)*(HILL_SCALE/2)
if treeNoise >= 0.975 then
task.spawn(blockHandler.newTree, position + Vector3.new(0,3,0), SEED, chunkFolder)
end
task.spawn(function()
for i = 1, numDirtLayers do
local dirtPosition = Vector3.new(
realX * BLOCKSIZE,
math.floor(noise)*BLOCKSIZE - (BLOCKSIZE * i),
realZ * BLOCKSIZE
)
local dirtBlock = blockHandler.newBlock(CFrame.new(dirtPosition), chunkFolder, "Dirt")
rs.Heartbeat:Wait()
end
end)
task.spawn(function()
for i = 1, numStoneLayers do
local stonePosition = Vector3.new(
realX * BLOCKSIZE,
math.floor(noise)*BLOCKSIZE - (BLOCKSIZE * i) - ( (numDirtLayers) * BLOCKSIZE),
realZ * BLOCKSIZE
)
local stoneBlock = blockHandler.newBlock(CFrame.new(stonePosition), chunkFolder, "Stone")
rs.Heartbeat:Wait()
end
end)
task.spawn(function()
for i = 1, numCaveStoneLayers do
-- Use math.noise to simulate a perlin worm to create caves
local caveNoise = math.noise(realX/12, realZ/12, i/12) * CAVE_SCALE
if caveNoise > 0 then
local stonePosition = Vector3.new(
realX * BLOCKSIZE,
math.floor(noise)*BLOCKSIZE - (BLOCKSIZE * i) - ( (numDirtLayers + numStoneLayers) * BLOCKSIZE),
realZ * BLOCKSIZE
)
local stoneBlock = blockHandler.newBlock(CFrame.new(stonePosition), chunkFolder, "Stone")
rs.Heartbeat:Wait()
end
end
end)
task.spawn(function()
for i = 1, numStoneLayers do
local stonePosition = Vector3.new(
realX * BLOCKSIZE,
math.floor(noise)*BLOCKSIZE - (BLOCKSIZE * i) - ( (numDirtLayers + numStoneLayers + numCaveStoneLayers) * BLOCKSIZE),
realZ * BLOCKSIZE
)
local stoneBlock = blockHandler.newBlock(CFrame.new(stonePosition), chunkFolder, "Stone")
rs.Heartbeat:Wait()
end
end)
task.spawn(function()
for i = 1, numDeeprockLayers do
local deepRockPosition = Vector3.new(
realX * BLOCKSIZE,
math.floor(noise)*BLOCKSIZE - (BLOCKSIZE * i) - ((numDirtLayers + (numStoneLayers * 2) + numCaveStoneLayers) * BLOCKSIZE),
realZ * BLOCKSIZE
)
local deepRockBlock = blockHandler.newBlock(CFrame.new(deepRockPosition), chunkFolder, "Deeprock")
rs.Heartbeat:Wait()
end
end)
end
end
print("-")
print("Generated chunk: ".. chunkFolder.Name)
print("Blocks in chunk: ".. #selfModule:GetBlocks(chunkFolder))
replicatedStorage.Values.ChunksInGeneration.Value -= 1
replicatedStorage.Values.ChunksGenerated.Value += 1
return chunkFolder
end
return selfModule
BlockHandler module
local replicatedStorage = game:GetService("ReplicatedStorage")
local sharedTableService = game:GetService("SharedTableRegistry")
local models = replicatedStorage:WaitForChild("Models")
local blockModel = models.Block
local module = {}
task.spawn(function()
if sharedTableService:GetSharedTable("TreePositions") == nil then
local treePositions = SharedTable.new()
sharedTableService:SetSharedTable("TreePositions", treePositions)
end
if sharedTableService:GetSharedTable("BlockPositions") == nil then
local blockPositions = SharedTable.new()
sharedTableService:SetSharedTable("BlockPositions", blockPositions)
end
end)
local function copyArray(s: SharedTable)
local t = {}
for key, val in s do
if typeof(val) == "SharedTable" then
t[key] = copyArray(val)
else
t[key] = val
end
end
return t
end
local blocks = {
["Grass"] = {
["Destroyable"] = true,
["DestroyTime"] = 1,
["Color"] = BrickColor.new("Bright green"),
["Material"] = Enum.Material.Plastic,
["Surfaces"] = {
["Top"] = Enum.SurfaceType.Studs,
["Front"] = Enum.SurfaceType.Studs,
["Right"] = Enum.SurfaceType.Studs,
["Left"] = Enum.SurfaceType.Studs,
["Back"] = Enum.SurfaceType.Studs,
["Bottom"] = Enum.SurfaceType.Inlet
},
["CanCollide"] = true,
["CastShadows"] = true,
},
["Dirt"] = {
["Destroyable"] = true,
["DestroyTime"] = 1,
["Color"] = BrickColor.new("Brown"),
["Material"] = Enum.Material.Plastic,
["Surfaces"] = {
["Top"] = Enum.SurfaceType.Universal,
["Front"] = Enum.SurfaceType.Universal,
["Right"] = Enum.SurfaceType.Universal,
["Left"] = Enum.SurfaceType.Universal,
["Back"] = Enum.SurfaceType.Universal,
["Bottom"] = Enum.SurfaceType.Universal
},
["CanCollide"] = true,
["CastShadows"] = true,
},
["Stone"] = {
["Destroyable"] = true,
["DestroyTime"] = 6,
["Color"] = BrickColor.new("Medium stone grey"),
["Material"] = Enum.Material.Plastic,
["Surfaces"] = {
["Top"] = Enum.SurfaceType.Weld,
["Front"] = Enum.SurfaceType.Weld,
["Right"] = Enum.SurfaceType.Weld,
["Left"] = Enum.SurfaceType.Weld,
["Back"] = Enum.SurfaceType.Weld,
["Bottom"] = Enum.SurfaceType.Weld
},
["CanCollide"] = true,
["CastShadows"] = true,
},
["Deeprock"] = {
["Destroyable"] = false,
["DestroyTime"] = 1000,
["Color"] = BrickColor.new("Black"),
["Material"] = Enum.Material.Plastic,
["Surfaces"] = {
["Top"] = Enum.SurfaceType.Smooth,
["Front"] = Enum.SurfaceType.Smooth,
["Right"] = Enum.SurfaceType.Smooth,
["Left"] = Enum.SurfaceType.Smooth,
["Back"] = Enum.SurfaceType.Smooth,
["Bottom"] = Enum.SurfaceType.Smooth
},
["CanCollide"] = true,
["CastShadows"] = false,
},
["Wool"] = {
["Destroyable"] = true,
["DestroyTime"] = 0.5,
["Color"] = BrickColor.new("Bright red"),
["Material"] = Enum.Material.Plastic,
["Surfaces"] = {
["Top"] = Enum.SurfaceType.Weld,
["Front"] = Enum.SurfaceType.Weld,
["Right"] = Enum.SurfaceType.Weld,
["Left"] = Enum.SurfaceType.Weld,
["Back"] = Enum.SurfaceType.Weld,
["Bottom"] = Enum.SurfaceType.Weld
},
["CanCollide"] = true,
["CastShadows"] = true,
},
["TreeLeaves"] = {
["Destroyable"] = true,
["DestroyTime"] = 0.5,
["Color"] = BrickColor.new("Dark green"),
["Material"] = Enum.Material.Plastic,
["Surfaces"] = {
["Top"] = Enum.SurfaceType.Weld,
["Front"] = Enum.SurfaceType.Weld,
["Right"] = Enum.SurfaceType.Weld,
["Left"] = Enum.SurfaceType.Weld,
["Back"] = Enum.SurfaceType.Weld,
["Bottom"] = Enum.SurfaceType.Weld
},
["CanCollide"] = true,
["CastShadows"] = false,
},
["TreeLog"] = {
["Destroyable"] = true,
["DestroyTime"] = 3.5,
["Color"] = BrickColor.new("Medium brown"),
["Material"] = Enum.Material.Plastic,
["Surfaces"] = {
["Top"] = Enum.SurfaceType.Studs,
["Front"] = Enum.SurfaceType.Smooth,
["Right"] = Enum.SurfaceType.Smooth,
["Left"] = Enum.SurfaceType.Smooth,
["Back"] = Enum.SurfaceType.Smooth,
["Bottom"] = Enum.SurfaceType.Inlet
},
["CanCollide"] = true,
["CastShadows"] = true,
}
}
module.newBlock = function(blockPosition: CFrame, parent: Folder, blockName: string): BasePart
task.spawn(function()
local getPositions = sharedTableService:GetSharedTable("BlockPositions")
local vc = blockPosition.Position
if module:IsBlockPositionAvailable(vc) then
local newBlock = blockModel:Clone()
newBlock.Parent = parent or workspace:FindFirstChild("World")
getPositions[vc.X .. vc.Y .. vc.Z] = true
sharedTableService:SetSharedTable("BlockPositions", getPositions)
newBlock:PivotTo(blockPosition)
newBlock.CanTouch = false
newBlock:SetAttribute("Destroyable", true)
newBlock:SetAttribute("DestroyTime", 1)
if blockName and blocks[blockName] then
newBlock:SetAttribute("Destroyable", blocks[blockName].Destroyable)
newBlock:SetAttribute("DestroyTime", blocks[blockName].DestroyTime)
newBlock.BrickColor = blocks[blockName].Color
newBlock.Material = blocks[blockName].Material
newBlock.TopSurface = blocks[blockName].Surfaces.Top
newBlock.FrontSurface = blocks[blockName].Surfaces.Front
newBlock.RightSurface = blocks[blockName].Surfaces.Right
newBlock.LeftSurface = blocks[blockName].Surfaces.Left
newBlock.BackSurface = blocks[blockName].Surfaces.Back
newBlock.BottomSurface = blocks[blockName].Surfaces.Bottom
newBlock.CanCollide = blocks[blockName].CanCollide
newBlock.CastShadow = blocks[blockName].CastShadows
end
local connection
connection = newBlock.AttributeChanged:Connect(function(attribute)
if attribute == "DestroyProgress" and newBlock:GetAttribute("Destroyable") == true then
local decals = {}
for i,v in pairs(newBlock:GetChildren()) do
if v.Name == "DestroyProgress_Decal" then
table.insert(decals, v)
end
end
local progress = newBlock:GetAttribute("DestroyProgress")
local transparency = (1 - ( progress / newBlock:GetAttribute("DestroyTime") ) )
for i,v in pairs(decals) do
v.Transparency = transparency
end
if progress >= newBlock:GetAttribute("DestroyTime") then
connection:Disconnect()
local getPositions = sharedTableService:GetSharedTable("BlockPositions")
local vc = blockPosition.Position
getPositions[vc.X .. vc.Y .. vc.Z] = nil
sharedTableService:SetSharedTable("BlockPositions", getPositions)
newBlock:Destroy()
end
end
end)
return newBlock
end
return nil
end)
end
module.newTree = function(treeStart: Vector3, worldSeed: number, parent: Folder)
local getTreePositions = sharedTableService:GetSharedTable("TreePositions")
for i,v in pairs(copyArray(getTreePositions)) do
local separatedString = string.split(i,",")
local getVector = Vector3.new(tonumber(separatedString[1]), tonumber(separatedString[2]), tonumber(separatedString[3]))
local distance = (treeStart - getVector).Magnitude
if distance <= (3*4) then
return
end
end
local treeFolder = Instance.new("Folder")
treeFolder.Name = "Tree"
treeFolder.Parent = parent
local baseBlock = module.newBlock(CFrame.new(treeStart), treeFolder, "TreeLog")
getTreePositions[treeStart.X..","..treeStart.Y..","..treeStart.Z] = true
sharedTableService:SetSharedTable("TreePositions", getTreePositions)
local amountOfLogs = math.noise(worldSeed, treeStart.Z/8, treeStart.X/8)
amountOfLogs = math.clamp(amountOfLogs, 3, 6)
for i= 1, amountOfLogs do
local logPosition: Vector3 = treeStart + Vector3.new(0,i*3,0)
module.newBlock(CFrame.new(logPosition), treeFolder, "TreeLog")
if i == amountOfLogs or i == amountOfLogs -1 then
if i == amountOfLogs-1 then
local leafPositions = {
Vector3.new(3,0,0),
Vector3.new(-3,0,0),
Vector3.new(3,0,3),
Vector3.new(-3,0,-3),
Vector3.new(3,0,-3),
Vector3.new(-3,0,3),
Vector3.new(6,0,0),
Vector3.new(0,0,6),
Vector3.new(-6,0,0),
Vector3.new(0,0,-6),
Vector3.new(6,0,-6),
Vector3.new(-6,0,-6),
Vector3.new(3,3,0),
Vector3.new(-3,3,0),
Vector3.new(0,3,3),
Vector3.new(0,3,-3)
}
for i,v in pairs(leafPositions) do
module.newBlock(CFrame.new(logPosition + v), treeFolder, "TreeLeaves")
end
elseif i == amountOfLogs then
local leafPositions = {
Vector3.new(3,0,0),
Vector3.new(-3,0,0),
Vector3.new(3,0,3),
Vector3.new(-3,0,-3),
Vector3.new(3,0,-3),
Vector3.new(-3,0,3),
Vector3.new(0,3,0),
Vector3.new(6,0,0),
Vector3.new(0,0,6),
Vector3.new(-6,0,0),
Vector3.new(0,0,-6),
Vector3.new(6,0,-6),
Vector3.new(-6,0,-6),
Vector3.new(3,3,0),
Vector3.new(-3,3,0),
Vector3.new(0,3,3),
Vector3.new(0,3,-3)
}
for i,v in pairs(leafPositions) do
module.newBlock(CFrame.new(logPosition + v), treeFolder, "TreeLeaves")
end
end
end
end
end
function module:IsBlockPositionAvailable(pos: Vector3): boolean
local getPositions = sharedTableService:GetSharedTable("BlockPositions")
if getPositions[pos.X .. pos.Y .. pos.Z] then
return false
end
return true
end
function module:DestroyBlock(block: BasePart)
if block:GetAttribute("Destroyable") == true then
block:Destroy()
end
end
return module
WorldGeneration script
-- World generation script
local replicatedStorage = game:GetService("ReplicatedStorage")
local modules = replicatedStorage.Modules
local rs = game:GetService("RunService")
local worldFolder = Instance.new("Folder")
worldFolder.Name = "World"
worldFolder.Parent = workspace
local blockHandler = require(modules.BlockHandler)
local chunkHandler = require(modules.ChunkHandler)
local MAPSIZE = 35
local MAPSCALE = MAPSIZE/2
local BLOCKSIZE = 3
local HILL_SCALE = 7
local CAVE_SCALE = 5.5
local SEED = math.random(-10000000,10000000)
local chunkSize = 16
local numChunks = math.ceil(MAPSIZE / chunkSize)
local numDirtLayers = 2
local numStoneLayers = 5
local numCaveStoneLayers = 26
local numDeeprockLayers = 2
local chunksInGeneration = replicatedStorage.Values.ChunksInGeneration
local chunksGenerated = replicatedStorage.Values.ChunksGenerated
local chunksToGenerate = replicatedStorage.Values.ChunksToGenerate
local chunksLimit = 6
local function getBlocks(chunkFolder: Folder)
local blocks = {}
for i,v in pairs(chunkFolder:GetDescendants()) do
if v:IsA("BasePart") then
table.insert(blocks, v)
end
end
return blocks
end
local function getChunks()
local chunks = {}
for i,v in pairs(worldFolder:GetChildren()) do
if v:IsA("Folder") then
table.insert(chunks, v)
end
end
return chunks
end
function generateChunk(chunkX, chunkZ)
task.spawn(function()
if chunksInGeneration.Value >= chunksLimit then
repeat task.wait(2) until chunksInGeneration.Value < chunksLimit
end
task.spawn(chunkHandler.newChunk, chunkX, chunkZ, chunkSize, SEED, HILL_SCALE, BLOCKSIZE, numDirtLayers, numStoneLayers, numCaveStoneLayers, numDeeprockLayers, CAVE_SCALE)
end)
end
function beginWorldGeneration()
task.spawn(function()
for chunkX = 0, numChunks do
for chunkZ = 0, numChunks do
generateChunk(chunkX, chunkZ)
if chunkX > 0 and chunkZ > 0 then
chunksToGenerate.Value += 1
generateChunk(-chunkX, -chunkZ)
end
if chunkX > 0 then
chunksToGenerate.Value += 1
generateChunk(-chunkX, chunkZ)
end
if chunkZ > 0 then
chunksToGenerate.Value += 1
generateChunk(chunkX, -chunkZ)
end
end
end
end)
if chunksGenerated.Value < chunksToGenerate.Value then
repeat task.wait(4) until chunksGenerated.Value >= chunksToGenerate.Value
end
print("-")
print("World generated")
print("Chunks: ".. #getChunks())
print("Blocks: ".. #getBlocks(worldFolder))
print("Seed: ".. SEED)
game.Players.CharacterAutoLoads = true
for i,v in pairs(game.Players:GetPlayers()) do
v:LoadCharacter()
end
end
replicatedStorage.Events.WorldGeneration.OnServerEvent:Connect(function(player, dataTable)
SEED = dataTable["Seed"] or math.random(-10000000,10000000)
MAPSIZE = math.clamp(dataTable["WorldSize"] or 35, 10, 200) or 35
HILL_SCALE = math.clamp(dataTable["HillScale"] or 7,1,35) or 7
CAVE_SCALE = math.clamp(dataTable["CaveScale"] or 5.5,1, 20) or 5.5
numDirtLayers = math.clamp(dataTable["DirtAmount"] or 2,1, 40) or 2
numStoneLayers = math.clamp(dataTable["StoneAmount"] or 5,1, 40) or 5
numCaveStoneLayers = math.clamp(dataTable["CaveStoneAmount"] or 26,1,40) or 26
numDeeprockLayers = math.clamp(dataTable["DeeprockAmount"] or 2,1,40) or 2
chunksLimit = math.clamp(dataTable["ChunkLimit"] or 6,1,12) or 6
MAPSCALE = MAPSIZE/2
numChunks = math.ceil(MAPSIZE / chunkSize)
chunksToGenerate.Value = numChunks
beginWorldGeneration()
end)
You can try out the game by yourself here.
I would also like to clarify that im mostly a begineer in terms of scripting.
Any help and feedback is appreciated, thanks!