Files
esengine/docs/en/modules/procgen/index.md
YHH 4a16e30794 docs(modules): 添加框架模块文档 (#350)
* docs(modules): 添加框架模块文档

添加以下模块的完整文档:
- FSM (状态机): 状态定义、转换条件、优先级、事件监听
- Timer (定时器): 定时器调度、冷却系统、服务令牌
- Spatial (空间索引): GridSpatialIndex、AOI 兴趣区域管理
- Pathfinding (寻路): A* 算法、网格地图、导航网格、路径平滑
- Procgen (程序化生成): 噪声函数、种子随机数、加权随机

所有文档均基于实际源码 API 编写,包含:
- 快速开始示例
- 完整 API 参考
- 实际使用案例
- 蓝图节点说明
- 最佳实践建议

* docs(modules): 添加 Blueprint 模块文档和所有模块英文版

新增中文文档:
- Blueprint (蓝图可视化脚本): VM、自定义节点、组合系统、触发器

新增英文文档 (docs/en/modules/):
- FSM: State machine API, transitions, ECS integration
- Timer: Timers, cooldowns, service tokens
- Spatial: Grid spatial index, AOI management
- Pathfinding: A*, grid map, NavMesh, path smoothing
- Procgen: Noise functions, seeded random, weighted random
- Blueprint: Visual scripting, custom nodes, composition

所有文档均基于实际源码 API 编写。
2025-12-26 20:02:21 +08:00

9.1 KiB

Procedural Generation (Procgen)

@esengine/procgen provides core tools for procedural content generation, including noise functions, seeded random numbers, and various random utilities.

Installation

npm install @esengine/procgen

Quick Start

Noise Generation

import { createPerlinNoise, createFBM } from '@esengine/procgen';

// Create Perlin noise
const perlin = createPerlinNoise(12345); // seed

// Sample 2D noise
const value = perlin.noise2D(x * 0.1, y * 0.1);
console.log(value); // [-1, 1]

// Use FBM for more natural results
const fbm = createFBM(perlin, {
    octaves: 6,
    persistence: 0.5
});

const height = fbm.noise2D(x * 0.01, y * 0.01);

Seeded Random

import { createSeededRandom } from '@esengine/procgen';

// Create deterministic random generator
const rng = createSeededRandom(42);

// Same seed always produces same sequence
console.log(rng.next());          // 0.xxx
console.log(rng.nextInt(1, 100)); // 1-100
console.log(rng.nextBool(0.3));   // 30% true

Weighted Random

import { createWeightedRandom, createSeededRandom } from '@esengine/procgen';

const rng = createSeededRandom(42);

const loot = createWeightedRandom([
    { value: 'common',    weight: 60 },
    { value: 'uncommon',  weight: 25 },
    { value: 'rare',      weight: 10 },
    { value: 'legendary', weight: 5 }
]);

const drop = loot.pick(rng);
console.log(drop); // Likely 'common'

Noise Functions

Perlin Noise

Classic gradient noise, output range [-1, 1]:

import { createPerlinNoise } from '@esengine/procgen';

const perlin = createPerlinNoise(seed);
const value2D = perlin.noise2D(x, y);
const value3D = perlin.noise3D(x, y, z);

Simplex Noise

Faster than Perlin, less directional bias:

import { createSimplexNoise } from '@esengine/procgen';

const simplex = createSimplexNoise(seed);
const value = simplex.noise2D(x, y);

Worley Noise

Cell-based noise for stone, cell textures:

import { createWorleyNoise } from '@esengine/procgen';

const worley = createWorleyNoise(seed);
const distance = worley.noise2D(x, y);

FBM (Fractal Brownian Motion)

Layer multiple noise octaves for richer detail:

import { createPerlinNoise, createFBM } from '@esengine/procgen';

const baseNoise = createPerlinNoise(seed);

const fbm = createFBM(baseNoise, {
    octaves: 6,        // Layer count (more = richer detail)
    lacunarity: 2.0,   // Frequency multiplier
    persistence: 0.5,  // Amplitude decay
    frequency: 1.0,    // Initial frequency
    amplitude: 1.0     // Initial amplitude
});

// Standard FBM
const value = fbm.noise2D(x, y);

// Ridged FBM (for mountains)
const ridged = fbm.ridged2D(x, y);

// Turbulence
const turb = fbm.turbulence2D(x, y);

// Billowed (for clouds)
const cloud = fbm.billowed2D(x, y);

Seeded Random API

SeededRandom

Deterministic PRNG based on xorshift128+:

import { createSeededRandom } from '@esengine/procgen';

const rng = createSeededRandom(42);

Basic Methods

rng.next();              // [0, 1) float
rng.nextInt(1, 10);      // [min, max] integer
rng.nextFloat(0, 100);   // [min, max) float
rng.nextBool();          // 50%
rng.nextBool(0.3);       // 30%
rng.reset();             // Reset to initial state

Distribution Methods

// Normal distribution (Gaussian)
rng.nextGaussian();          // mean 0, stdDev 1
rng.nextGaussian(100, 15);   // mean 100, stdDev 15

// Exponential distribution
rng.nextExponential();       // λ = 1
rng.nextExponential(0.5);    // λ = 0.5

Geometry Methods

// Uniform point in circle
const point = rng.nextPointInCircle(50); // { x, y }

// Point on circle edge
const edge = rng.nextPointOnCircle(50);  // { x, y }

// Uniform point in sphere
const point3D = rng.nextPointInSphere(50); // { x, y, z }

// Random direction vector
const dir = rng.nextDirection2D(); // { x, y }, length 1

Weighted Random API

WeightedRandom

Precomputed cumulative weights for efficient selection:

import { createWeightedRandom } from '@esengine/procgen';

const selector = createWeightedRandom([
    { value: 'apple',  weight: 5 },
    { value: 'banana', weight: 3 },
    { value: 'cherry', weight: 2 }
]);

const result = selector.pick(rng);
const result2 = selector.pickRandom(); // Uses Math.random

console.log(selector.getProbability(0)); // 0.5 (5/10)
console.log(selector.size);              // 3
console.log(selector.totalWeight);       // 10

Convenience Functions

import { weightedPick, weightedPickFromMap } from '@esengine/procgen';

const item = weightedPick([
    { value: 'a', weight: 1 },
    { value: 'b', weight: 2 }
], rng);

const item2 = weightedPickFromMap({
    'common': 60,
    'rare': 30,
    'epic': 10
}, rng);

Shuffle and Sampling

shuffle / shuffleCopy

Fisher-Yates shuffle:

import { shuffle, shuffleCopy } from '@esengine/procgen';

const arr = [1, 2, 3, 4, 5];
shuffle(arr, rng);           // In-place
const shuffled = shuffleCopy(arr, rng); // Copy

pickOne

import { pickOne } from '@esengine/procgen';

const item = pickOne(['a', 'b', 'c', 'd'], rng);

sample / sampleWithReplacement

import { sample, sampleWithReplacement } from '@esengine/procgen';

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const unique = sample(arr, 3, rng);      // 3 unique
const withRep = sampleWithReplacement(arr, 5, rng); // 5 with replacement

randomIntegers

import { randomIntegers } from '@esengine/procgen';

// 5 unique random integers from 1-100
const nums = randomIntegers(1, 100, 5, rng);

weightedSample

import { weightedSample } from '@esengine/procgen';

const items = ['A', 'B', 'C', 'D', 'E'];
const weights = [10, 8, 6, 4, 2];
const selected = weightedSample(items, weights, 3, rng);

Practical Examples

Procedural Terrain

import { createPerlinNoise, createFBM } from '@esengine/procgen';

class TerrainGenerator {
    private fbm: FBM;
    private moistureFbm: FBM;

    constructor(seed: number) {
        const heightNoise = createPerlinNoise(seed);
        const moistureNoise = createPerlinNoise(seed + 1000);

        this.fbm = createFBM(heightNoise, {
            octaves: 8,
            persistence: 0.5,
            frequency: 0.01
        });

        this.moistureFbm = createFBM(moistureNoise, {
            octaves: 4,
            persistence: 0.6,
            frequency: 0.02
        });
    }

    getHeight(x: number, y: number): number {
        let height = this.fbm.noise2D(x, y);
        height += this.fbm.ridged2D(x * 0.5, y * 0.5) * 0.3;
        return (height + 1) * 0.5; // Normalize to [0, 1]
    }

    getBiome(x: number, y: number): string {
        const height = this.getHeight(x, y);
        const moisture = (this.moistureFbm.noise2D(x, y) + 1) * 0.5;

        if (height < 0.3) return 'water';
        if (height < 0.4) return 'beach';
        if (height > 0.8) return 'mountain';

        if (moisture < 0.3) return 'desert';
        if (moisture > 0.7) return 'forest';
        return 'grassland';
    }
}

Loot System

import { createSeededRandom, createWeightedRandom } from '@esengine/procgen';

class LootSystem {
    private rng: SeededRandom;
    private raritySelector: WeightedRandom<string>;

    constructor(seed: number) {
        this.rng = createSeededRandom(seed);
        this.raritySelector = createWeightedRandom([
            { value: 'common',    weight: 60 },
            { value: 'uncommon',  weight: 25 },
            { value: 'rare',      weight: 10 },
            { value: 'legendary', weight: 5 }
        ]);
    }

    generateLoot(count: number): LootItem[] {
        const loot: LootItem[] = [];
        for (let i = 0; i < count; i++) {
            const rarity = this.raritySelector.pick(this.rng);
            // Get item from rarity table...
            loot.push(item);
        }
        return loot;
    }
}

Blueprint Nodes

Noise Nodes

  • SampleNoise2D - Sample 2D noise
  • SampleFBM - Sample FBM noise

Random Nodes

  • SeededRandom - Generate random float
  • SeededRandomInt - Generate random integer
  • WeightedPick - Weighted random selection
  • ShuffleArray - Shuffle array
  • PickRandom - Pick random element
  • SampleArray - Sample from array
  • RandomPointInCircle - Random point in circle

Best Practices

  1. Use seeds for reproducibility

    const seed = Date.now();
    const rng = createSeededRandom(seed);
    saveSeed(seed);
    
  2. Precompute weighted selectors

    // Good: Create once, use many times
    const selector = createWeightedRandom(items);
    for (let i = 0; i < 1000; i++) {
        selector.pick(rng);
    }
    
  3. Choose appropriate noise

    • Perlin: Smooth terrain, clouds
    • Simplex: Performance-critical
    • Worley: Cell textures, stone
    • FBM: Natural multi-detail effects
  4. Tune FBM parameters

    • octaves: More = richer detail, higher cost
    • persistence: 0.5 is common, higher = more high-frequency detail
    • lacunarity: Usually 2, controls frequency growth