🌍 Protseduurne Genereerimine
Perlin noise, dungeon genereerimine, Wave Function Collapse, BSP puud — loo lõpmata unikaalseid maailmu!
🌊 Samm 1 — Perlin Noise
Perlin noise genereerib sujuvaid juhuslikke mustreid — ideaalne maastiku, pilvede ja muude loomulike kujundite jaoks.
// Lihtne 2D noise implementatsioon
class SimpleNoise {
private perm: number[];
constructor(seed: number = Math.random() * 65536) {
this.perm = this.buildPermutation(seed);
}
private buildPermutation(seed: number): number[] {
const p = Array.from({ length: 256 }, (_, i) => i);
// Shuffle seemnega
let s = seed;
for (let i = 255; i > 0; i--) {
s = (s * 16807 + 0) % 2147483647;
const j = s % (i + 1);
[p[i], p[j]] = [p[j], p[i]];
}
return [...p, ...p]; // Dubleeri
}
private fade(t: number): number {
return t * t * t * (t * (t * 6 - 15) + 10);
}
private grad(hash: number, x: number, y: number): number {
const h = hash & 3;
const u = h < 2 ? x : y;
const v = h < 2 ? y : x;
return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
}
noise2D(x: number, y: number): number {
const X = Math.floor(x) & 255;
const Y = Math.floor(y) & 255;
const xf = x - Math.floor(x);
const yf = y - Math.floor(y);
const u = this.fade(xf);
const v = this.fade(yf);
const p = this.perm;
const a = p[X] + Y, b = p[X + 1] + Y;
return this.lerp(
this.lerp(this.grad(p[a], xf, yf), this.grad(p[b], xf - 1, yf), u),
this.lerp(this.grad(p[a + 1], xf, yf - 1), this.grad(p[b + 1], xf - 1, yf - 1), u),
v
);
}
private lerp(a: number, b: number, t: number): number {
return a + t * (b - a);
}
// Fractal Brownian Motion — mitu kihti noise'i
fbm(x: number, y: number, octaves: number = 6): number {
let value = 0, amplitude = 1, frequency = 1, maxValue = 0;
for (let i = 0; i < octaves; i++) {
value += this.noise2D(x * frequency, y * frequency) * amplitude;
maxValue += amplitude;
amplitude *= 0.5; // Persistence
frequency *= 2; // Lacunarity
}
return value / maxValue;
}
}
// Maastikaart genereerimiseks
function generateHeightmap(width: number, height: number, scale: number = 0.02): number[][] {
const noise = new SimpleNoise(42);
const map: number[][] = [];
for (let y = 0; y < height; y++) {
map[y] = [];
for (let x = 0; x < width; x++) {
// FBM annab detailsema tulemuse
const value = noise.fbm(x * scale, y * scale, 6);
map[y][x] = (value + 1) / 2; // Normaliseeri 0-1
}
}
return map;
}
🏰 Samm 2 — Dungeon Genereerimine (BSP)
interface Room {
x: number; y: number; width: number; height: number;
}
class BSPNode {
left: BSPNode | null = null;
right: BSPNode | null = null;
room: Room | null = null;
constructor(
public x: number, public y: number,
public width: number, public height: number
) {}
}
function generateDungeon(
width: number, height: number, minRoomSize: number = 6
): { grid: number[][]; rooms: Room[] } {
const grid = Array.from({ length: height }, () => Array(width).fill(1)); // 1 = sein
const rooms: Room[] = [];
function split(node: BSPNode, depth: number): void {
if (depth <= 0 || node.width < minRoomSize * 2 || node.height < minRoomSize * 2) {
// Loo tuba sellesse lehtsõlme
const roomW = minRoomSize + Math.floor(Math.random() * (node.width - minRoomSize));
const roomH = minRoomSize + Math.floor(Math.random() * (node.height - minRoomSize));
const roomX = node.x + Math.floor(Math.random() * (node.width - roomW));
const roomY = node.y + Math.floor(Math.random() * (node.height - roomH));
node.room = { x: roomX, y: roomY, width: roomW, height: roomH };
rooms.push(node.room);
// Joonista tuba grid'ile
for (let ry = roomY; ry < roomY + roomH; ry++) {
for (let rx = roomX; rx < roomX + roomW; rx++) {
if (ry >= 0 && ry < height && rx >= 0 && rx < width) {
grid[ry][rx] = 0; // 0 = põrand
}
}
}
return;
}
// Jaga horisontaalselt või vertikaalselt
const horizontal = Math.random() > 0.5;
if (horizontal && node.height > minRoomSize * 2) {
const splitY = node.y + minRoomSize + Math.floor(Math.random() * (node.height - minRoomSize * 2));
node.left = new BSPNode(node.x, node.y, node.width, splitY - node.y);
node.right = new BSPNode(node.x, splitY, node.width, node.y + node.height - splitY);
} else if (node.width > minRoomSize * 2) {
const splitX = node.x + minRoomSize + Math.floor(Math.random() * (node.width - minRoomSize * 2));
node.left = new BSPNode(node.x, node.y, splitX - node.x, node.height);
node.right = new BSPNode(splitX, node.y, node.x + node.width - splitX, node.height);
}
if (node.left) split(node.left, depth - 1);
if (node.right) split(node.right, depth - 1);
}
const root = new BSPNode(1, 1, width - 2, height - 2);
split(root, 5);
// Ühenda toad koridoridega
for (let i = 0; i < rooms.length - 1; i++) {
connectRooms(grid, rooms[i], rooms[i + 1]);
}
return { grid, rooms };
}
function connectRooms(grid: number[][], a: Room, b: Room): void {
let x = Math.floor(a.x + a.width / 2);
let y = Math.floor(a.y + a.height / 2);
const tx = Math.floor(b.x + b.width / 2);
const ty = Math.floor(b.y + b.height / 2);
while (x !== tx) {
if (y >= 0 && y < grid.length && x >= 0 && x < grid[0].length)
grid[y][x] = 0;
x += x < tx ? 1 : -1;
}
while (y !== ty) {
if (y >= 0 && y < grid.length && x >= 0 && x < grid[0].length)
grid[y][x] = 0;
y += y < ty ? 1 : -1;
}
}
🎲 Samm 3 — Cellular Automata (Koopamängud)
function generateCaves(width: number, height: number, fillPct = 0.45, iterations = 5): number[][] {
// 1. Juhuslik algseisund
let grid = Array.from({ length: height }, () =>
Array.from({ length: width }, () => Math.random() < fillPct ? 1 : 0)
);
// Äärised on alati seinad
for (let y = 0; y < height; y++) {
grid[y][0] = 1;
grid[y][width - 1] = 1;
}
for (let x = 0; x < width; x++) {
grid[0][x] = 1;
grid[height - 1][x] = 1;
}
// 2. Cellular automata iteratsioonid
for (let i = 0; i < iterations; i++) {
const newGrid = grid.map(row => [...row]);
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
const walls = countNeighborWalls(grid, x, y);
// B678/S345678 reegel (tavalisim koobaste jaoks)
newGrid[y][x] = walls >= 5 ? 1 : walls <= 2 ? 1 : 0;
}
}
grid = newGrid;
}
return grid;
}
function countNeighborWalls(grid: number[][], x: number, y: number): number {
let count = 0;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (dx === 0 && dy === 0) continue;
count += grid[y + dy]?.[x + dx] ?? 1;
}
}
return count;
}
📝 Harjutused
-
Perlin Noise maastik
Genereeri 2D heightmap Perlin noise'iga. Visualiseeri: vesi (sinine), liiv (kollane), rohi (roheline), mägi (hall), lumi (valge). Lisa FBM mitu kihti.
-
BSP Dungeon generaator
Implementeeri BSP dungeon. Visualiseeri toad ja koridorid. Lisa vaenlased tubadesse, üks tuba on algus, teine lõpp. Iga refresh annab uue dungeni.
-
Cellular automata koobad
Loo koobas cellular automata meetodiga. Lisa flood fill et kontrollida ühenduvust. Ühenda eraldatud koobad koridoridega. Visualiseeri iteratsioonide sammud.
-
Protseduurne linnagenereerimine
Genereeri lihtsustatud linnakaart: teed ruudustikuna, majad (erineva suuruse kastid), pargid (rohelised alad). Lisa juhuslikkust teedele ja hoonetele.
-
Seemnepõhine genereerimine
Lisa kõigile generaatoritele seemnefunktsioon (seed). Sama seeme = sama tulemus. Lisa UI kus kasutaja saab seemnearvu sisestada ja tulemust näha. Lisa "share seed" nupp.