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