2D random cave generator

I wanted to make a random generating cave system for my 2d exploration game. It’s supposed to use seeds to make sure it’s different every time you play and use a chunk system for easier rendering. Im looking for spaghetti-like caves kind of like those in terraria. As of right now though, it’s only generating a square the size of one chunk instead of multiple chunks and chunks having a cave going through them. The code ive currently got is: (ive added comments for easier understanding)
extends Node2D

Map chunk size

var chunk_size: int = 50 # Size of each chunk (in tiles)

World size (in chunks)

var world_size: int = 3 # Number of chunks to load around the player

Player position

var player_pos: Vector2 = Vector2(0, 0) # Player starts at the origin
var current_chunk_pos: Vector2 = Vector2(0, 0)

Cave map (2D array) for the world

var cave_chunks: Dictionary = {}

Noise parameters

var noise: FastNoiseLite
var noise_scale: float = 0.1 # Scale for the noise
var threshold: float = 0.5 # Threshold to determine wall vs empty space

To track already loaded chunks and avoid redundant loading

var loaded_chunk_keys: Dictionary = {}

Called when the node enters the scene tree for the first time.

func _ready():

Initialize the noise generator with a random seed

noise = FastNoiseLite.new()
noise.seed = randi() # Random seed for varied caves
noise.noise_type = FastNoiseLite.TYPE_PERLIN # Use Perlin noise type

# Initialize the first chunk at (0, 0)
current_chunk_pos = Vector2(0, 0)
load_chunk(current_chunk_pos)

# Example: Generate cave in the player's current position
update_world_around_player()

Called every frame

func _process(delta):

Here we simulate player movement by changing the player position.

In a real game, you would update player_pos based on actual player movement.

if Input.is_action_pressed("ui_right"):
	player_pos.x += 1
elif Input.is_action_pressed("ui_left"):
	player_pos.x -= 1
elif Input.is_action_pressed("ui_down"):
	player_pos.y += 1
elif Input.is_action_pressed("ui_up"):
	player_pos.y -= 1

# Update the chunks around the player
update_world_around_player()

Function to update the world around the player

func update_world_around_player():
var chunk_offset_x = int(player_pos.x / chunk_size)
var chunk_offset_y = int(player_pos.y / chunk_size)

# Check if the chunk has changed (i.e., player has moved to a new chunk)
if chunk_offset_x != current_chunk_pos.x or chunk_offset_y != current_chunk_pos.y:
	current_chunk_pos = Vector2(chunk_offset_x, chunk_offset_y)
	
	# Load the new chunk around the player
	load_chunk(current_chunk_pos)
	
	# Unload distant chunks
	unload_distant_chunks()

Function to load a chunk and its neighbors

func load_chunk(chunk_pos: Vector2):
var chunk_key = get_chunk_key(chunk_pos) # Generate the chunk key as a string

# Prevent loading the same chunk again by checking if it has been loaded already
if loaded_chunk_keys.has(chunk_key):
	return  # Prevent reloading the chunk if it's already loaded

# Mark this chunk as loaded
loaded_chunk_keys[chunk_key] = true

# If chunk already exists, no need to regenerate
if not cave_chunks.has(chunk_key):
	var chunk = generate_cave_chunk(chunk_pos)
	cave_chunks[chunk_key] = chunk

# Load surrounding chunks iteratively (avoid recursion)
load_surrounding_chunks(chunk_pos)

Function to generate a cave chunk at a specific position using Perlin noise (FastNoiseLite)

func generate_cave_chunk(chunk_pos: Vector2) → Array:
var cave_map =

# Generate a new cave map for the chunk using FastNoiseLite (Perlin noise)
for y in range(chunk_size):
	var row = []
	for x in range(chunk_size):
		# Get the FastNoiseLite value at the current coordinates
		var nx = (chunk_pos.x * chunk_size + x) * noise_scale
		var ny = (chunk_pos.y * chunk_size + y) * noise_scale
		var value = noise.get_noise_2d(nx, ny)
		
		# Apply threshold to decide if it's a wall or empty space
		if value > threshold:
			row.append(1)  # Wall
		else:
			row.append(0)  # Empty space
	cave_map.append(row)

return cave_map

Function to get a unique key for a chunk based on its position

func get_chunk_key(chunk_pos: Vector2) → String:
return str(chunk_pos.x) + “,” + str(chunk_pos.y)

Function to load surrounding chunks around the player iteratively

func load_surrounding_chunks(chunk_pos: Vector2):
for dx in range(-world_size, world_size + 1):
for dy in range(-world_size, world_size + 1):
var neighbor_pos = chunk_pos + Vector2(dx, dy)
var neighbor_key = get_chunk_key(neighbor_pos)

		# Prevent reloading if the chunk is already loaded
		if not loaded_chunk_keys.has(neighbor_key):
			load_chunk(neighbor_pos)  # This will load the chunk if it's not already loaded

Function to unload distant chunks

func unload_distant_chunks():
var to_remove =
for key in cave_chunks.keys():
var chunk_pos = parse_chunk_key(key)
if abs(chunk_pos.x - current_chunk_pos.x) > world_size or abs(chunk_pos.y - current_chunk_pos.y) > world_size:
to_remove.append(key)

# Remove distant chunks
for key in to_remove:
	cave_chunks.erase(key)
	loaded_chunk_keys.erase(key)

Function to parse a chunk key into a Vector2 position

func parse_chunk_key(chunk_key: String) → Vector2:
var parts = chunk_key.split(“,”)
return Vector2(parts[0].to_int(), parts[1].to_int())

This is where the cave visualization happens

The draw_world() function needs to be in _draw() to be called properly

func _draw():
for chunk_key in cave_chunks.keys():
var chunk_pos = parse_chunk_key(chunk_key)
var cave_map = cave_chunks[chunk_key]

	for y in range(chunk_size):
		for x in range(chunk_size):
			var tile_type = cave_map[y][x]
			
			# Set the color for the tile
			var color = Color(1, 1, 1) if tile_type == 0 else Color(0, 0, 0)  # Empty space: white, wall: black
			
			# Draw a rectangle for each tile in the chunk
			var position = chunk_pos * chunk_size + Vector2(x, y)
			draw_rect(Rect2(position * 10, Vector2(10, 10)), color)  # Scale up by 10 to make tiles visible

Your code in the current form is not readable.
Please use preformatted text for code snippets - put it between triple backticks ``` at the beginning and end of the code snippet to properly format it.

1 Like

What @wchc said. Also you can press Ctrl + E to do this. Though with as much code as you have, I also recommend this:

```gdscript
# Add your comments like this
func do_stuff() → void:
print(“I’m doing stuff.”)
```

Then it’ll look like this:

# Add your comments like this
func do_stuff() -> void:
	print("I'm doing stuff.")

You’ll note that the tab doesn’t show up in the first one, but does in the second. I also recommend adding the comments as GDScript comments with # in front of them and inline with your code as opposed to headers.

I think the comments were with #, it’s just that markdown makes it turn into a header

2 Likes