I’m trying to procedurally generate minecraft-like terrain and am able to do the first layer, but am struggling with lag and performance issues. Everytime new chunks are loaded the player gets a lag spike, and if I try generating layers under the first its even worse. Whats the best way of doing this, heres my code so far:
-- Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
-- Chunks and Blocks variables/settings
local chunksFolder = workspace:WaitForChild("Chunks")
local loadedChunks = {}
local chunkSize = 16
local serverRenderDistance = 5
local blocksFolder = ReplicatedStorage:WaitForChild("Blocks")
local blockSize = blocksFolder.Grass.Size
-- Map settings
local amplitude = 0.4
-- Perlin Noise setup and variables/settings
math.randomseed(os.time())
local noiseOffsetX = math.random(0, 10000)
local noiseOffsetZ = math.random(0, 10000)
-- Functions
local function worldToBlockCoordinates(position: Vector3)
local blockX = math.floor(position.X / blockSize.X)
local blockY = math.floor(position.Y / blockSize.Y)
local blockZ = math.floor(position.Z / blockSize.Z)
return Vector3.new(blockX, blockY, blockZ)
end
local function placeBlock(block: Instance, position: Vector3, parent: Instance)
local generatedBlock = block:Clone()
generatedBlock.Position = position
if parent then
generatedBlock.Parent = parent
else
generatedBlock.Parent = workspace
end
return generatedBlock
end
local function generateChunk(chunkX: number, chunkZ: number)
local chunkExists = loadedChunks["X: "..chunkX.." Z: "..chunkZ]
if chunkExists then return end
local chunkFolder = Instance.new("Folder", chunksFolder)
chunkFolder.Name = "X: "..chunkX.." Z: "..chunkZ
local chunkData = {}
for x = 1, chunkSize do
chunkData[x] = {}
for z = 1, chunkSize do
local noiseValue = math.noise(((chunkX - 1) * chunkSize + x + noiseOffsetX) / 10, ((chunkZ - 1) * chunkSize + z + noiseOffsetZ) / 10)
chunkData[x][z] = math.round(noiseValue * (15 * amplitude)) * blockSize.Y
end
end
for x = 1, chunkSize do
for z = 1, chunkSize do
local chunkWorldPositionX = (chunkX - 1) * chunkSize * blockSize.X
local chunkWorldPositionZ = (chunkZ - 1) * chunkSize * blockSize.Z
local blockWorldPosition = Vector3.new(
chunkWorldPositionX + (x - 1) * blockSize.X,
chunkData[x][z],
chunkWorldPositionZ + (z - 1) * blockSize.Z
)
if worldToBlockCoordinates(blockWorldPosition).Y > 0 then
placeBlock(blocksFolder.Grass, blockWorldPosition, chunkFolder)
else
placeBlock(blocksFolder.Dirt, blockWorldPosition, chunkFolder)
end
end
end
loadedChunks["X: "..chunkX.." Z: "..chunkZ] = chunkFolder
return chunkFolder
end
local function generateChunkRegion(startX: number, startZ: number, xChunks: number, zChunks: number)
for x = 0, xChunks do
for z = 0, zChunks do
local chunkX = startX + x
local chunkZ = startZ + z
generateChunk(chunkX, chunkZ)
end
end
end
local function generateChunksAroundPlayer(player: Player)
local character = player.Character
if not character then return end
local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
if not humanoidRootPart then return end
local playerPosition = humanoidRootPart.Position
local playerChunkX = math.floor(playerPosition.X / (chunkSize * blockSize.X))
local playerChunkZ = math.floor(playerPosition.Z / (chunkSize * blockSize.Z))
generateChunkRegion(playerChunkX - serverRenderDistance, playerChunkZ - serverRenderDistance, serverRenderDistance * 2 + 1, serverRenderDistance * 2 + 1)
end
local function unloadChunksAroundPlayers()
local occupiedChunks = {}
-- Determine the chunks that are within the server render distance of any player
for _, player in pairs(Players:GetPlayers()) do
local character = player.Character
if not character then continue end
local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
if not humanoidRootPart then continue end
local playerPosition = humanoidRootPart.Position
local chunkSizeX = chunkSize * blockSize.X
local chunkSizeZ = chunkSize * blockSize.Z
-- Calculate the player's chunk
local playerChunkX = math.floor(playerPosition.X / chunkSizeX + 1)
local playerChunkZ = math.floor(playerPosition.Z / chunkSizeZ + 1)
-- Calculate min and max chunk coordinates
local minChunkX = playerChunkX - serverRenderDistance - 1
local maxChunkX = playerChunkX + serverRenderDistance + 1
local minChunkZ = playerChunkZ - serverRenderDistance - 1
local maxChunkZ = playerChunkZ + serverRenderDistance + 1
-- Add occupied chunks to the table
for x = minChunkX, maxChunkX do
for z = minChunkZ, maxChunkZ do
occupiedChunks["X: " .. x .. " Z: " .. z] = true
end
end
end
-- Unload chunks that are not occupied by any player
for chunkKey, chunkFolder in pairs(loadedChunks) do
if not occupiedChunks[chunkKey] then
chunkFolder.Parent = nil
loadedChunks[chunkKey] = nil
end
end
end
-- Runtime
Players.PlayerAdded:Connect(function(player)
player.CharacterAdded:Connect(function(character)
local humanoid = character:WaitForChild("Humanoid")
-- Generate initial chunks
generateChunksAroundPlayer(player)
humanoid.Running:Connect(function()
generateChunksAroundPlayer(player)
end)
end)
end)
task.spawn(function()
while true do
wait(5)
for _, player in pairs(Players:GetPlayers()) do
unloadChunksAroundPlayers()
end
end
end)