Hello, everyone!
This is my first ever tutorial here, so feel free to tell me some tips on how to improve it (or criticize it as much as you can (plz no))
My english is also not the best with sometimes very limited vocabulary, so I apologize for all those grammar or other related mistakes I’ve definitely made in this text.
You’ve probably seen all those voxel engines (mostly MC copies) that are running extremely slow.
Well, in this tutorial series I will show you how to make one but FAST. (No, no, I mean voxel engine, not MC copy)
For comparison, this is a voxel engine I’ve made a while back. This is the speed we want to reach.
I expect that you have some experience with scripting, because I won’t be explaining the basics (like how tables work etc.) I will mostly focus on the algorithms themself and optimizations.
So let’s start with the most basic model.
Create a ‘Local Script’ and put it wherever you want (I recommend StarterGui (don’t kill me plz) because it’s going to be useful later) and open it
We want some cubes so let’s define a size for them:
local BLOCK_SIZE = 3
Now we can create some, so let’s do that in the most simple way:
for x = 1,16 do
for y = 1,16 do
for z = 1,16 do
local part = Instance.new("Part")
part.Anchored = true
part.Size = Vector3.new(BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
part.Position = Vector3.new(x, y, z) * BLOCK_SIZE
part.Parent = workspace
end
end
end
So basicly we loop through the x-, y- and z-axis, set every part’s size to our BLOCK_SIZE
and offset them them by multiplying the position with the BLOCK_SIZE
. This creates a big cube made out of smaller cubes. Let’s call that a chunk
. And because the world is made out of chunks, we should also define a size for them, in this case 16, so let’s add that.
local CHUNK_SIZE = 16
Now we can replace those 16s with the CHUNK_SIZE
. Our script should look like this so far:
local CHUNK_SIZE = 16
local BLOCK_SIZE = 3
for x = 1,CHUNK_SIZE do
for y = 1,CHUNK_SIZE do
for z = 1,CHUNK_SIZE do
local part = Instance.new("Part")
part.Anchored = true
part.Size = Vector3.new(BLOCK_SIZE,BLOCK_SIZE,BLOCK_SIZE)
part.Position = Vector3.new(x, y, z) * BLOCK_SIZE
part.Parent = workspace
end
end
end
Okay. We have a cube. But how do we add real terrain generation?
It is possible to do it in an extremely simple way, but we won’t do that here. You will see later why.
(The simple way for anyone wondering)
local CHUNK_SIZE = 16
local BLOCK_SIZE = 3
for x = 1,CHUNK_SIZE do
for z = 1,CHUNK_SIZE do
local noise = (math.clamp(math.noise(x/10,z/10,123.4),-1,1) + 1) * CHUNK_SIZE * 0.5
noise = math.round(noise)
for y = 1,noise do
local part = Instance.new("Part")
part.Anchored = true
part.Size = Vector3.new(BLOCK_SIZE,BLOCK_SIZE,BLOCK_SIZE)
part.Position = Vector3.new(x, y, z) * BLOCK_SIZE
part.Parent = workspace
end
end
end
So first let’s create a table that will store the data for each block. But what data?
All we need is just an id, from which we can tell what block it is.
So let’s create a table that holds information for all blocks in our chunk.
We well do it similary to how we created the blocks.
First create a new table with the size of our chunk
local data = table.create(CHUNK_SIZE)
Now that let’s add the other dimensions to the table by looping through and creating a new table for each index.
for x = 1,CHUNK_SIZE do
data[x] = table.create(CHUNK_SIZE)
for y = 1,CHUNK_SIZE do
data[x][y] = table.create(CHUNK_SIZE, 0)
end
end
Now we can access each block’s id like this:
local id = data[x][y][z]
With this data we should now edit our generation part so it works with the data.
for x = 1,CHUNK_SIZE do
for y = 1,CHUNK_SIZE do
for z = 1,CHUNK_SIZE do
local id = data[x][y][z]
if id ~= 0 then
local part = Instance.new("Part")
part.Anchored = true
part.Size = Vector3.new(BLOCK_SIZE,BLOCK_SIZE,BLOCK_SIZE)
part.Position = Vector3.new(x, y, z) * BLOCK_SIZE
part.Parent = workspace
end
end
end
end
Let’s try it out!
…
nothing?
Yes, because we inicialized our data to 0 and 0 will be the id for air.
Let’s add some terrain generation now so we can see that it works.
How can we do that?
Well, we will use our data and edit it’s values, so if we want a block we set it to 1 otherwise we set it to 0! It’s that simple.
To generate some terrain we can use math.noise
which we will use to determine the height.
I’m not going to go over every detail on how it works, but in a nutshell we give it coordinates and seed and it gives us a height for that specific block. If you want to read more about it I would recommend looking at this tutorial for basic understanding and this tutorial for more advanced noise.
So we need a height for each column of blocks. We can start by looping through the x and z axis and generating the height.
for x = 1,CHUNK_SIZE do
for z = 1,CHUNK_SIZE do
local height = math.clamp(math.noise(x / 10, z / 10, 123.4),-1,1) --10 for scaling, 123.4 as seed
end
end
This will give us a number between -1 and 1. We can now use that like this for example:
height += 1 --Now the range is between 0-2
height *= CHUNK_SIZE --Now 0-(2 * CHUNK_SIZE)
height /= 2 -- Now 0-(2 * CHUNK_SIZE / 2) => 0-CHUNK_SIZE
We should also round it to make it aligned to the grid by rounding it.
height = math.round(height)
We have the height. So let’s edit the data so we finally get some blocks.
Let’s loop on the y-axis from 1 to the height and set all data in the range to 1.
for y = 1,height do
data[x][y][z] = 1
end
The final code for generating should look like this:
for x = 1,CHUNK_SIZE do
for z = 1,CHUNK_SIZE do
local height = math.clamp(math.noise(x / 10, z / 10, 123.4),-1,1) --10 for scaling, 123.4 as seed
height += 1 --Now the range is between 0-2
height *= CHUNK_SIZE --Now 0-(2 * CHUNK_SIZE)
height /= 2 -- Now 0-(2 * CHUNK_SIZE / 2) => 0-CHUNK_SIZE
height = math.round(height)
for y = 1,height do
data[x][y][z] = 1
end
end
end
Okay, so this is what we got so far:
local CHUNK_SIZE = 16
local BLOCK_SIZE = 3
local data = table.create(CHUNK_SIZE)
for x = 1,CHUNK_SIZE do
data[x] = table.create(CHUNK_SIZE)
for y = 1,CHUNK_SIZE do
data[x][y] = table.create(CHUNK_SIZE, 0)
end
end
for x = 1,CHUNK_SIZE do
for z = 1,CHUNK_SIZE do
local height = math.clamp(math.noise(x / 10, z / 10, 123.4),-1,1) --10 for scaling, 123.4 as seed
height += 1 --Now the range is between 0-2
height *= CHUNK_SIZE --Now 0-(2 * CHUNK_SIZE)
height /= 2 -- Now 0-(2 * CHUNK_SIZE / 2) => 0-CHUNK_SIZE
height = math.round(height)
for y = 1,height do
data[x][y][z] = 1
end
end
end
for x = 1,CHUNK_SIZE do
for y = 1,CHUNK_SIZE do
for z = 1,CHUNK_SIZE do
local id = data[x][y][z]
if id ~= 0 then
local part = Instance.new("Part")
part.Anchored = true
part.Size = Vector3.new(BLOCK_SIZE,BLOCK_SIZE,BLOCK_SIZE)
part.Position = Vector3.new(x, y, z) * BLOCK_SIZE
part.Parent = workspace
end
end
end
end
That’s all nice, but it can generate just one chunk. It should be able to create more of them. So let’s divide our code to functions and use them to create more chunks. That should be pretty straightforward. We are creating data, generating terrain data and creating the terrain it self.
local CHUNK_SIZE = 16
local BLOCK_SIZE = 3
local function createData()
local data = table.create(CHUNK_SIZE)
for x = 1,CHUNK_SIZE do
data[x] = table.create(CHUNK_SIZE)
for y = 1,CHUNK_SIZE do
data[x][y] = table.create(CHUNK_SIZE, 0)
end
end
return data
end
local function generateTerrainData(data)
for x = 1,CHUNK_SIZE do
for z = 1,CHUNK_SIZE do
local height = math.clamp(math.noise(x / 10, z / 10, 123.4),-1,1) --10 for scaling, 123.4 as seed
height += 1 --Now the range is between 0-2
height *= CHUNK_SIZE --Now 0-(2 * CHUNK_SIZE)
height /= 2 -- Now 0-(2 * CHUNK_SIZE / 2) => 0-CHUNK_SIZE
height = math.round(height)
for y = 1,height do
data[x][y][z] = 1
end
end
end
--No need to return data, tables are passed as references
-- ==>
--[[
function edit(tab)
tab[1] = 8
end
local tab = {1,2,3}
edit(tab)
print(tab) --> {8,2,3}
]]
end
local function createTerrain(data)
for x = 1,CHUNK_SIZE do
for y = 1,CHUNK_SIZE do
for z = 1,CHUNK_SIZE do
local id = data[x][y][z]
if id ~= 0 then
local part = Instance.new("Part")
part.Anchored = true
part.Size = Vector3.new(BLOCK_SIZE,BLOCK_SIZE,BLOCK_SIZE)
part.Position = Vector3.new(x, y, z) * BLOCK_SIZE
part.Parent = workspace
end
end
end
end
end
Now we can combine them together and render that chunk again. It can be done like so:
local function createChunk()
local data = createData()
generateTerrainData(data)
createTerrain(data)
end
Oh wait, the chunks have different positions, which means we must account for that. We can pass the chunk position
to the createChunk
function as an argument and give it to other functions. The chunk position will be equal to floor(position / CHUNK_SIZE / BLOCK_SIZE)
to make it easier to work with later on.
Let’s start by editing the createTerrain function because it’s the easiest thing to start with. We know the formula for calculating chunk position so we can just reverse it to get the base position from it like so: base_position = chunk_position * CHUNK_SIZE * BLOCK_SIZE
. Now we can just add it to the block’s position and we have everything we need. It’s time to do that in code.
local function createTerrain(data, chunk_position)
local base_position = chunk_position * CHUNK_SIZE * BLOCK_SIZE
for x = 1,CHUNK_SIZE do
for y = 1,CHUNK_SIZE do
for z = 1,CHUNK_SIZE do
local id = data[x][y][z]
if id ~= 0 then
local part = Instance.new("Part")
part.Anchored = true
part.Size = Vector3.new(BLOCK_SIZE,BLOCK_SIZE,BLOCK_SIZE)
part.Position = base_position + Vector3.new(x, y, z) * BLOCK_SIZE
part.Parent = workspace
end
end
end
end
end
The next thing we need to change is the generateTerrainData
function, otherwise it will result in one chunk repeating over and over again. This is a little bit trickier but still pretty straightforward. First we must offset the x
and z
values we give to math.noise. It can be done like so:
local offsetX = x + chunk_position.X * CHUNK_SIZE
local offsetZ = z + chunk_position.Z * CHUNK_SIZE
And for the y-axis we can just subtract from the height and clamp it to a valid range like so:
height -= chunk_position.Y * CHUNK_SIZE
height = math.min(height,CHUNK_SIZE)
With this added the final function should look like this:
local function generateTerrainData(data, chunk_position)
for x = 1,CHUNK_SIZE do
for z = 1,CHUNK_SIZE do
local offsetX = x + chunk_position.X * CHUNK_SIZE
local offsetZ = z + chunk_position.Z * CHUNK_SIZE
local height = math.clamp(math.noise(offsetX / 10, offsetZ / 10, 123.4),-1,1) --10 for scaling, 123.4 as seed
height += 1 --Now the range is between 0-2
height *= CHUNK_SIZE --Now 0-(2 * CHUNK_SIZE)
height /= 2 -- Now 0-(2 * CHUNK_SIZE / 2) => 0-CHUNK_SIZE
height = math.round(height)
height -= chunk_position.Y * CHUNK_SIZE
height = math.min(height,CHUNK_SIZE)
for y = 1,height do
data[x][y][z] = 1
end
end
end
end
And let’s not forget to change the createChunk
function which should now look like this:
local function createChunk(chunk_position)
local data = createData()
generateTerrainData(data, chunk_position)
createTerrain(data, chunk_position)
end
I think it’s time to test it and see if it works so let’s make a 3,3,3 cube of chunks with simple for loops:
for x = -1,1 do
for y = -1,1 do
for z = -1,1 do
createChunk(Vector3.new(x,y,z))
end
end
end
The result is great, but it really took some time to load. This is because our method of generating chunks is very VERY SLOW and we don’t want that. We want to make it FAST. There are multiple methods to achieve this like block culling or greedy meshing. Sadly greedy meshing won’t work really well with textures (I’m not using Texture/Decal/Anything like this instance for that and therefore can’t do greedy meshing. These Instances are extremly slow too and make the greedy mesher worthless in this case.) Both of these methods aim at redusing the amount of parts created, in our case of block culling, not creating blocks the player can’t see. Let’s see how much will the part count decrease with this. Right now, it creates 55049 parts.
So how does this algorithm work? It’s simple. Loop through chunk data and see if a block is neighbouring with air. If so, then create it, otherwise don’t. There’s nothing more to it.
Let’s do that in code. First we should create a new function to handle that. Let’s call it cullData
. Inside the function we can loop through the data with again simple for loops like this:
local function cullData(data)
for x = 1,CHUNK_SIZE do
for y = 1,CHUNK_SIZE do
for z = 1,CHUNK_SIZE do
end
end
end
end
Inside the loops we get the block’s index from the x,y,z
values and it’s id from data[x][y][z]
. Let’s now get the neighbours of that block. We can get them from x+1, x-1, y+1, y-1, z+1, z-1
. This is how it looks like in code:
--n_id = neighbour_id
local n_id1 = data[x+1][y][z]
local n_id2 = data[x-1][y][z]
local n_id3 = data[x][y+1][z]
local n_id4 = data[x][y-1][z]
local n_id5 = data[x][y][z+1]
local n_id6 = data[x][y][z-1]
Now let’s check if any of these is an air block. Remember the id for air? We set it to be 0 so it can just check if any of the neighbours are zero. This is the code for that:
if
n_id1 == 0 or
n_id2 == 0 or
n_id3 == 0 or
n_id4 == 0 or
n_id5 == 0 or
n_id6 == 0
then
end
Okay we have all that. Now we just need to somehow make it not create blocks. This is simple. We will just create new data and store the culled information there. We will write the block id to the culled data if it neighbours with air, otherwise we set it to 0. Here’s how it can look in code:
local function cullData(data)
local culled_data = createData()
for x = 1,CHUNK_SIZE do
for y = 1,CHUNK_SIZE do
for z = 1,CHUNK_SIZE do
local my_id = data[x][y][z]
--n_id = neighbour_id
local n_id1 = data[x+1][y][z]
local n_id2 = data[x-1][y][z]
local n_id3 = data[x][y+1][z]
local n_id4 = data[x][y-1][z]
local n_id5 = data[x][y][z+1]
local n_id6 = data[x][y][z-1]
if
n_id1 == 0 or
n_id2 == 0 or
n_id3 == 0 or
n_id4 == 0 or
n_id5 == 0 or
n_id6 == 0
then
culled_data[x][y][z] = my_id
else
culled_data[x][y][z] = 0
end
end
end
end
return culled_data
end
There’s still one problem tho because 1-1 is 0 and 0 is not a valid index for our array. That means it will error if we don’t stop it somehow. Again we can avoid it with really simple if statement. If x,y or z is 1 or CHUNK_SIZE, don’t check for neighbours. That means we should probaly write the data of that block to the culled data if the condition isn’t met because otherwise it will end up creating spaces with no blocks between the chunks.
Now it will defenitely work but there’s a small trick to make it faster (sometimes by a lot) and it’s also really simple. When we create the data it is defaultly initialized to zero so we can skip the cycle if my_id
is equal to zero. This saves us time because we don’t write to the culled_data
table.
Now if we combine all this with our existing code, it should end up looking like this:
local function cullData(data)
local culled_data = createData()
for x = 1,CHUNK_SIZE do
for y = 1,CHUNK_SIZE do
for z = 1,CHUNK_SIZE do
local my_id = data[x][y][z]
if my_id == 0 then
continue
end
if
x == 1 or y == 1 or z == 1 or
x == CHUNK_SIZE or y == CHUNK_SIZE or z == CHUNK_SIZE
then
culled_data[x][y][z] = my_id
else
--n_id = neighbour_id
local n_id1 = data[x+1][y][z]
local n_id2 = data[x-1][y][z]
local n_id3 = data[x][y+1][z]
local n_id4 = data[x][y-1][z]
local n_id5 = data[x][y][z+1]
local n_id6 = data[x][y][z-1]
if
n_id1 == 0 or
n_id2 == 0 or
n_id3 == 0 or
n_id4 == 0 or
n_id5 == 0 or
n_id6 == 0
then
culled_data[x][y][z] = my_id
else
culled_data[x][y][z] = 0
end
end
end
end
end
return culled_data
end
Let’s put it in the createChunk
function like so:
local function createChunk(chunk_position)
local data = createData()
generateTerrainData(data, chunk_position)
data = cullData(data)
createTerrain(data, chunk_position)
end
and see the difference in part count. Remember last time we tested it, it created 55049 parts.
And the result is… 19941 parts! That decreased a lot! (That’s around 36% of the original count.)
Now, I know it can be decreased much more but I will explain this in another part (maybe part 2-3?) because it can get really complicated and is just too long for this already long enough part.
Okay, we have a decently fast chunk generator. But let’s make it generate chunks around the player. But for this we must first modify our code a littlebit and make it remember which chunks are loaded. This will be very simple too. We will just modify the createTerrain
function to put the blocks into a model and store a reference to that model in a table. (This is much faster than :FindFistChild because the table is a hashmap)
Let’s start by creating a model for the parts.
local parent = Instance.new("Model")
Then we will change the .Parent of the parts
part.Parent = parent
Now we need to store the models in a table so let’s create one at the top of the script and name it LOADED_CHUNKS
. Now we can just store the reference to the model in the table like so:
LOADED_CHUNKS[chunk_position] = parent
And let’s not forget to parent the model to workspace (or a folder named chunks or anything else in workspace)
parent.Parent = workspace
This is the final look of the createTerrain
function:
local function createTerrain(data, chunk_position)
local base_position = chunk_position * CHUNK_SIZE * BLOCK_SIZE
local parent = Instance.new("Model")
for x = 1,CHUNK_SIZE do
for y = 1,CHUNK_SIZE do
for z = 1,CHUNK_SIZE do
local id = data[x][y][z]
if id ~= 0 then
local part = Instance.new("Part")
part.Anchored = true
part.Size = Vector3.new(BLOCK_SIZE,BLOCK_SIZE,BLOCK_SIZE)
part.Position = base_position + Vector3.new(x, y, z) * BLOCK_SIZE
part.Parent = parent
end
end
end
end
LOADED_CHUNKS[chunk_position] = parent
parent.Parent = workspace
end
Now we are ready to start generating chunks around player. But first we need to get the chunk position of the player. We can do that with a simple function:
local function getPlayersChunkPosition()
if not game.Players.LocalPlayer.Character or not game.Players.LocalPlayer.Character:FindFirstChild("HumanoidRootPart") then
return
end
local root = game.Players.LocalPlayer.Character.HumanoidRootPart
local pos = root.Position / CHUNK_SIZE / BLOCK_SIZE
return Vector3.new(math.floor(pos.X),math.floor(pos.Y),math.floor(pos.Z))
end
I think there’s no need to explain this and I believe you can figure out how it works.
The next step is to create a loop that checks for existing chunks around the player and if there isn’t one, generate it. Here’s a function for that which should be self explanatory:
local function generateChunksAroundPlayer()
local position = getPlayersChunkPosition()
for x = -1,1 do
for y = -1,1 do
for z = -1,1 do
local targetPos = position + Vector3.new(x,y,z)
if not LOADED_CHUNKS[targetPos] then
createChunk(targetPos)
end
end
end
end
end
That’s nice, we can generate chunks around a player. But we also need to remove them if they are far enough. This can be done for example like this, which again should be self explanatory:
local function removeChunksFarFromPlayer()
local playerPos = getPlayersChunkPosition()
for position, instance in LOADED_CHUNKS do
local diff = position - playerPos
diff = Vector3.new(math.abs(diff.X),math.abs(diff.Y),math.abs(diff.Z))
if diff.X > 2 or diff.Y > 2 or diff.Z > 2 then
instance:Destroy()
LOADED_CHUNKS[position] = nil
end
end
end
After that the last step is to put it in a while loop and we’re done!
while task.wait(0) do
if not getPlayersChunkPosition() then
continue
end
generateChunksAroundPlayer()
removeChunksFarFromPlayer()
end
Or are we? It seems like it’s lagging everytime we generate new chunks. We can fix that by optimizing the code (part 2-3?) or spreading the tasks to multiple frames. I like to use a simple function to check if the code has exceeded allowed time per frame and if so then wait for the next frame. Here is the code for that:
local lastWait = os.clock()
local function waitInNecessary()
if os.clock() - lastWait > 0.005 then --5 ms
task.wait(0)
lastWait = os.clock()
end
end
local function resetTimings()
lastWait = os.clock()
end
Now we can just put it in a few places in the code and we are done!
The final code should look like this:
local CHUNK_SIZE = 16
local BLOCK_SIZE = 3
local LOADED_CHUNKS = {}
local lastWait = os.clock()
local function waitInNecessary()
if os.clock() - lastWait > 0.005 then
task.wait(0)
lastWait = os.clock()
end
end
local function resetTimings()
lastWait = os.clock()
end
local function createData()
local data = table.create(CHUNK_SIZE)
for x = 1,CHUNK_SIZE do
data[x] = table.create(CHUNK_SIZE)
for y = 1,CHUNK_SIZE do
data[x][y] = table.create(CHUNK_SIZE, 0)
end
end
return data
end
local function generateTerrainData(data, chunk_position)
for x = 1,CHUNK_SIZE do
for z = 1,CHUNK_SIZE do
local offsetX = x + chunk_position.X * CHUNK_SIZE
local offsetZ = z + chunk_position.Z * CHUNK_SIZE
local height = math.clamp(math.noise(offsetX / 10, offsetZ / 10, 123.4),-1,1) --10 for scaling, 123.4 as seed
height += 1 --Now the range is between 0-2
height *= CHUNK_SIZE --Now 0-(2 * CHUNK_SIZE)
height /= 2 -- Now 0-(2 * CHUNK_SIZE / 2) => 0-CHUNK_SIZE
height = math.round(height)
height -= chunk_position.Y * CHUNK_SIZE
height = math.min(height,CHUNK_SIZE)
for y = 1,height do
data[x][y][z] = 1
end
end
waitInNecessary()
end
end
local function createTerrain(data, chunk_position)
local base_position = chunk_position * CHUNK_SIZE * BLOCK_SIZE
local parent = Instance.new("Model")
for x = 1,CHUNK_SIZE do
for y = 1,CHUNK_SIZE do
for z = 1,CHUNK_SIZE do
local id = data[x][y][z]
if id ~= 0 then
local part = Instance.new("Part")
part.Anchored = true
part.Size = Vector3.new(BLOCK_SIZE,BLOCK_SIZE,BLOCK_SIZE)
part.Position = base_position + Vector3.new(x, y, z) * BLOCK_SIZE
part.Parent = parent
waitInNecessary()
end
end
end
end
LOADED_CHUNKS[chunk_position] = parent
parent.Parent = workspace
end
local function cullData(data)
local culled_data = createData()
for x = 1,CHUNK_SIZE do
for y = 1,CHUNK_SIZE do
for z = 1,CHUNK_SIZE do
local my_id = data[x][y][z]
if my_id == 0 then
continue
end
if
x == 1 or y == 1 or z == 1 or
x == CHUNK_SIZE or y == CHUNK_SIZE or z == CHUNK_SIZE
then
culled_data[x][y][z] = my_id
else
--n_id = neighbour_id
local n_id1 = data[x+1][y][z]
local n_id2 = data[x-1][y][z]
local n_id3 = data[x][y+1][z]
local n_id4 = data[x][y-1][z]
local n_id5 = data[x][y][z+1]
local n_id6 = data[x][y][z-1]
if
n_id1 == 0 or
n_id2 == 0 or
n_id3 == 0 or
n_id4 == 0 or
n_id5 == 0 or
n_id6 == 0
then
culled_data[x][y][z] = my_id
else
culled_data[x][y][z] = 0
end
end
end
end
waitInNecessary()
end
return culled_data
end
local function createChunk(chunk_position)
local data = createData()
generateTerrainData(data, chunk_position)
data = cullData(data)
createTerrain(data, chunk_position)
end
local function getPlayersChunkPosition()
if not game.Players.LocalPlayer.Character or not game.Players.LocalPlayer.Character:FindFirstChild("HumanoidRootPart") then
return
end
local root = game.Players.LocalPlayer.Character.HumanoidRootPart
local pos = root.Position / CHUNK_SIZE / BLOCK_SIZE
return Vector3.new(math.floor(pos.X),math.floor(pos.Y),math.floor(pos.Z))
end
local function generateChunksAroundPlayer()
local position = getPlayersChunkPosition()
for x = -1,1 do
for y = -1,1 do
for z = -1,1 do
local targetPos = position + Vector3.new(x,y,z)
if not LOADED_CHUNKS[targetPos] then
createChunk(targetPos)
end
end
end
end
end
local function removeChunksFarFromPlayer()
local playerPos = getPlayersChunkPosition()
for position, instance in LOADED_CHUNKS do
local diff = position - playerPos
diff = Vector3.new(math.abs(diff.X),math.abs(diff.Y),math.abs(diff.Z))
if diff.X > 2 or diff.Y > 2 or diff.Z > 2 then
instance:Destroy()
LOADED_CHUNKS[position] = nil
waitInNecessary()
end
end
end
while task.wait(0) do
if not getPlayersChunkPosition() then
continue
end
resetTimings()
generateChunksAroundPlayer()
removeChunksFarFromPlayer()
end
Now you can also play with the render distance in generateChunksAroundPlayer
and removeChunksFarFromPlayer
or other variables and see what works!
Here’s what render distance 2 looks like:
That’s all for now. I hope you enjoyed or learned something.
I am also planning to make part 2 for this where I would go over more optimizations, multithreading or texturing the parts. This could also be a really large topic so there might be part 3 for this and part 4 for micro optimizations and at the end reaching very good speed. (There could also be more parts, who knows… If I talk too much about some stuff…)
Have a nice day!
matulo2