
Godot Procedural Generation
- 304 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-procedural-generation for development tasks
About
godot-procedural-generation: A skill for development. This provides functionality for development workflows.
- godot-procedural-generation
Godot Procedural Generation by the numbers
- 304 all-time installs (skills.sh)
- +28 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,339 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-procedural-generationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 304 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-procedural-generation for development tasks
Files
Procedural Generation
Seeded algorithms, noise functions, and constraint propagation define replayable content generation.
Available Scripts
fast_noise_noise2d_master.gd
Advanced usage of FastNoiseLite with image-based sampling for maximum performance.
cellular_automata_dungeon.gd
The classic 4-5 rule implementation for organic cave and terrain generation.
poisson_disk_sampling_2d.gd
Blue-noise distribution algorithm for non-clumping object and enemy placement.
multi_threaded_chunk_gen.gd
Expert pattern for offloading procedural generation to the WorkerThreadPool.
drunknard_walk_path.gd
Lightweight algorithm for generating winding paths, tunnels, and rivers.
marching_squares_metaballs.gd
Implementing the Marching Squares algorithm for smooth contouring and influential maps.
bsp_tree_rooms.gd
Binary Space Partitioning for generating structured, non-overlapping floor plans.
wave_function_collapse_lite.gd
Foundation for Wave Function Collapse (WFC) using entropy-based adjacency rules.
mesh_gen_infinite_terrain.gd
Runtime 3D terrain generation using ArrayMesh and SurfaceTool with LOD potential.
l_system_tree_gen.gd
L-System string grammar for procedural plant and tree growth in 3D.
wfc_level_generator.gd
Expert Wave Function Collapse implementation with tile adjacency rules.
proc_gen_marching_cubes_base.gd
Base class for 3D terrain generation using ArrayMesh and direct GPU vertex array committing.
proc_gen_graph_layout.gd
Pattern for managing logical dungeon layouts using AStar2D/3D as a directed graph.
proc_gen_seed_history.gd
Seed and state history manager for deterministic, undoable procedural sequences.
NEVER Do in Procedural Generation
- NEVER generate chunks on the Main Thread — Proc-gen is CPU intensive and causes frame-rate spikes. Use
WorkerThreadPoolor a backgroundThreadto keep the UI responsive. - NEVER query `FastNoiseLite` every frame — Sampling noise per frame (especially in
_process) is a massive waste. Generate your map into anImageorArrayonce and sample from memory [NoiseSampling]. - NEVER use `randi()` for reproducible seeds — Always store and reuse a specific
seedwithin your random number generator (RandomNumberGenerator.new()) to ensure consistent world generation. - NEVER use pure randomness for object placement — Pure random (white noise) causes clumping and overlapping. Use Poisson Disk Sampling or Jittered Grids for natural-looking distributions.
- NEVER forget to bound your loops — Procedural loops (like WFC or Cellular Automata) can easily enter infinite states if constraints are impossible. Always include a
max_iterationssafety break. - NEVER instantiate nodes directly from proc-gen threads — You cannot touch the SceneTree from a worker thread. Generate the data in the thread, then notify the Main Thread to handle
add_child(). - NEVER use complex WFC for simple layouts — Wave Function Collapse is powerful but overkill for simple paths. Use Drunkard's Walk or BSP for lightweight structured layouts.
- NEVER rely on `TileMap.set_cell()` for large-scale updates — Updating 10,000 cells individually is slow. Prepare a
TileMapPatternand useset_pattern()orset_cells_terrain_connect()for batch updates. - NEVER forget to bake Navigation at the end — Procedurally generated worlds need their navmeshes rebaked at runtime or the AI will walk into walls.
- NEVER ignore data serialization — If you generate a world, you must be able to save the seed and any player modifications. Don't try to save the entire raw chunk state if avoidable.
---
func generate_dungeon(width: int, height: int, fill_percent: float = 0.4) -> Array:
var grid := []
for y in height:
var row := []
for x in width:
row.append(1) # 1 = wall
grid.append(row)
# Start in center
var x := width / 2
var y := height / 2
var floor_tiles := 0
var target_floor := int(width * height * fill_percent)
while floor_tiles < target_floor:
if grid[y][x] == 1:
grid[y][x] = 0 # Create floor
floor_tiles += 1
# Random walk
var dir := randi() % 4
match dir:
0: x = clampi(x + 1, 0, width - 1)
1: x = clampi(x - 1, 0, width - 1)
2: y = clampi(y + 1, 0, height - 1)
3: y = clampi(y - 1, 0, height - 1)
return gridPerlin Noise Terrain
var noise := FastNoiseLite.new()
func generate_terrain(width: int, height: int) -> Array:
noise.seed = randi()
noise.frequency = 0.05
var terrain := []
for y in height:
var row := []
for x in width:
var value := noise.get_noise_2d(x, y)
# Map noise to tile types
var tile: int
if value < -0.2:
tile = 0 # Water
elif value < 0.2:
tile = 1 # Grass
else:
tile = 2 # Mountain
row.append(tile)
terrain.append(row)
return terrainBSP Rooms
class_name BSPRoom
var x: int
var y: int
var width: int
var height: int
var left: BSPRoom = null
var right: BSPRoom = null
func split(min_size: int = 6) -> bool:
if left or right:
return false # Already split
# Choose split direction
var split_horizontal := randf() > 0.5
if width > height and float(width) / float(height) >= 1.25:
split_horizontal = false
elif height > width and float(height) / float(width) >= 1.25:
split_horizontal = true
var max := (height if split_horizontal else width) - min_size
if max <= min_size:
return false # Too small
var split_pos := randi_range(min_size, max)
if split_horizontal:
left = BSPRoom.new()
left.x = x
left.y = y
left.width = width
left.height = split_pos
right = BSPRoom.new()
right.x = x
right.y = y + split_pos
right.width = width
right.height = height - split_pos
else:
left = BSPRoom.new()
left.x = x
left.y = y
left.width = split_pos
left.height = height
right = BSPRoom.new()
right.x = x + split_pos
right.y = y
right.width = width - split_pos
right.height = height
return true
func generate_bsp_dungeon(width: int, height: int, iterations: int = 4) -> Array[BSPRoom]:
var root := BSPRoom.new()
root.x = 0
root.y = 0
root.width = width
root.height = height
var rooms: Array[BSPRoom] = [root]
for i in iterations:
var new_rooms: Array[BSPRoom] = []
for room in rooms:
if room.split():
new_rooms.append(room.left)
new_rooms.append(room.right)
else:
new_rooms.append(room)
rooms = new_rooms
return roomsRandom Loot
func generate_loot(loot_level: int) -> Array[Item]:
var items: Array[Item] = []
var roll_count := randi_range(1, 3)
for i in roll_count:
var rarity := roll_rarity()
var item := get_random_item(rarity, loot_level)
items.append(item)
return items
func roll_rarity() -> String:
var roll := randf()
if roll < 0.6:
return "common"
elif roll < 0.85:
return "uncommon"
elif roll < 0.95:
return "rare"
else:
return "legendary"Wave Function Collapse
# Simplified WFC for tile patterns
# Load compatible tile adjacency rules
var tile_rules := {
"grass": ["grass", "path", "water_edge"],
"water": ["water", "water_edge"],
"path": ["grass", "path"]
}
func wfc_generate(width: int, height: int) -> Array:
var grid := []
for y in height:
var row := []
for x in width:
row.append(null) # Uncollapsed
grid.append(row)
# Collapse cells until complete
while has_uncollapsed(grid):
var pos := find_lowest_entropy(grid)
collapse_cell(grid, pos)
propagate_constraints(grid, pos)
return gridBest Practices
1. Seeding - Use seeds for reproducibility 2. Validation - Ensure playable levels 3. Performance - Generate async if needed
Expert Procedural Patterns
1. 3D Terrain via ArrayMesh (Marching Cubes)
For voxel-like or smooth organic terrain, use ArrayMesh to generate geometry from code.
- Logic: Calculate vertices, normals, and indices in a worker thread.
- Commit: Use
add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)to create the mesh. - Performance: Use
create_trimesh_collision()only for the current chunk to keep physics updates fast.
2. Graph-Based Dungeon Logic
Don't generate your dungeon geometry first. Build a logical graph using AStar2D.
- Vertices: Represent "Rooms".
- Edges: Represent "Hallways" or "Doors".
- Benefit: You can easily run validation (is every room reachable?) before spawning a single mesh.
Reference
- Related:
godot-tilemap-mastery,godot-resource-data-patterns
Related
- Master Skill: godot-master
# bsp_tree_rooms.gd
# Binary Space Partitioning for structured floor plans
extends Node
class RoomNode:
var x: int; var y: int; var w: int; var h: int
var left: RoomNode; var right: RoomNode
func split():
# Logic to split vertically or horizontally
# until min_room_size is reached.
pass
# cellular_automata_dungeon.gd
# Smooth cave generation using Cellular Automata (4/5 rule)
extends Node
@export var width := 60
@export var height := 40
@export var fill_percent := 45
var map: Array = []
func generate_caves():
_random_fill()
for i in range(5):
_smooth_map()
return map
func _smooth_map():
var new_map = map.duplicate(true)
for x in range(1, width - 1):
for y in range(1, height - 1):
var neighbors = _get_neighbor_count(x, y)
if neighbors > 4:
new_map[x][y] = 1 # Wall
elif neighbors < 4:
new_map[x][y] = 0 # Floor
map = new_map
func _get_neighbor_count(grid_x, grid_y):
var count = 0
for x in range(grid_x-1, grid_x+2):
for y in range(grid_y-1, grid_y+2):
if x != grid_x or y != grid_y:
count += map[x][y]
return count
func _random_fill():
map.clear()
for x in range(width):
map.append([])
for y in range(height):
map[x].append(1 if randi() % 100 < fill_percent else 0)
# drunknard_walk_path.gd
# Simple path generation for dungeons or rivers
extends Node
func generate_path(start: Vector2i, steps: int) -> Array[Vector2i]:
var current = start
var path: Array[Vector2i] = [start]
var directions = [Vector2i.UP, Vector2i.DOWN, Vector2i.LEFT, Vector2i.RIGHT]
for i in range(steps):
var dir = directions[randi() % 4]
current += dir
if not path.has(current):
path.append(current)
return path
# skills/procedural-generation/scripts/dungeon_generator.gd
extends Node2D
## Dungeon Generator Expert Pattern
## BSP-based room placement with FastNoiseLite for terrain variation.
class_name DungeonGenerator
@export var room_count := 10
@export var min_room_size := Vector2i(5, 5)
@export var max_room_size := Vector2i(12, 12)
@export var map_size := Vector2i(100, 100)
var noise := FastNoiseLite.new()
var rooms: Array[Rect2i] = []
func _ready() -> void:
noise.seed = randi()
noise.frequency = 0.05
func generate() -> Array[Rect2i]:
rooms.clear()
# BSP rectangle subdivision
var initial_rect := Rect2i(Vector2i.ZERO, map_size)
var partitions := [initial_rect]
# Split into smaller partitions
while partitions.size() < room_count:
var partition: Rect2i = partitions.pick_random()
partitions.erase(partition)
var split_rects := _split_rect(partition)
if split_rects.size() == 2:
partitions.append_array(split_rects)
else:
partitions.append(partition) # Couldn't split, keep it
# Create rooms inside partitions
for partition in partitions:
var room := _create_room_in_partition(partition)
if room.has_area():
rooms.append(room)
return rooms
func _split_rect(rect: Rect2i) -> Array[Rect2i]:
# Can't split if too small
if rect.size.x < min_room_size.x * 2 or rect.size.y < min_room_size.y * 2:
return []
var split_horizontal := randf() > 0.5
if split_horizontal:
var split_y := randi_range(rect.position.y + min_room_size.y, rect.end.y - min_room_size.y)
return [
Rect2i(rect.position, Vector2i(rect.size.x, split_y - rect.position.y)),
Rect2i(Vector2i(rect.position.x, split_y), Vector2i(rect.size.x, rect.end.y - split_y))
]
else:
var split_x := randi_range(rect.position.x + min_room_size.x, rect.end.x - min_room_size.x)
return [
Rect2i(rect.position, Vector2i(split_x - rect.position.x, rect.size.y)),
Rect2i(Vector2i(split_x, rect.position.y), Vector2i(rect.end.x - split_x, rect.size.y))
]
func _create_room_in_partition(partition: Rect2i) -> Rect2i:
var room_w := randi_range(min_room_size.x, min(max_room_size.x, partition.size.x - 2))
var room_h := randi_range(min_room_size.y, min(max_room_size.y, partition.size.y - 2))
var room_x := partition.position.x + randi_range(1, partition.size.x - room_w - 1)
var room_y := partition.position.y + randi_range(1, partition.size.y - room_h - 1)
return Rect2i(room_x, room_y, room_w, room_h)
func get_noise_value_at(pos: Vector2i) -> float:
return noise.get_noise_2d(float(pos.x), float(pos.y))
## EXPERT USAGE:
## var gen := DungeonGenerator.new()
## gen.room_count = 15
## add_child(gen)
## var rooms := gen.generate()
##
## # Use rooms to place tiles
## for room in rooms:
## for x in range(room.position.x, room.end.x):
## for y in range(room.position.y, room.end.y):
## tilemap.set_cell(0, Vector2i(x, y), 0, Vector2i.ZERO)
# fast_noise_noise2d_master.gd
# Advanced usage of FastNoiseLite for terrain and heightmaps
extends Node
# EXPERT NOTE: Noise generation is expensive. Generate noise maps
# into a typed Array or Image rather than querying 'get_noise_2d' per tile.
var noise := FastNoiseLite.new()
func _ready() -> void:
noise.seed = randi()
noise.frequency = 0.01
noise.noise_type = FastNoiseLite.TYPE_PERLIN
# Generate a 100x100 heightmap image for faster sampling
var img = noise.get_image(100, 100)
# Process image data...
# l_system_tree_gen.gd
# Procedural tree/plant growth using L-Systems
extends Node3D
# Turtle graphics approach to plant generation.
func draw_lsystem(axiom: String, rules: Dictionary, iterations: int):
var current = axiom
for i in range(iterations):
var next = ""
for char in current:
next += rules.get(char, char)
current = next
# Then iterate characters to draw lines/branches
return current
# marching_squares_metaballs.gd
# Smooth contour generation (Marching Squares algorithm)
extends Node
# EXPERT NOTE: Use Marching Squares for organic-looking terrains,
# liquid simulations, or influence maps.
func get_contour_index(tl: float, tr: float, br: float, bl: float, threshold: float) -> int:
var index = 0
if tl >= threshold: index |= 8
if tr >= threshold: index |= 4
if br >= threshold: index |= 2
if bl >= threshold: index |= 1
return index
# mesh_gen_infinite_terrain.gd
# Dynamic Mesh generation for 3D terrain [ArrayMesh]
extends MeshInstance3D
# EXPERT NOTE: For infinite 3D terrain, generating a custom ArrayMesh
# is better than tiling StaticBody3D planes.
func generate_plane(width: int, depth: int):
var am = ArrayMesh.new()
var st = SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
# Generate vertices with noise height
for z in range(depth):
for x in range(width):
var y = 0 # noise.get_noise_2d(x, z)
st.add_vertex(Vector3(x, y, z))
# Generate indices...
st.generate_normals()
am = st.commit()
mesh = am
# multi_threaded_chunk_gen.gd
# Offloading heavy proc-gen tasks to WorkerThreadPool
extends Node
# EXPERT NOTE: Procedural generation creates 'frame-time spikes'.
# Offloading to a thread prevents the game from freezing.
func request_chunk(chunk_pos: Vector2i):
WorkerThreadPool.add_task(_generate_chunk_task.bind(chunk_pos))
func _generate_chunk_task(pos: Vector2i):
# Heavy noise calculations here
var data = _calc_data(pos)
# Pass data back to main thread for node instantiation
call_deferred("_finalize_chunk", pos, data)
func _calc_data(_pos): return []
func _finalize_chunk(_pos, _data): pass
# poisson_disk_sampling_2d.gd
# Blue-noise distribution for non-overlapping object placement
extends Node
# EXPERT NOTE: Poisson Disk Sampling is superior to random placement
# for trees, rocks, and spawns because it guarantees a minimum
# distance between objects, preventing 'clumping'.
func generate_points(width: float, height: float, radius: float, k: int = 30) -> Array[Vector2]:
var points: Array[Vector2] = []
var spawn_points: Array[Vector2] = []
spawn_points.append(Vector2(width/2, height/2))
while spawn_points.size() > 0:
var spawn_index = randi() % spawn_points.size()
var spawn_centre = spawn_points[spawn_index]
var accepted = false
for i in range(k):
var angle = randf() * PI * 2
var dir = Vector2(cos(angle), sin(angle))
var candidate = spawn_centre + dir * randf_range(radius, 2*radius)
if _is_valid(candidate, width, height, radius, points):
points.append(candidate)
spawn_points.append(candidate)
accepted = true
break
if not accepted:
spawn_points.remove_at(spawn_index)
return points
func _is_valid(p, w, h, r, points):
if p.x < 0 or p.x > w or p.y < 0 or p.y > h: return false
for other in points:
if p.distance_to(other) < r: return false
return true
class_name ProcGenGraphLayout
extends Node
## Expert pattern for managing dungeon layouts using AStar data structures.
## Decouples logical connections (rooms/hallways) from physical geometry.
var layout_graph := AStar2D.new()
## Adds a room node to the layout.
func add_room(id: int, pos: Vector2) -> void:
layout_graph.add_point(id, pos)
## Connects two rooms with a hallway.
func connect_rooms(id_a: int, id_b: int, bidirectional: bool = true) -> void:
layout_graph.connect_points(id_a, id_b, bidirectional)
## Returns all rooms in the layout.
func get_all_rooms() -> PackedInt64Array:
return layout_graph.get_point_ids()
## Returns connections for a specific room (e.g. to determine where doors should be).
func get_room_connections(id: int) -> PackedInt64Array:
return layout_graph.get_point_connections(id)
## Returns the spatial distance between two connected rooms.
func get_hallway_length(id_a: int, id_b: int) -> float:
return layout_graph.get_point_position(id_a).distance_to(layout_graph.get_point_position(id_b))
class_name ProcGenMarchingCubesBase
extends MeshInstance3D
## Base class for 3D terrain generation using ArrayMesh.
## Provides the foundation for Marching Cubes or Voxel geometry.
func update_geometry(vertices: PackedVector3Array, normals: PackedVector3Array, indices: PackedInt32Array) -> void:
var surface_array = []
surface_array.resize(Mesh.ARRAY_MAX)
surface_array[Mesh.ARRAY_VERTEX] = vertices
surface_array[Mesh.ARRAY_NORMAL] = normals
surface_array[Mesh.ARRAY_INDEX] = indices
var arr_mesh = ArrayMesh.new()
# PRIMITIVE_TRIANGLES is the standard for 3D surfaces
arr_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, surface_array)
self.mesh = arr_mesh
# Optimization: Generate collision if needed
# create_trimesh_collision()
class_name ProcGenSeedHistory
extends Node
## Expert Seed & State History Manager.
## Ensures deterministic procedural generation and allows "undo/redo" of random sequences.
var rng := RandomNumberGenerator.new()
var state_history: Array[int] = []
func _ready() -> void:
rng.randomize()
## Seeds the generator and clears history.
func initialize_seed(new_seed: int) -> void:
rng.seed = new_seed
state_history.clear()
## Records the current RNG state before a generation step.
func push_state() -> void:
state_history.append(rng.state)
## Restores the RNG to a previous state.
func pop_state() -> void:
if state_history.is_empty(): return
rng.state = state_history.pop_back()
## Returns the current seed string (useful for sharing).
func get_seed_string() -> String:
return str(rng.seed)
# wave_function_collapse_lite.gd
# Procedural tile arrangement using WFC principles
extends Node
# EXPERT NOTE: WFC is excellent for city generation or
# complex level layouts where tiles must obey adjacency rules.
var entropy_map: Array = []
func iterate():
# 1. Find cell with lowest entropy
# 2. Collapse it to a random valid state
# 3. Propagate changes to neighbors
pass
func propagate(_x, _y): pass
func collapse(_x, _y): pass
func find_lowest_entropy(): return Vector2i.ZERO
# skills/procedural-generation/code/wfc_level_generator.gd
extends Node
## Wave Function Collapse (WFC) Expert Pattern
## Generates rule-based tile maps with zero constraint violations.
@export var grid_size: Vector2i = Vector2i(10, 10)
@export var tile_library: Array[Resource] # Contains TileData with adjacency rules
var _grid: Array = [] # 2D array of 'Cell' objects
class Cell:
var possibilities: Array = [] # List of TileData
var collapsed: bool = false
var selected_tile: Resource = null
func _ready() -> void:
# 1. WFC Initialization
_init_grid()
_collapse_next()
func _init_grid() -> void:
for x in grid_size.x:
_grid.append([])
for y in grid_size.y:
var cell = Cell.new()
cell.possibilities = tile_library.duplicate()
_grid[x].append(cell)
func _collapse_next() -> void:
# 2. Entropy Selection
# Expert logic: Select the cell with the FEWEST possibilities
# to collapse next (Min-Entropy Heuristic).
var cell = _find_lowest_entropy()
if not cell:
print("Level Generation Complete")
return
cell.selected_tile = cell.possibilities.pick_random()
cell.collapsed = true
cell.possibilities = [cell.selected_tile]
# 3. Constraint Propagation
# Update neighbors based on the newly selected tile's rules.
_propagate_constraints()
# Recursively collapse until finished
_collapse_next()
func _find_lowest_entropy() -> Cell:
# Basic min-entropy search...
return null
func _propagate_constraints() -> void:
# Placeholder for the AC-3 or similar arc-consistency algorithm
pass
## EXPERT NOTE:
## Use 'Poisson Disk Sampling': For object distribution (trees/rocks),
## use Poisson Disk instead of pure Random to ensure a minimum
## distance between items, preventing unrealistic 'clumping'.
## For 'procedural-generation', use 'WorkerThreadPool' to run
## generation in the background, allowing for 'Infinite World'
## loading without frame-stutters.
## NEVER use 'randi()' for map seeds; use a unique 'RandomNumberGenerator'
## instance per level to ensure the same seed ALWAYS produces the same map.