How to write 3D cave generation?

Intorduction

If you would like to skip to the problem, click here.

So, I’ve been bashing my head at a wall for the past 5 hours attempting to program cave generation into my game.

This game is a voxel-based mining game, but don’t think minecraft, I am not looking for minecraft.

My system only generates blocks specifically when required, thus I’d want caves to only generate when required.

Games like TCC have achieved this system, where a cave generates when a player digs into it.

My game is inspired by games like TCC or Azure Mines, however I am programming all my own systems to use in this game as I’d like my game to differ from the kit significantly.


How my game’s generation system works

My game uses a “hashing” system to store the locations of blocks.

function toKey(pos: Vector3): string
	local x = math.floor(pos.X / 6)
	local y = math.floor(pos.Y / 6)
	local z = math.floor(pos.Z / 6)
	return string.format("%d,%d,%d", x, y, z)
end
function placeInTable(position)
	local key = toKey(position)
	generatedBlocks[key] = true
end

This system simply prevents blocks generating over others, and I was attempting to use it to generate air “blocks” within caves.

The placement system for blocks is rather peculiar I’d say, using this function to place a block.

function placeBlock(dir, layer, realBlockPos, surface)
	if dir.Y > 0 and surface then return end
	local placeLocation = realBlockPos + (dir * 6)
	
	local prms = RaycastParams.new()
	prms.FilterDescendantsInstances = {workspace.Map.Terrain}
	prms.FilterType = Enum.RaycastFilterType.Include
	local directionRay = workspace:Raycast(placeLocation, dir * 12, prms)
	local upRay = workspace:Raycast(placeLocation, Vector3.new(0, 12, 0), prms)
	local depthFromSurface = workspace:Raycast(placeLocation, Vector3.new(0, 100, 0), prms)
	local yLevel = (depthFromSurface and math.floor(depthFromSurface.Distance/6)) or layer
	
	local newBlock = partCreator:CreateBlock(placeLocation, workspace.Map.Mine, yLevel, nil) --// get biome later
	if directionRay or upRay or placeLocation.Y > maxY then
		partCreator:SetLayer(newBlock, -999)
	else
		local randomness = math.random(-2,4)
		if getLayerForYLevel(layer + randomness) ~= getLayerForYLevel(yLevel) then
			yLevel = layer + randomness
		end
		partCreator:SetLayer(newBlock, getLayerForYLevel(yLevel))
		loadOre(newBlock, getLayerForYLevel(yLevel))
	end
	placeInTable(newBlock.Position)
end

dir is a Vector3 scaling from 0-1, which is simply the direction (LookVector) a block will be placed in.

layer is an int, representing the vertical position of the block. The layer variable starts at 0 on ground level, and getting higher the deeper you go. My method to calculate this in my previous cave generation function was to simply add 1 to the layer when the check position moved down, and remove 1 when it went up.

realBlockPos is the real-world position of the initial block the new block is generating off.
Simply, block.Position

surface is a boolean, determining whether a block is on the surface. If true, it won’t place a block to avoid the cave clipping through the actual map. This is determined if layer == 0.


The problem

Both of those functions work completely fine and as intended. My struggles start with actual cave generation.

Do I have any existing cave code?
No, my code was so far from working that I deleted it all. It wouldn’t generate large caves, wouldn’t generate air blocks and it wouldn’t place blocks correctly.

What I believe I have to do is step through each block, checking if it should be air or a block with noise, stopping stepping once a block has been placed in the direction it was headed.

My previous implementation of this had several issues, from the noise system being terrible to blocks simply not placing correctly, and air not getting added to the block position list.

I was previously using math.noise to determine where a cave is and isn’t, which I barely understand.

Any other “solutions” I’ve found on the forums don’t meet my requirements, either replicating Minecraft, which wouldn’t work with my system, or just saying “it’ll be complicated!” and nothing else.


How my previous cave system worked

Starting at the block that just got mined, I’d repeatedly step down 1 block until I hit the floor. Each step, I’d step additionally left and right, each of those steps I did the same but including stepping up and down to fully generate the cave.
Each step, I’d check if the position would be in a cave based off noise with the position as the input.
If true, it’d hash the position and insert it into generatedBlocks so additional blocks couldn’t be loaded into the air space, else, it’d call placeBlock.

However, my approach, for a reason I am unable to explain, simply wouldn’t place any air blocks.
If this was due to my noise math being bad or some insanely peculiar issue, I’m unsure, but I’m leaning it to being a problem with my noise math.


So in the end, I’m left with a cave-less mine right now. Bashing my head at a wall for 5 hours and about 45 minutes typing this for absolutely zero caves.

If anyone could help me, guide me or just supply code in any way I’d be forever greatful.

Preferably what I’d like

  • Proper guidance on how to use noise to generate caves

  • Any optimization tips, primarily how to better write the “fill” system to not have a million loops happening at once if possible.

1 Like