Procedural block terrain generator

wait(6)--idk i let my studio load first before everything runs.

--Settings--
local Settings = {
	BlockSize = 6,--Size of blocks measured in studs.
	NoiseScale = 14,--Noise scale thing... Idk how to explain this.
	ChunkSize = 16,--Size of chunks measured in blocks.
	WorldMaxHight = 100,--Max world height in blocks that will be allowed to generate.
	WorldBaseHight = 50,--Average world height in blocks that the terrain will be.
}
--Settings--

--Services--
local RaritySytem = require(game.ServerScriptService.RaritySystem)--Rarity module that will be used for ore rarities.
--Services--

--Constants--
local Blocks = game.ReplicatedStorage.Blocks--Blocks that will be used.
local MaxOpperations = 128000--Thing that I used in previous iterations to not crash the server on worldgen... I want to make it so I dont need this anymore.
local worldseed = math.random(-1000000,1000000)--World seed for unique world generations.
--Constants--

--Values--
local WorldChunks = {}--All generated chunks are stored in this table.
local Opperations = MaxOpperations--Current opperations left before cooldown.
--Values--

--Functions--
function RunServerCooldown()--Runs the cooldowns system thing mentioned before.
	if Opperations > 0 then
		Opperations = Opperations - 1
		--print(Opperations)
	else
		Opperations = MaxOpperations
		game:GetService("RunService").Heartbeat:Wait()
		--print("Cooldown")
	end
end

function ChangeMessage(MessageText)--Unused at the moment.
	for index,child in pairs (game.Players:GetChildren()) do
		if child:IsA("Player") then

			if child.PlayerGui:FindFirstChild("MessageGui") ~= nil then child.PlayerGui:FindFirstChild("MessageGui"):Destroy() end

			if MessageText ~= false and MessageText ~= nil then
				local gui = script.MessageGui:Clone()
				gui.Parent = child.PlayerGui
				gui.TextLabel.Text = MessageText
				gui.Enabled = true
			end

		end
	end
end

function GetChunk(Xpos:number,Zpos:number)--Gets a spesific chunk with the cordinates.
	--print(WorldChunks)
	for index = 1,#WorldChunks do
		local Chunk = WorldChunks[index]
		if Chunk.Position == Vector2.new(Xpos,Zpos) then
			return Chunk
		end
	end
	return nil
end

function FindNeighborBlock(xGridPos,zGridPos,xPos,zPos,yPos,TargetBlockType:string)--Checks if there is a spesific block in contact with the targeted block.

	local Chunk = GetChunk(xGridPos,zGridPos)
	if Chunk then
		local Grid = Chunk.Grid

		--print("Pos = "..tostring(xPos)..", "..tostring(yPos)..", "..tostring(zPos))
		--print(Grid)

		local TargetLocation = Grid[xPos][zPos][yPos]

		if Grid[xPos][zPos][yPos+1] ~= nil and Grid[xPos][zPos][yPos+1].BlockName == TargetBlockType then--Up
			--print("Found "..TargetBlockType.."!")
			return true
		end

		if Grid[xPos][zPos][yPos-1] ~= nil and Grid[xPos][zPos][yPos-1].BlockName == TargetBlockType then--Down
			--print("Found "..TargetBlockType.."!")
			return true
		end

		if zPos >= Settings.ChunkSize then--Back
			local NextChunk = GetChunk(xGridPos,zGridPos + 1)
			if NextChunk then
				local NextGrid = NextChunk.Grid
				if NextGrid then
					if NextGrid[xPos][1][yPos] ~= nil and NextGrid[xPos][1][yPos].BlockName == TargetBlockType then
						--print("Found "..LookingFor.."!")
						return true
					end
				else
					--error("Tried to find a chunk that does not exist yet or has an invalid position!")
					warn("Tried to find a chunk that does not exist yet or has an invalid position!")
				end
			end
		else
			if Grid[xPos][zPos + 1][yPos] ~= nil and Grid[xPos][zPos + 1][yPos].BlockName == TargetBlockType then
				--print("Found "..TargetBlockType.."!")
				return true
			end
		end

		if zPos <= 1 then--Forward
			local NextChunk = GetChunk(xGridPos,zGridPos - 1)
			if NextChunk then
				local NextGrid = NextChunk.Grid
				if NextGrid then
					if NextGrid[xPos][Settings.ChunkSize][yPos] ~= nil and NextGrid[xPos][Settings.ChunkSize][yPos].BlockName == TargetBlockType then
						--print("Found "..LookingFor.."!")
						return true
					end
				else
					--error("Tried to find a chunk that does not exist yet or has an invalid position!")
					warn("Tried to find a chunk that does not exist yet or has an invalid position!")
				end
			end
		else
			if Grid[xPos][zPos - 1][yPos] ~= nil and Grid[xPos][zPos - 1][yPos].BlockName == TargetBlockType then
				--print("Found "..TargetBlockType.."!")
				return true
			end
		end

		if xPos >= Settings.ChunkSize then--Left
			local NextChunk = GetChunk(xGridPos + 1,zGridPos)
			if NextChunk then
				local NextGrid = NextChunk.Grid
				if NextGrid then
					if NextGrid[1][zPos][yPos] ~= nil and NextGrid[1][zPos][yPos].BlockName == TargetBlockType then
						--print("Found "..LookingFor.."!")
						return true
					end
				else
					--error("Tried to find a chunk that does not exist yet or has an invalid position!")
					warn("Tried to find a chunk that does not exist yet or has an invalid position!")
				end
			end
		else
			if Grid[xPos + 1][zPos][yPos] ~= nil and Grid[xPos + 1][zPos][yPos].BlockName == TargetBlockType then
				--print("Found "..TargetBlockType.."!")
				return true
			end
		end

		if xPos <= 1 then--Right
			local NextChunk = GetChunk(xGridPos - 1,zGridPos)
			if NextChunk then
				local NextGrid = NextChunk.Grid
				if NextGrid then
					if NextGrid[Settings.ChunkSize][zPos][yPos] ~= nil and NextGrid[Settings.ChunkSize][zPos][yPos].BlockName == TargetBlockType then
						--print("Found "..LookingFor.."!")
						return true
					end
				else
					--error("Tried to find a chunk that does not exist yet or has an invalid position!")
					warn("Tried to find a chunk that does not exist yet or has an invalid position!")
				end
			end
		else
			if Grid[xPos - 1][zPos][yPos] ~= nil and Grid[xPos - 1][zPos][yPos].BlockName == TargetBlockType then
				--print("Found "..TargetBlockType.."!")
				return true
			end
		end
	else
		--error("Tried to find a chunk that does not exist yet or has an invalid position!")
		warn("Tried to find a chunk that does not exist yet or has an invalid position!")
	end

	return false
end

function IsExposedBlock(xGridPos,zGridPos,xPos,zPos,yPos)--Checks if a block is exposed to a defined transparent block. Used for rendering.
	if FindNeighborBlock(xGridPos,zGridPos,xPos,zPos,yPos,"Air") == true then
		--print("Exposed to Air.")
		return true
	end
	if FindNeighborBlock(xGridPos,zGridPos,xPos,zPos,yPos,"Water") == true then
		--print("Exposed to Water.")
		return true
	end
	--print("Not exposed block.")
	return false
end

function GenerateChunk(xGridPos,zGridPos)--The main chunk generation function.
	local ChunkData = {
		Position = Vector2.new(xGridPos,zGridPos),
		Grid = nil,
		Rendered = true,
		ChunkModel = nil,
	}

	local Grid = {}

	for index = 1,Settings.ChunkSize do
		table.insert(Grid,{})--X
	end

	for index = 1,#Grid do
		local Xpos = Grid[index]
		for secondindex = 1,Settings.ChunkSize do
			table.insert(Xpos,{})--Z
		end
	end

	for index = 1,#Grid do
		local Xpos = Grid[index]
		for secondindex = 1,Settings.ChunkSize do
			local Zpos = Xpos[secondindex]
			for thirdindex = 1,Settings.WorldMaxHight do
				RunServerCooldown()
				table.insert(Zpos,{--Y
					--Greedy value things are going to be used for when I attempt to use Greedy Meshing again.
					BlockName = "",
					IsGreedy = false,
					GreedyXsize = 1,
					GreedyYsize = 1,
					GreedyZsize = 1}
				)
			end
		end
	end

	----Height Map----
	--Generates the basic heights of the map using various noise values.

	--Types of noise:
	--Main noise - Primary noise value that creates the basic hills and valeys that will further be modified later.
	--Sharpness noise - Secondary noise that will amplify or dull the intensity of the hills generated by the main noise based on the value given by this noise.
	--Height noise - A subtle noise value that is much larger in size but it controls the main height of certain areas allowing for high areas and low areas such as oceans and high lands.

	local extraXPos = xGridPos * Settings.ChunkSize
	local extraZPos = zGridPos * Settings.ChunkSize

	for Xpos = 1,#Grid do
		--print("X")
		local Xgrid = Grid[Xpos]
		for Zpos = 1,Settings.ChunkSize do
			--print("Z")
			--game:GetService("RunService").Heartbeat:Wait()
			local Zgrid = Xgrid[Zpos]
			for Ypos = 1,Settings.WorldMaxHight do
				--print("Y")
				RunServerCooldown()
				local Ygrid = Zgrid[Ypos]
				--wait(0.5)
				local perlinX = (Xpos + extraXPos) / Settings.NoiseScale
				local perlinZ = (Zpos + extraZPos) / Settings.NoiseScale
				local perlinY = Ypos / Settings.NoiseScale

				local Noise = math.noise(perlinX,worldseed,perlinZ)--Main noise
				Noise = Noise * (2 + math.noise(perlinX * 0.1,worldseed,perlinZ * 0.1))--Sharpness noise
				Noise = Noise + (Settings.WorldBaseHight + math.noise(perlinX * 0.05,worldseed,perlinZ * 0.05) * 30)--Height noise

				--print("Noise = "..noiseValue)

				if Ypos <= Noise then
					Zgrid[Ypos].BlockName = "Noise"
				else
					Zgrid[Ypos].BlockName = "Air"
				end
			end
		end
	end
	----Height Map----

	----Perlin Worms----
	print("Generating perlin worms...")
	for Xpos = 1,#Grid do
		--print("X")
		local Xgrid = Grid[Xpos]
		for Zpos = 1,Settings.ChunkSize do
			--print("Z")
			--game:GetService("RunService").Heartbeat:Wait()
			local Zgrid = Xgrid[Zpos]
			for Ypos = 1,Settings.WorldMaxHight do
				--print("Y")
				RunServerCooldown()
				local Ygrid = Zgrid[Ypos]
				--wait(0.5)
				local perlinX = (Xpos + extraXPos) / Settings.NoiseScale
				local perlinZ = (Zpos + extraZPos) / Settings.NoiseScale
				local perlinY = Ypos / Settings.NoiseScale

				local noiseValue = math.noise(perlinX,perlinY + worldseed,perlinZ)

				--print(noiseValue)
				if noiseValue > (0.5 * (Ypos/Settings.WorldBaseHight)) then
					Zgrid[Ypos].BlockName = "Air"
				end
			end
		end
	end
	print("Perlin worms done.")
	----Perlin Worms----

	ChunkData.Grid = Grid
	table.insert(WorldChunks,ChunkData)

	----Final generation----
	print("Final generation...")
	local ChunkModel = Instance.new("Model")
	ChunkModel.Name = tostring(xGridPos,zGridPos)

	for Xpos = 1,#Grid do
		--print("X")
		local Xgrid = Grid[Xpos]
		for Zpos = 1,Settings.ChunkSize do
			--print("Z")
			--game:GetService("RunService").Heartbeat:Wait()
			local Zgrid = Xgrid[Zpos]
			for Ypos = 1,Settings.WorldMaxHight do
				--print("Y")
				RunServerCooldown()
				local Ygrid = Zgrid[Ypos]
				--wait(0.5)
				if Zgrid[Ypos].BlockName == "Noise" and IsExposedBlock(xGridPos,zGridPos,Xpos,Zpos,Ypos) == true then
					local block = Blocks.Stone:Clone()
					block.Parent = ChunkModel
					block.Size = Vector3.new(Settings.BlockSize, Settings.BlockSize, Settings.BlockSize)
					block.Position = Vector3.new((Xpos + extraXPos), Ypos, (Zpos + extraZPos)) * Settings.BlockSize
					block:SetAttribute("GridPos",Vector3.new(Xpos,Ypos,Zpos))
					--Zgrid[Ypos].BlockName = "Updated"
				end
			end
		end
	end

	ChunkModel.Parent = script.Parent.Terrain
	print("Final generation done.")
	----Final generation----

	ChunkData.ChunkModel = ChunkModel
	ChunkData.Grid = Grid

	--table.insert(WorldChunks,ChunkData)
end
--Functions--

script.Parent.Loaded.Value = false

local findexistingworld = game.Workspace:FindFirstChild("Tfolder")
if findexistingworld ~= nil then
	findexistingworld:Destroy()
end

for _,v in pairs (script.Parent.Terrain:GetChildren()) do
	if v:IsA("BasePart") then
		v:Destroy()
	end
end

for _,v in pairs (script.Parent.Resources:GetChildren()) do
	if v:IsA("Model") then
		v:Destroy()
	end
end

script.Parent.Baseplate.Position = Vector3.new(0, 10, 0)

----Chunk rendering----
--A large majority of this is generated by Chat GPT.
local RenderDistance = 5 -- You can increase this for larger render distances
local UnloadDistance = RenderDistance + 1
local PlayerRenderedChunks = {} -- Track which chunks each player has loaded

while true do
	wait(1) -- Adjust the wait time for better performance if needed
	for _,Player in pairs(game.Players:GetChildren()) do
		if Player:IsA("Player") then
			local Character = Player.Character
			if Character then
				local Humanoid = Character:FindFirstChildOfClass("Humanoid")
				if Humanoid then
					local RootPart = Humanoid.RootPart
					if RootPart then
						-- Calculate chunk position based on player position
						local ChunkPosX = math.floor(RootPart.Position.X / (Settings.BlockSize * Settings.ChunkSize))
						local ChunkPosZ = math.floor(RootPart.Position.Z / (Settings.BlockSize * Settings.ChunkSize))

						-- Create an entry for this player if it doesn't exist
						if not PlayerRenderedChunks[Player] then
							PlayerRenderedChunks[Player] = {}
						end

						-- Iterate over all chunks within render distance for this player
						for Xindex = -RenderDistance, RenderDistance do
							for Zindex = -RenderDistance, RenderDistance do
								local TargetChunkX = ChunkPosX + Xindex
								local TargetChunkZ = ChunkPosZ + Zindex
								local ExistingChunk = GetChunk(TargetChunkX, TargetChunkZ)

								-- If the chunk does not exist, generate it
								if not ExistingChunk then
									--print("Generating chunk at: ", TargetChunkX, TargetChunkZ)
									GenerateChunk(TargetChunkX, TargetChunkZ)
									-- Keep track of loaded chunk for this player
									PlayerRenderedChunks[Player][TargetChunkX .. "," .. TargetChunkZ] = true
								elseif ExistingChunk.Rendered == false then
									-- Reload chunk if it's not rendered
									ExistingChunk.ChunkModel.Parent = script.Parent.Terrain
									ExistingChunk.Rendered = true
									PlayerRenderedChunks[Player][TargetChunkX .. "," .. TargetChunkZ] = true
								end
							end
						end
					end
				end
			end
		end
	end

	-- Unload chunks that no players are nearby
	for index, chunk in pairs(WorldChunks) do
		local chunkX, chunkZ = chunk.Position.X, chunk.Position.Y
		local isChunkNeeded = false

		-- Check if any player still needs this chunk
		for _,Player:Player in pairs(game.Players:GetChildren()) do
			if Player and Player.Character and Player.Character:FindFirstChildOfClass("Humanoid") and Player.Character:FindFirstChildOfClass("Humanoid").RootPart then
				if PlayerRenderedChunks[Player] then
					local ChunkPosX = math.floor(Player.Character.HumanoidRootPart.Position.X / (Settings.BlockSize * Settings.ChunkSize))
					local ChunkPosZ = math.floor(Player.Character.HumanoidRootPart.Position.Z / (Settings.BlockSize * Settings.ChunkSize))

					local distanceX = math.abs(ChunkPosX - chunkX)
					local distanceZ = math.abs(ChunkPosZ - chunkZ)

					if distanceX <= UnloadDistance and distanceZ <= UnloadDistance then
						isChunkNeeded = true
						break -- If one player needs the chunk, stop checking
					end
				end
			end
		end

		-- If no player needs the chunk, unload it
		if not isChunkNeeded and chunk.Rendered == true then
			chunk.Rendered = false
			chunk.ChunkModel.Parent = game.ServerStorage
			--print("Unloading chunk at: ", chunkX, chunkZ)
		end
	end
end
----Chunk rendering----

This is my Procedural block terrain generator that I plan to use for a larger project. It’s the fourth iteration so far with the goal to make it generate an infinite world instead of the limited world it was before. I aimed to do this using Chunks to generate things.

I managed to get a working form of this with some help from Chat GPT specifically with the rendering part. However one primary problem I see is Roblox does not seem to be able to keep up enough to allow for large enough world to be generated. This is before I even add any advanced world generation features like biomes and ore gen, so this is a primary issue that needs to be fixed.

Ultimately, I am not too sure how to make things much better, but I have had thoughts of trying to have rendering done on the client side instead although that might be out of my skill set. I will still try it if it’s my only option.

Basically, I just need optimizations or just better ways to run the code in general. I am open to anything.

Provide an overview of:

  • What does the code do and what are you not satisfied with?
  • What potential improvements have you considered?
  • How (specifically) do you want to improve the code?
1 Like

image

two easy tricks to optimize/beautify ugly code → reduce indentation and avoid nested for loops

use guard clauses and a 1 dimensional array for the grid

(let me know if you need help cleaning up)

3 Likes

If I’m being honest, I don’t understand a single thing you said in more ways than one.
Firstly, what’s a guard clause? Secondly… It’s possible to make the grid be a single table? I don’t think I know how to do that yet. Thirdly, I always thought my code was okay but if it’s really that bad then maybe I should just not rely on whatever Roblox automatically does…
Maybe I do need some help…

1 Like

a guard clause is basically an “early return/continue statement”

if you did something like this

local Bones = {
  ["IsRich"] = false;
  ["IsGenius"] = true;
  ["IsAwesome"] = true;
}
if Bones.IsRich then
  if Bones.IsGenius then
    if Bones.IsAwesome then
      print("Bones is amazing")
    else
      print("Bones is not awesome")
    end
  else
    print("Bones is not a genius")
  end
else
  print("Bones is not rich")
end

this can be harder to maintain since you’d need to look at what each condition and what each statement does

something like this would be a lot better

if not Bones.IsRich then
  print("Bones is not rich")
  return
end

if not Bones.IsGenius then
  print("Bones is not a genius")
  return
end

if not Bones.IsAwesome then
  print("Bones is not awesome")
  return
end

print("Bones is amazing")

(i would even turn short clauses into one-liners)

if not Bones.IsRich then print("Bones is not rich") return end
if not Bones.IsGenius then print("Bones is not a genius") return end
if not Bones.IsAwesome then print("Bones is not awesome") return end

print("Bones is amazing")

there’s a couple videos on youtube explaining this; here’s a short


for the (flattened) 3d grid, it really depends on how you want your block coordinates to be structured
(looks like your grid is doing X, Z, Y, but i’m going to show X, Y, Z with barely any explanation)

local Grid_3D = {
  [1] = {
    [1] = {1, 5, 9, 13};
    [2] = {17, 21, 25, 29};
    [3] = {33, 37, 41, 45};
    [4] = {49, 53, 57, 61};
  };
  [2] = {
    [1] = {2, 6, 10, 14};
    [2] = {18, 22, 26, 30};
    [3] = {34, 38, 42, 46};
    [4] = {50, 54, 58, 62};
  };
  [3] = {
    [1] = {3, 7, 11, 15};
    [2] = {19, 23, 27, 31};
    [3] = {35, 39, 43, 47};
    [4] = {51, 55, 59, 63};
  };
  [4] = {
    [1] = {4, 8, 12, 16};
    [2] = {20, 24, 28, 32};
    [3] = {36, 40, 44, 48};
    [4] = {52, 56, 60, 64};
  };
}

local Grid_1D = {
  [1] = 1,   [2] = 5,   [3] = 9,   [4] = 13,
  [5] = 17,  [6] = 21,  [7] = 25,  [8] = 29,
  [9] = 33,  [10] = 37, [11] = 41, [12] = 45,
  [13] = 49, [14] = 53, [15] = 57, [16] = 61,
  [17] = 2,  [18] = 6,  [19] = 10, [20] = 14,
  [21] = 18, [22] = 22, [23] = 26, [24] = 30,
  [25] = 34, [26] = 38, [27] = 42, [28] = 46,
  [29] = 50, [30] = 54, [31] = 58, [32] = 62,
  [33] = 3,  [34] = 7,  [35] = 11, [36] = 15,
  [37] = 19, [38] = 23, [39] = 27, [40] = 31,
  [41] = 35, [42] = 39, [43] = 43, [44] = 47,
  [45] = 51, [46] = 55, [47] = 59, [48] = 63,
  [49] = 4,  [50] = 8,  [51] = 12, [52] = 16,
  [53] = 20, [54] = 24, [55] = 28, [56] = 32,
  [57] = 36, [58] = 40, [59] = 44, [60] = 48,
  [61] = 52, [62] = 56, [63] = 60, [64] = 64
}

local GridSizeX, GridSizeY, GridSizeZ = 4, 4, 4
local X, Y, Z = 1, 3, 4
local Index = (X - 1) * (GridSizeY * GridSizeZ) + (Y - 1) * GridSizeZ + Z

print("Element at X = 1, Y = 3, Z = 4:", Grid_1D[Index])
print("Element at X = 1, Y = 3, Z = 4:", Grid_3D[X][Y][Z])

-- Output: Element at X = 1, Y = 3, Z = 4: 45 (x2)

using 1 array is possible with calculations

-- Block is a number in this example
-- Returns X, Y, Z Coordinate
local function GetBlockCoordinate(Block: number): (number, number, number)
  local Remainder = (Block - 1) % (GridSizeY * GridSizeZ)
  return (Remainder % GridSizeZ + 1), (math.floor((Block - 1) / (GridSizeY * GridSizeZ)) + 1), (math.floor(Remainder / GridSizeZ) + 1)
end

local function CoordinateToIndex(X: number, Y: number, Z: number): number
  return (X - 1) * (GridSizeY * GridSizeZ) + (Y - 1) * GridSizeZ + Z
end
-- Turn this
for X, Y_Axis in Grid_3D do
  for Y, Z_Axis in Y_Axis do
    for Z, Block in Z_Axis do
      local BlockX, BlockY, BlockZ = GetBlockCoordinate(Block)
      print("Block", Block, "at coordinate:", BlockX, BlockY, BlockZ)
    end
  end
end

-- Into this
for _, Block in Grid_1D do
  local BlockX, BlockY, BlockZ = GetBlockCoordinate(Block)
  print("Block", Block, "at coordinate:", BlockX, BlockY, BlockZ)
end
2 Likes

Sorry I kinda dipped for a while. This might just have to be one of those things I have to come back to at a later point. Sort of just a bit much for me to understand and I rather only use code I can understand. If I decide to work on this again then I will probably use the things here as a reference but for now I will just put this thread to rest. Thanks for the help though.