Help on biome generation for Procedural Generation

Hello, I am wondering how I could make biomes generate correctly with my procedural world generation.
I’ve got the basics (with biome gen) but it seems to just be a mess with biomes.
Here is my code:

local RepStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

local DEBUG_MODE = false
local EXPERIMENTAL_MODE = false

local Plr = Players.LocalPlayer
local chunks = {}
local BLOCKS_GENERATED = 0
local BASE_HEIGHT = nil
local CHUNK_SCALE = 16
local RENDER_DISTANCE = 6
local SCALE = 90
local BLOCK_GEN_WAIT = 125*RENDER_DISTANCE
local GENERATION_SEED = math.random()
local LAST_BIOME = "Mountains"
local BEDROCK_LEVELS = {-63, -66}

local BIOME_TYPES = {
	["DESERT_GRASS"] = {
		["TEMP_MIN"] = 0.25,
		["TEMP_MAX"] = 0.75,
	},
	["MOUNTAINS"] = {
		["TEMP_MIN"] = 0.00,
		["TEMP_MAX"] = 0.24,
	},
}

local BIOMES = {
	["Plains"] = {
		["BIOME_NAME"] = "Plains",
		["HEIGHT_LIMIT"] = 5,
		["BIOME_TYPE"] = "DESERT_GRASS",
		["MIN_NOISE"] = 0,
		["MAX_NOISE"] = .34,
	},
	["LowMountains"] = {
		["BIOME_NAME"] = "LowMountains",
		["HEIGHT_LIMIT"] = 15,
		["BIOME_TYPE"] = "MOUNTAINS",
		["MIN_NOISE"] = .35,
		["MAX_NOISE"] = .54,
	},
	["MidMountains"] = {
		["BIOME_NAME"] = "MidMountains",
		["HEIGHT_LIMIT"] = 23,
		["BIOME_TYPE"] = "MOUNTAINS",
		["MIN_NOISE"] = .55,
		["MAX_NOISE"] = .74,
	},
	["Mountains"] = {
		["BIOME_NAME"] = "Mountains",
		["HEIGHT_LIMIT"] = 35,
		["BIOME_TYPE"] = "MOUNTAINS",
		["MIN_NOISE"] = .75,
		["MAX_NOISE"] = 1,
	},
}

local function roundToOdd(n, RoundTo)
	local RoundTo = RoundTo or 3
	return math.floor(n - n % RoundTo);
end

local function chunkExists(chunkX, chunkZ)
	if not chunks[chunkX] then
		chunks[chunkX] = {}
	end
	return chunks[chunkX][chunkZ]
end

local function GET_CHUNK(POS_X, POS_Z)
	local POS_Y = 0
	local CHUNK = nil
	for I, CHUNK_FOUND in pairs(workspace.Chunks:GetChildren()) do
		if CHUNK_FOUND:GetAttribute("CHUNK_POS") == tostring(POS_X..", "..POS_Y..", "..POS_Z) then
			CHUNK = CHUNK_FOUND
		end
	end
	for I, CHUNK_FOUND in pairs(RepStorage.UnloadedChunks:GetChildren()) do
		if CHUNK_FOUND:GetAttribute("CHUNK_POS") == tostring(POS_X..", "..POS_Y..", "..POS_Z) then
			CHUNK = CHUNK_FOUND
		end
	end
	return CHUNK
end

local function LOAD_BLOCK(x, endY, z, BLOCK_NAME, CHUNK)
	local beginY = 0
	local cframe = CFrame.new(x * 3 + 1, roundToOdd((beginY - endY) * 3 / 1), z * 3 + 1)
	local size = Vector3.new(3, (beginY+(endY+BASE_HEIGHT)) * 3, 3)
	local p = Instance.new("Part", CHUNK)
	p.Anchored = true
	p.CFrame = cframe
	p.Size = Vector3.new(3, 3, 3)
	if BLOCK_NAME == "Grass" then
		p.Material = Enum.Material.Grass
		p.BrickColor = BrickColor.new("Forest green")
	elseif BLOCK_NAME == "Dirt" then
		p.Material = Enum.Material.Slate
		p.Color = Color3.new(0.356863, 0.254902, 0.192157)
	elseif BLOCK_NAME == "Stone" then
		p.Material = Enum.Material.Slate
		p.Color = Color3.new(0.458824, 0.458824, 0.458824)
	elseif BLOCK_NAME == "Bedrock" then
		p.Material = Enum.Material.Slate
		p.Color = Color3.new(0.239216, 0.239216, 0.239216)
		p.Position += Vector3.new(0, BEDROCK_LEVELS[math.random(1,2)], 0)
	end
	return p
end

local function GET_NOISE(noise, cx, cz)
	if noise == "HEIGHTMAP" then
		return math.noise(GENERATION_SEED, cx / SCALE, cz / SCALE)
	elseif noise == "TEMPRATURE" then
		return math.noise(GENERATION_SEED, -1+(cx/100), 1+(cz/100))
	end
end

function makeChunk(Xchunk, Zchunk)
	local chunkX, chunkZ = math.floor(Xchunk), math.floor(Zchunk)
	local rootPosition = Vector3.new(chunkX * CHUNK_SCALE, 0, chunkZ * CHUNK_SCALE)
	local CHUNK = Instance.new("Model", workspace.Chunks)
	CHUNK.Name = "Chunk"
	local cx = (chunkX * CHUNK_SCALE)
	local cz = (chunkZ * CHUNK_SCALE)
	local HEIGHTMAP = GET_NOISE("HEIGHTMAP", cx, cz)	--	math.noise(GENERATION_SEED, cx / SCALE, cz / SCALE)
	local BIOME_TEMP = GET_NOISE("TEMPRATURE", cx, cz)	--	math.noise(GENERATION_SEED, 0, 1)
	local REAL_BIOME = nil
	for I, NOISE_BIOME in pairs(BIOMES) do
		local BIOME_TYPE = BIOME_TYPES[NOISE_BIOME["BIOME_TYPE"]]
		if BIOME_TYPE["TEMP_MIN"] <= BIOME_TEMP and BIOME_TYPE["TEMP_MAX"] >= BIOME_TEMP and NOISE_BIOME["MIN_NOISE"] <= HEIGHTMAP  and NOISE_BIOME["MAX_NOISE"] >= HEIGHTMAP then
			REAL_BIOME = NOISE_BIOME["BIOME_NAME"]
		end
	end
	if REAL_BIOME == nil then
		REAL_BIOME = LAST_BIOME
	else
		LAST_BIOME = REAL_BIOME
	end
	if DEBUG_MODE then
	    	warn("HEIGHTMAP - "..HEIGHTMAP)
	    	warn("BIOME TEMP - "..BIOME_TEMP)
	    	warn(REAL_BIOME)
	end
	CHUNK:SetAttribute("BIOME", REAL_BIOME)
	CHUNK:SetAttribute("CHUNK_POS", chunkX..", "..rootPosition.Y..", "..chunkZ)
	chunks[chunkX][chunkZ] = {
		["Chunk"] = CHUNK,
		["Biome"] = REAL_BIOME,
	} -- Acknowledge the chunk's existance.
	local BIOME_INFO = BIOMES[REAL_BIOME]
	for x = 1, CHUNK_SCALE do
	--	for Y=0,-64,-1 do
			for z = 1, CHUNK_SCALE do
				BLOCKS_GENERATED += 1
				local BIOME_HEIGHT = BIOME_INFO["HEIGHT_LIMIT"]
				if HEIGHTMAP < .35 and BIOME_HEIGHT >= 35 then
					BIOME_HEIGHT = 20
				end
				if BASE_HEIGHT ~= nil then
					if BASE_HEIGHT ~= BIOME_HEIGHT then
						if BASE_HEIGHT < BIOME_HEIGHT then
							BASE_HEIGHT += 1
						elseif BASE_HEIGHT > BIOME_HEIGHT then
							BASE_HEIGHT -= 1
						end
					end
				else
					BASE_HEIGHT = BIOME_HEIGHT
				end
				local cy = HEIGHTMAP * BASE_HEIGHT
				local BLOCK_NAME = "Grass"
				if EXPERIMENTAL_MODE then
				    if Y <= BEDROCK_LEVELS[math.random(1,2)] then
				    	BLOCK_NAME = "Bedrock"
				    else
				    	BLOCK_NAME = "Grass"
				    end
				end
				LOAD_BLOCK(cx, cy, cz, BLOCK_NAME, CHUNK)
				cx = (chunkX * CHUNK_SCALE) + x
				cz = (chunkZ * CHUNK_SCALE) + z
				HEIGHTMAP = GET_NOISE("HEIGHTMAP", cx, cz)	--	math.noise(GENERATION_SEED, cx / SCALE, cz / SCALE)
				if BLOCKS_GENERATED > BLOCK_GEN_WAIT then
					wait()
					BLOCKS_GENERATED = 0
				end
			end
	--	end
	end
--	warn("CHUNK GENERATED AT - "..chunkX..", 0, "..chunkZ)
	return CHUNK
end

local function GET_CHUNK_STANDING(PLR_POS)
	local POS_X, POS_Z = math.floor(PLR_POS.X / 3 / CHUNK_SCALE), math.floor(PLR_POS.Z / 3 / CHUNK_SCALE)
--	warn("POSSIBLE CHUNK POS - "..POS_X..", "..POS_Z)
	local REAL_CHUNK = GET_CHUNK(POS_X, POS_Z)
	if REAL_CHUNK ~= nil then
	--	warn("FOUND CHUNK!")
		Plr.CurrentChunk.Value = REAL_CHUNK
	end
end

function checkSurroundings(location)
	local chunkX, chunkZ = math.floor(location.X / 3 / CHUNK_SCALE), math.floor(location.Z / 3 / CHUNK_SCALE)
	local range = math.max(1, RENDER_DISTANCE)
	local CHUNKS_LOADED = {}
	for x = -range, range do
		for z = -range, range do
			BLOCKS_GENERATED += 1
			local cx = math.floor(chunkX + x)
			local cz = math.floor(chunkZ + z)
			local EXISTING_CHUNK = chunkExists(cx, cz)
			local REAL_CHUNK
			if not EXISTING_CHUNK then
				REAL_CHUNK = makeChunk(cx, cz)
			else
				EXISTING_CHUNK = EXISTING_CHUNK["Chunk"]
				EXISTING_CHUNK.Parent = workspace.Chunks
				REAL_CHUNK = EXISTING_CHUNK
			end
			CHUNKS_LOADED[REAL_CHUNK] = REAL_CHUNK
			if BLOCKS_GENERATED > BLOCK_GEN_WAIT then
				wait()
				BLOCKS_GENERATED = 0
			end
		end
	end
	for I, CHUNK_FOUND in pairs(workspace.Chunks:GetChildren()) do
		if not CHUNKS_LOADED[CHUNK_FOUND] then
			CHUNK_FOUND.Parent = RepStorage.UnloadedChunks
		end
	end
end

local CurChunk = Instance.new("ObjectValue", Plr)
CurChunk.Name = "CurrentChunk"

repeat wait() until Plr.Character ~= nil
Plr.Character:WaitForChild("Humanoid")
Plr.Character.Humanoid.WalkSpeed = 72
checkSurroundings(Plr.Character.HumanoidRootPart.Position)
RunService.Heartbeat:Connect(function()
	if Plr.Character ~= nil then
		GET_CHUNK_STANDING(Plr.Character.HumanoidRootPart.Position)
	end
end)
Plr.CurrentChunk.Changed:Connect(function()
	if Plr.Character ~= nil then
		checkSurroundings(Plr.Character.HumanoidRootPart.Position)
	end
end)

NOTE: This code goes in a localscript in "StarterPlayerScripts"
Any help on this will be appreciated!
Thank you, Zonix.

EDIT: The biomes are mountains and plains for testing!
Feel free to add more for your own testing to fix my issue!

If you have found this in research on how you can make a system like this, feel free to use it BUT this is just bare bones of a Minecraft like world gen script
EDIT 2: Improved code for easier use

2 Likes

If you want to write a very simple biome generator, here are a few steps (I used them for my survival island generator):

  1. Create a set of biomes with different generation parameters (which it seems you have started)
  2. Define a criteria for selecting biomes (it looks like you use temperature)
  3. Define an easing function for transitioning between the biomes
  4. In your generator, after you determine the biome type and generation parameters, apply your easing function if the location is near the boundary (you can use the noise selection values for determining this)

When you say you have the basics with biome generation but it seems to be a mess, I’m not entirely too sure what you mean. I ran the code and it seems mostly fine to me except for some areas of discontinuity (which can be handled by step 3 and 4), so I am not sure if I missed something or not.

2 Likes

Hello, I have read your methods.
I see 1 and 2 I’ve already done done as you say, but I am wondering how would go about make easing for transitioning of biomes and how I would apply it.

Thank you, Zonix!

There are several different methods for creating easing functions. You can compute the distances to your biome’s different selector boundaries and take the L1 norm (sum of absolute values), the L2 norm (magnitude), the minimum, or something else. Then if that value is within a certain margin, you can sample some number of nearby cells and then blend between their different properties with linear interpolation, random selection, stacking, etc. Here’s a very simple and rough idea:

local temperature = computeTemperature(cell.position)
local moisture = computeMoisture(cell.position)
local biome = computeBiome(temperature, moisture, ...)

local t_d = math.min(temperature - biome.minTemperature, biome.maxTemperature - temperature)
local m_d = math.min(moisture - biome.minMoisture, biome.maxMoisture - moisture)
local d = computeDistance(t_d, m_d) -- can be the minimum, sum of absolute values, magnitude, or something else

local cellProperties = computeCellProperties(cell, biome)

if d < THRESHOLD then
	local nearbyCells = sampleCells(cell.position)
	
	cellProperties = blendCells(nearbyCells)
	
	-- blend them with interpolation, stacking, randomization, etc. to obtain your final cell properties
end

makeCell(cell, cellProperties)

The island generator I wrote simply interpolates between the current cell and another nearby cell in the different biome using a polynomial when the cell is near the margin. If there is no different biome, then I do not apply any transitioning.

1 Like

I will attempt to implement this system into my world generation system!

Thank you, Zonix!