| name | procedural-generation |
| description | Guide complet de génération procédurale — bruit de Perlin/Simplex, terrain heightmap, dungeon generation, L-systems, Wave Function Collapse (WFC), cellular automata, PCG pour textures/armes/quêtes, et optimisation. |
Procedural Generation — Guide Complet
Ce skill couvre la génération procédurale de contenu (PCG) pour jeux vidéo. À charger pour toute tâche impliquant des algorithmes de génération automatique de niveaux, terrains, textures, ou structures.
1. Bruit (Noise) — Fondations du PCG
Bruit de Perlin (Classique)
public static class Bruit
{
public static float Perlin2D(float x, float y, float scale = 1.0f)
{
return Mathf.PerlinNoise(x * scale, y * scale);
}
public static float OctaveNoise(float x, float y, int octaves = 4, float persistence = 0.5f)
{
float value = 0f;
float amplitude = 1f;
float frequency = 1f;
float maxValue = 0f;
for (int i = 0; i < octaves; i++)
{
value += Mathf.PerlinNoise(x * frequency, y * frequency) * amplitude;
maxValue += amplitude;
amplitude *= persistence;
frequency *= 2f;
}
return value / maxValue;
}
}
Bruit de Simplex (Godot)
# Godot — Simplex Noise avec FastNoiseLite
extends Node
var noise := FastNoiseLite.new()
func _ready() -> void:
noise.seed = randi()
noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
noise.fractal_type = FastNoiseLite.FRACTAL_FBM
noise.fractal_octaves = 4
noise.fractal_gain = 0.5
noise.fractal_lacunarity = 2.0
func get_height(x: float, z: float) -> float:
return noise.get_noise_2d(x, z) * 20.0 # scale
Bruit de Voronoi (Worley)
import numpy as np
from scipy.spatial import KDTree
def voronoi_noise(width: int, height: int, points_count: int = 50) -> np.ndarray:
points = np.random.rand(points_count, 2) * [width, height]
tree = KDTree(points)
grid = np.zeros((height, width))
for y in range(height):
for x in range(width):
dist, idx = tree.query([x, y])
grid[y, x] = dist / max(width, height)
return grid
2. Génération de Terrain (Heightmap)
Heightmap + Coloration par hauteur
# Godot — Génération de terrain procédural
extends MeshInstance3D
@export var taille := 100
@export var resolution := 128
@export var hauteur_max := 20.0
@export var seed := 42
func generer_terrain() -> void:
var noise := FastNoiseLite.new()
noise.seed = seed
noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
noise.fractal_octaves = 6
noise.fractal_gain = 0.5
var st = SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
var step := taille / float(resolution)
for z in range(resolution):
for x in range(resolution):
var wx := x * step - taille / 2.0
var wz := z * step - taille / 2.0
var h := noise.get_noise_2d(wx, wz) * hauteur_max
# Ajouter un lac au centre
var dist_centre := Vector2(wx, wz).length()
if dist_centre < 10.0:
h = -1.0 # fond de lac
# Vertex
st.add_vertex(Vector3(wx, h, wz))
# Triangles (quad mesh)
for z in range(resolution - 1):
for x in range(resolution - 1):
var i0 := z * resolution + x
var i1 := i0 + 1
var i2 := (z + 1) * resolution + x
var i3 := i2 + 1
st.add_index(i0); st.add_index(i1); st.add_index(i2)
st.add_index(i1); st.add_index(i3); st.add_index(i2)
st.generate_normals()
mesh = st.commit()
Coloration par hauteur et pente
# Dans le même script, après generate_normals
func colorer_terrain(st: SurfaceTool, heightmap: PackedFloat32Array) -> void:
for i in range(heightmap.size()):
var h = heightmap[i]
var color: Color
if h < -1.0: color = Color(0.2, 0.4, 0.8) # Eau
elif h < 2.0: color = Color(0.6, 0.8, 0.3) # Herbe
elif h < 5.0: color = Color(0.4, 0.6, 0.2) # Forêt
elif h < 10.0: color = Color(0.5, 0.4, 0.3) # Roche
else: color = Color(0.9, 0.9, 0.9) # Neige
st.set_color(color)
3. Génération de Donjons (BSP + Rooms)
BSP (Binary Space Partition) Dungeon
import random
from dataclasses import dataclass
@dataclass
class Room:
x: int; y: int; w: int; h: int
@dataclass
class BSPNode:
x: int; y: int; w: int; h: int
left: 'BSPNode' = None
right: 'BSPNode' = None
room: Room = None
def split_node(node: BSPNode, min_size: int = 8):
if node.w < min_size * 2 or node.h < min_size * 2:
return
split_h = random.choice([True, False])
if split_h and node.w >= min_size * 2:
split = random.randint(min_size, node.w - min_size)
node.left = BSPNode(node.x, node.y, split, node.h)
node.right = BSPNode(node.x + split, node.y, node.w - split, node.h)
elif node.h >= min_size * 2:
split = random.randint(min_size, node.h - min_size)
node.left = BSPNode(node.x, node.y, node.w, split)
node.right = BSPNode(node.x, node.y + split, node.w, node.h - split)
else:
return
split_node(node.left, min_size)
split_node(node.right, min_size)
def create_rooms(node: BSPNode, padding: = ):
node.left node.right:
node.left: create_rooms(node.left, padding)
node.right: create_rooms(node.right, padding)
:
margin =
w = random.randint(margin, node.w - margin * )
h = random.randint(margin, node.h - margin * )
x = node.x + random.randint(margin, node.w - w - margin)
y = node.y + random.randint(margin, node.h - h - margin)
node.room = Room(x, y, w, h)
() -> []:
corridors = []
node.left node.right:
l_rooms = get_rooms(node.left)
r_rooms = get_rooms(node.right)
l_rooms r_rooms:
r1 = random.choice(l_rooms)
r2 = random.choice(r_rooms)
cx = (r1.x + r1.w // + r2.x + r2.w // ) //
corridors.append((r1.x + r1.w // , r1.y + r1.h // , cx, r1.y + r1.h // ))
corridors.append((cx, r1.y + r1.h // , cx, r2.y + r2.h // ))
corridors.append((cx, r2.y + r2.h // , r2.x + r2.w // , r2.y + r2.h // ))
node.left: corridors.extend(connect_rooms(node.left))
node.right: corridors.extend(connect_rooms(node.right))
corridors
() -> [Room]:
node.room: [node.room]
rooms = []
node.left: rooms.extend(get_rooms(node.left))
node.right: rooms.extend(get_rooms(node.right))
rooms
4. Wave Function Collapse (WFC)
WFC — Algorithme de base
import random
from collections import Counter
class WFC:
"""Wave Function Collapse — génération de tuiles contrainte"""
def __init__(self, width: int, height: int, tiles: list[str], rules: dict):
self.width = width
self.height = height
self.tiles = tiles
self.rules = rules
self.grid = [[set(tiles) for _ in range(width)] for _ in range(height)]
def run(self) -> list[list[str]]:
while not self.is_collapsed():
x, y = self.find_lowest_entropy()
if x is None:
break
self.collapse(x, y)
self.propagate(x, y)
return self.extract_grid()
() -> [, ] | :
min_entropy = ()
result =
y (.height):
x (.width):
entropy = (.grid[y][x])
entropy > entropy < min_entropy:
neighbors = .count_neighbors(x, y)
score = entropy - * neighbors
score < min_entropy:
min_entropy = score
result = (x, y)
result
() -> :
options = (.grid[y][x])
weights = [.get_weight(x, y, t) t options]
total = (weights)
total > :
weights = [w / total w weights]
.grid[y][x] = {random.choices(options, weights=weights)[]}
:
.grid[y][x] = {random.choice(options)}
() -> :
stack = [(x, y)]
stack:
cx, cy = stack.pop()
current = .grid[cy][cx]
(current) == :
tile = (current)[]
dx, dy, direction [(, -, ), (, , ), (-, , ), (, , )]:
nx, ny = cx + dx, cy + dy
<= nx < .width <= ny < .height:
allowed = .rules.get(tile, {}).get(direction, .tiles)
before = (.grid[ny][nx])
.grid[ny][nx] &= (allowed)
(.grid[ny][nx]) < before:
stack.append((nx, ny))
() -> :
((cell) == row .grid cell row)
() -> :
count =
dx, dy [(, -), (, ), (-, ), (, )]:
nx, ny = x + dx, y + dy
<= nx < .width <= ny < .height:
(.grid[ny][nx]) == :
count +=
count
() -> :
weight =
dx, dy, direction [(, -, ), (, , ), (-, , ), (, , )]:
nx, ny = x + dx, y + dy
<= nx < .width <= ny < .height:
neighbors = .grid[ny][nx]
allowed = .rules.get(tile, {}).get(direction, [])
overlap = (neighbors & (allowed))
overlap == :
weight *=
weight
() -> [[]]:
[[(cell)[] cell row] row .grid]
5. Cellular Automata (Cavernes)
import numpy as np
def generate_cave(width: int, height: int, fill_prob: float = 0.45,
iterations: int = 5, wall_threshold: int = 5) -> np.ndarray:
"""Génère des cavernes style Spelunky via automates cellulaires"""
grid = np.random.rand(height, width) < fill_prob
for _ in range(iterations):
new_grid = grid.copy()
for y in range(height):
for x in range(width):
walls = 0
for dy in [-1, 0, 1]:
for dx in [-1, 0, 1]:
if dx == 0 and dy == 0:
continue
nx, ny = x + dx, y + dy
if 0 <= nx < width and 0 <= ny < height:
if grid[ny, nx]:
walls += 1
:
walls +=
grid[y, x]:
new_grid[y, x] = walls >= wall_threshold -
:
new_grid[y, x] = walls >= wall_threshold
grid = new_grid
grid
6. L-Systems (Plantes Procédurales)
import turtle
import random
class LSystem:
"""L-System pour génération de plantes et fractales"""
def __init__(self, axiom: str, rules: dict, angle: float = 25.0):
self.axiom = axiom
self.rules = rules
self.angle = angle
def generate(self, iterations: int) -> str:
result = self.axiom
for _ in range(iterations):
result = ''.join(self.rules.get(c, c) for c in result)
return result
def draw(self, instructions: str, segment_length: int = 10) -> None:
stack = []
turtle.speed(0)
turtle.left(90)
for cmd in instructions:
if cmd == 'F':
turtle.forward(segment_length)
elif cmd == 'f':
turtle.penup()
turtle.forward(segment_length)
turtle.pendown()
cmd == :
turtle.left(.angle + random.uniform(-, ))
cmd == :
turtle.right(.angle + random.uniform(-, ))
cmd == :
stack.append((turtle.position(), turtle.heading()))
cmd == :
pos, heading = stack.pop()
turtle.penup()
turtle.goto(pos)
turtle.setheading(heading)
turtle.pendown()
axiom =
rules = {
:
}
lsys = LSystem(axiom, rules, angle=)
instructions = lsys.generate()
7. PCG pour Textures (Material Generation)
import numpy as np
from PIL import Image
def generate_marble_texture(width: int, height: int, seed: int = 42) -> Image:
"""Génération procédurale de texture marbre"""
np.random.seed(seed)
x = np.linspace(0, 6, width)
y = np.linspace(0, 6, height)
X, Y = np.meshgrid(x, y)
noise = np.sin(X * 3 + Y * 2 + np.sin(X * 5 + Y * 3) * 2)
veins = np.sin(noise * 4 + X * 0.5)
veins = (veins - veins.min()) / (veins.max() - veins.min())
r = (200 + veins * 55).astype(np.uint8)
g = (190 + veins * 50).astype(np.uint8)
b = (170 + veins * 45).astype(np.uint8)
return Image.fromarray(np.stack([r, g, b], axis=2), 'RGB')
def generate_wood_texture(width: int, height: int) -> Image:
"""Génération procédurale de texture bois"""
x = np.arange(width)
y = np.arange(height)
X, Y = np.meshgrid(x, y)
rings = np.sqrt((X - width/)** + (Y - height/)**) /
rings += np.random.randn(height, width) *
rings = np.sin(rings * np.pi * )
rings = (rings - rings.()) / (rings.() - rings.())
r = ( + rings * ).astype(np.uint8)
g = ( + rings * ).astype(np.uint8)
b = ( + rings * ).astype(np.uint8)
Image.fromarray(np.stack([r, g, b], axis=), )
8. Génération de Quêtes et Contenu Narratif
import random
class QuestGenerator:
"""Génération procédurale de quêtes RPG"""
TEMPLATES = {
"kill": [
"{count} {monster} infestent {location}. Tue-les pour {reward}.",
"Un {monster} terrorise {location}. Élimine-le pour {reward}.",
],
"fetch": [
"Rapporte {item} de {location} pour {npc}.",
"{item} a été perdu dans {location}. Retrouve-le pour {reward}.",
],
"escort": [
"Escorte {npc} à travers {location} jusqu'à {destination}.",
],
"delivery": [
"Livraison urgente: apporte {item} à {npc} à {location}.",
]
}
def __init__(self):
self.monsters = ["gobelins", "loups", "squelettes", "bandits", "rats géants", "araignées"]
self.locations = ["la forêt sombre", "les ruines antiques", "la caverne oubliée",
"le marais empoisonné", "le donjon maudit", "les montagnes brumeuses"]
self.npcs = ["le vieux sage", "le forgeron", "la prêtresse", "le marchand", "le capitaine"]
self.items = ["un artefact ancien", "une potion rare", "un parchemin scellé",
, ]
() -> :
quest_type:
quest_type = random.choice((.TEMPLATES.keys()))
template = random.choice(.TEMPLATES[quest_type])
quest = {
: quest_type,
: random.choice(.monsters),
: random.choice(.locations),
: random.choice(.npcs),
: random.choice(.items),
: random.randint(, ),
: ,
: random.choice(.locations),
: ,
}
quest[] = template.(**quest)
quest[] = random.choice([, , ])
quest[] = quest[] * random.randint(, )
quest
9. Optimisation du PCG
Caching et Seed Control
class PCGManager:
def __init__(self, seed: int = None):
self.seed = seed or random.randint(0, 2**32 - 1)
self._rng = random.Random(self.seed)
def generate_terrain(self, region: str) -> np.ndarray:
region_seed = hash(f"{self.seed}:{region}") % 2**32
return self._generate_heightmap(region_seed)
def generate_dungeon(self, seed: int) -> dict:
rng = random.Random(seed)
LOD pour terrains procéduraux
- Low res : 32x32 heightmap pour la minimap
- Medium res : 128x128 pour le lointain
- High res : 512x512 pour le proche (streaming par chunks)
10. Pièges Courants
- Seed non reproductible :
random.seed(time.time()) → toujours exposer un seed fixe
- Terrain trop lisse : trop d'octaves de bruit → landscape plat
- Donjons sans connexion : BSP rooms non connectées → inaccessible
- WFC contradiction : règles de tuiles impossibles → reset et retry
- Performance PCG runtime : générer au chargement, pas en temps réel
- Contenu vide : PCG sans contraintes de design → boring maps
- Seed identique : hash collision → mêmes donjons à chaque partie