Dealing with Memory in a voxel-like game

I’ve been working on a voxel-like game over the past few days, and got to a point where I realized I will run into some Memory issues.

The game is structured like this:

  • It generates chunks and saves it on heap (a global variable called “Chunks”)
  • It can be accessed to load/unload the chunk later on

The problem with this is, if there are too many chunks stored on that variable, Roblox will start using lots of memory and will crash when it gets to 4GB. And when this happens, the game gives an error message of “No internet connection”.

Minecraft dealt with this issue by storing the chunks on a file on the computer, and the loaded chunks on the memory. But since this is a Roblox game afterall, I can’t really store it on a file, which leads me to believe this might just be a limitation error.

I have come up with a solution and possibly the only one, which would be to store it on the Roblox database and access it when I load the chunks. I could also compress the chunks, but that will just postpone the error to a later time.

All I want from you, the community, is to help me by giving me some suggestions. Any would be great as I really don’t know if this can even be done in a game like Roblox so I might have to switch to OpenGL :pensive: .

Anyway, thank you for your help!

7 Likes

How does that variable work, is it like a table?

Yes the “Chunks” variable is a table

Converting it to a string or compressing it might help significantly. There are some methods that can help you compress the data and save space.

Thanks, I’ll definitely have a try at that.

How exactly do your chunks look like in data/memory?

Hey robert!
Most voxel games use IDs to store each voxel and info about them, basically serializing them into smaller data types instead of full-on {Name = “Dirt”, Size = vector3(4,4,4)} data.

In your game you could do the same approach such as:

local BlockIDs = {
	Dirt = 0,
	Wood = 1,
	Stone = 2
}

and store them in chunks one by one.

You could also use the new buffer type for each chunk to use even less memory but might end up making the code-base harder to debug.

2 Likes

Buffers are definitely gonna be the ones I’m gonna opt for. Thanks for your help!

1 Like

Happy to help! Good luck in your game!

One optimization method would be to store only positions, or regions.

I’ve dealt with this problem in my game. Here is a stress test I did(that took hours). I was able to generate about 120k blocks(air included) per second, totaling 4B blocks after 9.5 hours.

Optimize your data: Make your chunk tables as small as they can possibly be.
Bunch of blocks with position of XXXX, YYY, ZZZZZ? No, you have a chunk position of XXXX, YYY, ZZZZZ, and all the blocks in that chunk are local to that position, being as small as X, Y, Z.
Blockids should be numbers, make sure the most common blocks are a small number(dirt, air, stone should be 0, 1, 2)

Compress your chunks: Any chunks you dont need immediately(like they are very far from players or no one has been in the area for a long time), need to be compressed. Figure out how to turn your chunk into the smallest compressed table you can, then turn it into a string and compress that further.

Store compressed chunks into a Datastore: At this point your world should be able to hold billions of blocks, so you may not even need to store anything in a Datastore. Your limitations will likely be how fast you can generate blocks, not how much space they take up. But using a Datastore will be the ultimate way of having a world with no real limit, plus you can make it survive server shutdowns.

3 Likes

Thank you guys for your responses, they truly help giving me ideas on how to approach this problem.
I will look into buffers and possibly cleaning up how the whole “Chunks” variable is structured, since it’s kinda inefficient rn.

Since you guys don’t know how I’m actually structuring this, I’ll give you a short summary:

First the chunk

local chunk = 
{
Name = "0 0" -- For example
Blocks = {} -- The blocks being stored
TopBlocks = {} -- The blocks at the top, useful to know where to generate trees
}

Then the block

local block = 
{
Position = Vector3.new(px, py, pz),
BlockID = 1,
Rendered = false
}

It stores the block’s position, blockID, and a boolean “Rendered”. The boolean just let’s the server know if the player can actually see the block.

However I do want to make the blocks variable a Buffer just like this:

local Blocks = buffer.create( ( 4 * 3 + 4 + 4 ) * _G.CHUNK_SIZE * _G.CHUNK_SIZE * _G.HEIGHT_LIMIT ) 

The vector has 3 parameters, each sizing 4 bytes, blockid sizing 4 bytes and rendered aswell, even if the rendered variable will only store 0 for false and 1 for true. Then I multiply the amount of bytes needed by the amount of blocks in a chunk.

I’m sure the buffers will greatly improve memory usage, however I want to hear from you guys aswell.

2 Likes

yeah that’s horrible.

You should try making your chunks like this

chunkTable = {[x]={[z]={[y]=blockid}}}
blockId = chunkTable[x][z][y]

So blockPosition is built into the chunktable tables(x, y, z), and blockId is at the end of that. Recommend not having a block be a whole table, only a single number, if possible. I don’t know what Rendered means, but you should also be doing clientsided rendering entirely. If you dont do that, then soon memory from your world wont be a limitation, but the amount of parts a client will have to render will be.

And in order to have good clientside rendering(decent render distance, good player count, no data spikes), you have to reduce your chunk data down significantly.

1 Like

When I load a chunk it loops through the blocks and checks if the Rendered variable is true. If so then it instantiates a part sizing it and giving it the appropriate materials.

It’s useful knowing if it should render it or not, since the function for checking if it should be rendered is a bit slow if I do it multiple times when loading the chunk.

If you don’t fully understand what it does, I can give a better explanation.

But I’m pretty sure buffers will speed it up a lot, since it’s a continueos line of bytes with data in it. At the end of the day vertices are stored in buffers, the very thing computers allow them to render stuff on our screens.

1 Like

I don’t think that’s necessary, you can store a “Rendered” boolean value inside your chunk table, but not in blocks. Your system should automatically loop through each block number, render them, and after it’s done, set the chunk’s rendered value to true.

2 Likes

You may be doing a Minecraft-like game where there are many world blocks pre-generated. My mining game is a classic Roblox one, where blocks only generate when its neighbor is mined.

If that is the case, then you can work the rendered bool into the blockid. Something like “12340” or “12341”, where the blockid is 1234 and the rendered bool is 0/1. Having a table for every single block will kill your performance.

This is assuming your way of determining what should be rendered is optimized. It may not be and you may not need to store a rendered variable for every single block at all(it does seem inefficient).
The client should calculate their own rendering.

1 Like

Yeah that’s what a buffer does essentially. You can imagine it as a series of values, that can be read by inputting a buffer index, so the size of a block (in bytes) multiplied by the block’s index.

So a chunk’s size will be: (4 * 3 + 4 + 4) * 8 * 8 * 128 = 163,840 bytes or 0.16 mb.

I dont use buffers, so cant comment on that. But you’re gonna need to cut down on data a lot

1 Like