# Chunk system uses a very big amount of ram

I’m writing a minecraft-ish game, with a chunk system.

Each chunk has a 16x256x16 size, which should be pretty small and easy to store, however with storing air (I really need to store air blocks) it takes 60.58 MiB of RAM for ONE CHUNK. I need at least 169 chunks per player, which uses ~9.9 GiB of RAM, and roblox servers only have 6 GiB of RAM, 1 of it being used by random roblox features.

Without storing air blocks it uses 30 MiB of RAM per chunk, but without storing air blocks I would have to work way harder to get the same thing done.

One ideea I have is to generate and delete chunks on the fly, and only save in a array the changes made by a player, however I would have to regenerate the chunk every time a block changes, making it extremely slow.

Any ideea on how to optimise this? This is the main culprit:

``````
function Terrain:GenerateChunk(chunkPosition, onFinished)
local chunkNeighbors = {}

for neightbor_x = -1, 1 do
for neightbor_z = -1, 1 do
for neightbor_y = -1, 1 do
if neightbor_x ~= 0 or not (neightbor_x == neightbor_y and neightbor_x == neightbor_z and neightbor_z == neightbor_y) then
local neighbor_position = Vector3.new(chunkPosition.X + neightbor_x, chunkPosition.Y + neightbor_y, chunkPosition.Z + neightbor_z)
table.insert(chunkNeighbors, neighbor_position)
end
end
end
end

local chunk = {
position = chunkPosition,
blocks = {},
neighbors = chunkNeighbors,
}

local start_x = Terrain.chunk_size.X * chunkPosition.X
local end_x = (Terrain.chunk_size.X) * (chunkPosition.X + 1)
local start_z = Terrain.chunk_size.Z * chunkPosition.Z
local end_z = (Terrain.chunk_size.Z ) * (chunkPosition.Z + 1)
local start_y = (Terrain.chunk_size.Y ) * (chunkPosition.Y)
local end_y = (Terrain.chunk_size.Y ) * (chunkPosition.Y + 1)

for x = start_x, end_x do
for z = start_z, end_z do
if chunkPosition.Y == 0 then
local max_y = math.floor((math.noise(x/self.scale, z/self.scale, self.seed) * self.amplitude) + 100)
local max_block_y = chunkPosition.Y * Terrain.chunk_size.Y + max_y

for y = chunkPosition.Y * Terrain.chunk_size.Y, max_block_y do
local material = "unknown"

if y == max_block_y - 1 then
material = "grass"
elseif y < max_block_y - 12 then
material = "stone"
elseif y >= max_block_y - 12 then
material = "dirt"
end

local blockPosition = Vector3.new(x, y, z)
local block = {
position = blockPosition,
visible = false,
rendered = false,
render_part = nil,
neighbors = {},
material = material,
}

chunk.blocks[blockPosition] = block
end

for y = max_block_y, ((chunkPosition.Y + 1) * Terrain.chunk_size.Y) do
local blockPosition = Vector3.new(x, y, z)
local block = {
position = blockPosition,
visible = false,
rendered = false,
render_part = nil,
neighbors = {},
material = "air",
}

chunk.blocks[blockPosition] = block
end
else

end
end
end

for x = start_x, end_x do
for z = start_z, end_z do
for y = (chunkPosition.Y * Terrain.chunk_size.Y), ((chunkPosition.Y + 1) * Terrain.chunk_size.Y) do
local block = chunk.blocks[Vector3.new(x, y, z)]
if block then
if x == start_x or x == end_x or z == start_z or z == end_z then
--block.visible = true
end

local possibleNeighbors = {
Vector3.new(x-1,y-1,z-1),
Vector3.new(x-1,y,z-1),
Vector3.new(x-1,y+1,z-1),
Vector3.new(x-1,y-1,z),
Vector3.new(x-1,y,z),
Vector3.new(x-1,y+1,z),
Vector3.new(x-1,y-1,z+1),
Vector3.new(x-1,y,z+1),
Vector3.new(x-1,y+1,z+1),
Vector3.new(x,y-1,z-1),
Vector3.new(x,y,z-1),
Vector3.new(x,y+1,z-1),
Vector3.new(x,y-1,z),
Vector3.new(x,y+1,z),
Vector3.new(x,y-1,z+1),
Vector3.new(x,y,z+1),
Vector3.new(x,y+1,z+1),
Vector3.new(x+1,y-1,z-1),
Vector3.new(x+1,y,z-1),
Vector3.new(x+1,y+1,z-1),
Vector3.new(x+1,y-1,z),
Vector3.new(x+1,y,z),
Vector3.new(x+1,y+1,z),
Vector3.new(x+1,y-1,z+1),
Vector3.new(x+1,y,z+1),
Vector3.new(x+1,y+1,z+1),
}

for _, neighbor_position in ipairs(possibleNeighbors) do
table.insert(block.neighbors, neighbor_position)

local real_block = chunk.blocks[neighbor_position]
if not real_block then
for _, chunkNeighbor in ipairs(chunkNeighbors) do
local chunkNeighbor = self.chunks[chunkNeighbor]
if chunkNeighbor then
if chunkNeighbor.blocks[neighbor_position] then
real_block = chunkNeighbor.blocks[neighbor_position]
break
end
end
end
end

if not real_block then
block.visible = true

if block.material == "dirt" then
block.material = "grass"
end

break
end
end
end
end
end

--[[if x % 50 == 0 then
end]]
end

self.chunks[chunkPosition] = chunk
onFinished()
end

``````

I’ve tried to debug it further, but it still uses the same amount of RAM

RLE? a compressed data format to store the chunk data

I’m pretty sure that I would need to decode and reencode the chunk every time I change anything with it, which would mean over 5 times per second, that would create a lot of lag, would’t it?

Not sure, you could try? Ultimately, the choice of compression technique would depend on the specific requirements of the application and the characteristics of the data being compressed.

Ill rewrite the way I’m storing chunks and blocks, and Ill make a message once I try your suggestion

1 Like

For the code you could do:

1. Remove the unnecessary conditional in the neighbor generation loop (simplify basically)
2. Avoid recalculating the same values in the nested for loops
3. Reuse table inserts instead of creating new tables
1 Like

A few ideas.

In general:
You can use fewer tables.
Or less data in tables.

Maybe if some data is only applicable if a part has been instantiated, put that data in the part and not the table.

I don’t understand all the code but there is a lot about neighbors. If the code stores information about which neighbors exist, it will be much better to check the neighbours at runtime instead of storing extra information.

Maybe table.create() is more efficient than table.insert() - don’t know.

1 Like

with a few hours of refactoring the entire code, I was able to improve the noise function (add overhangs and such), decrease the size of a chunk from 60MiB to 6MiB, and increase the speed from 600ms to less than 150ms.

I will also use a compression algorithm to store unused chunks in memory.

I gotta thank all of you for supporting me, and if anyone needs it, this is the new code:

``````local Terrain = {}
Terrain.blockSize = 2
Terrain.chunkSize = Vector3.new(16, 255, 16)
Terrain.splinePositions = {
[0] = -85,
[0.1] = -50,
[0.2] = -50,
[0.3] = -25,
[0.4] = -25,
[0.5] = 0,
[0.6] = 0,
[0.7] = 35,
[0.8] = 50,
[0.9] = 85,
[1] = 85
}

local TerrainObject = {}
TerrainObject.__index = TerrainObject

function Terrain.new(seed, scale, amplitude)
local newTerrain = {}
setmetatable(newTerrain, TerrainObject)

newTerrain.rng = Random.new(seed)
newTerrain.scale = scale
newTerrain.amplitude = amplitude
newTerrain.chunks = {}
newTerrain.seed = seed

return newTerrain
end

function TerrainObject:__calculateSpline(neighbors)
if #neighbors == 0 then
return {
["spline"] = 0.55,
["value"] = Terrain.splinePositions[0.5]
}
end

local neighborsValues = 0
local totalNeighbors = 0
for _, neighbor in ipairs(neighbors) do
neighbor = self.chunks[neighbor]
if neighbor then
totalNeighbors += 1
neighborsValues += neighbor.spline
end
end

if totalNeighbors == 0  then
return {
["spline"] = 0.55,
["value"] = Terrain.splinePositions[0.5]
}
end

local spline = math.clamp((neighborsValues / totalNeighbors) + self.rng:NextInteger(0, 50) / 1000, 0, 1)
local value = math.huge

for otherNumber, _ in pairs(Terrain.splinePositions) do
if math.abs(spline - otherNumber) < math.abs(spline - value) then
value = otherNumber
end
end

return {
["spline"] = spline,
["value"] = Terrain.splinePositions[value]
}
end

function TerrainObject:GenerateChunk(position, onFinished)
if not self.chunks[position.X] then self.chunks[position.X] = {} end
if not self.chunks[position.X][position.Y]  then self.chunks[position.X][position.Y] = {} end

local chunkNeighbors = table.create(26)

for neightbor_x = -1, 1 do
for neightbor_z = -1, 1 do
for neightbor_y = -1, 1 do
if neightbor_x ~= 0 or not (neightbor_x == neightbor_y and neightbor_x == neightbor_z and neightbor_z == neightbor_y) then
local neighbor_position = Vector3.new(position.X + neightbor_x, position.Y + neightbor_y, position.Z + neightbor_z)
table.insert(chunkNeighbors, neighbor_position)
end
end
end
end

local spline = self:__calculateSpline(chunkNeighbors)
local chunk = {
position = position,
blocks = table.create(Terrain.chunkSize.X),
spline = spline.spline,
splineValue = spline.value,
neighbors = chunkNeighbors,
}

for x = 0, Terrain.chunkSize.X do
if not chunk.blocks[x] then chunk.blocks[x] = table.create(Terrain.chunkSize.Z) end
local real_x = position.X * Terrain.chunkSize.X + x

for z = 0, Terrain.chunkSize.Z do
if not chunk.blocks[x][z] then chunk.blocks[x][z] = table.create(Terrain.chunkSize.Y) end
local real_z = position.Z * Terrain.chunkSize.Z + z

for y = 75, Terrain.chunkSize.Y do
local real_y = position.Y * Terrain.chunkSize.Y + y

local density_x = math.noise((real_y / self.scale), (real_z / self.scale), self.seed) * self.amplitude
local density_y = math.noise((real_x / self.scale), (real_z / self.scale), self.seed) * self.amplitude
local density_z = math.noise((real_x / self.scale), (real_y / self.scale), self.seed) * self.amplitude

local density = y + spline.value + (density_x + density_y + density_z) / 3

local block = {
position = Vector3.new(real_x, real_y, real_z),
light = 0,
visible = false,
material = "unknown",
}

if density < 130 then
--if density < 130 and density > 85 then
block.material = "stone"
else
block.material = "air"
end

chunk.blocks[x][z][y] = block
end

for y = 0, 75 do
local real_y = position.Y * Terrain.chunkSize.Y + y

local block = {
position = Vector3.new(real_x, real_y, real_z),
light = 0,
visible = false,
material = "stone",
}

chunk.blocks[x][z][y] = block
end
end

if x % 10 == 0 then
end
end

for x = 0, Terrain.chunkSize.X do
for z = 0, Terrain.chunkSize.Z do
for y = 0, Terrain.chunkSize.Y do
local block = chunk.blocks[x][z][y]

if block.material ~= "air" then
--[[local topBlock = chunk.blocks[x][z][y + 1]
if not topBlock or topBlock.material == "air" then
block.visible = true
end]]
else
for neighbor_x = -1, 1 do
for neighbor_y = -1, 1 do
for neighbor_z = -1, 1 do
if neighbor_z ~= 0 or not (neighbor_z == neighbor_x and neighbor_z == neighbor_y and neighbor_x == neighbor_y) then
local real_x = x + neighbor_x
local real_y = y + neighbor_y
local real_z = z + neighbor_z

if chunk.blocks[real_x] and chunk.blocks[real_x][real_z] then
local neighbor = chunk.blocks[real_x][real_z][real_y]
if neighbor then
neighbor.visible = true
end
end
end
end
end
end
end
end
end

if x % 10 == 0 then
end
end

self.chunks[position.X][position.Y][position.Z] = chunk
onFinished()
end

local chunk = self.chunks[position.X][position.Y][position.Z]

for x = 0, Terrain.chunkSize.X do
for z = 0, Terrain.chunkSize.Z do
for y = 0, Terrain.chunkSize.Y do
local block = chunk.blocks[x][z][y]
local real_x = (position.X * Terrain.chunkSize.X + x) * Terrain.blockSize
local real_y = (position.Y * Terrain.chunkSize.Y + y) * Terrain.blockSize
local real_z = (position.Z * Terrain.chunkSize.Z + z) * Terrain.blockSize

if block.visible and block.material ~= "air" then
local Part = Instance.new("Part")
Part.Anchored = true
Part.Size = Vector3.new(Terrain.blockSize, Terrain.blockSize, Terrain.blockSize)
Part.CanCollide = true
Part.Position = Vector3.new(real_x, real_y, real_z)
Part.Parent = workspace
end
end
end

if x % 25 == 0 then
end
end

onFinished()
end