Files
esengine/docs/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

12 KiB
Raw Blame History

程序化生成 (Procgen)

@esengine/procgen 提供了程序化内容生成的核心工具,包括噪声函数、种子随机数和各种随机工具。

安装

npm install @esengine/procgen

快速开始

噪声生成

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

// 创建 Perlin 噪声
const perlin = createPerlinNoise(12345); // 种子

// 采样 2D 噪声
const value = perlin.noise2D(x * 0.1, y * 0.1);
console.log(value); // [-1, 1]

// 使用 FBM 获得更自然的效果
const fbm = createFBM(perlin, {
    octaves: 6,
    persistence: 0.5
});

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

种子随机数

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

// 创建确定性随机数生成器
const rng = createSeededRandom(42);

// 相同种子总是产生相同序列
console.log(rng.next());      // 0.xxx
console.log(rng.nextInt(1, 100)); // 1-100
console.log(rng.nextBool(0.3));   // 30% true

加权随机

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); // 大概率是 'common'

噪声函数

Perlin 噪声

经典的梯度噪声,输出范围 [-1, 1]

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

const perlin = createPerlinNoise(seed);

// 2D 噪声
const value2D = perlin.noise2D(x, y);

// 3D 噪声
const value3D = perlin.noise3D(x, y, z);

Simplex 噪声

比 Perlin 更快、更少方向性偏差:

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

const simplex = createSimplexNoise(seed);

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

Worley 噪声

基于细胞的噪声,适合生成石头、细胞等纹理:

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

const worley = createWorleyNoise(seed);

// 返回到最近点的距离
const distance = worley.noise2D(x, y);

FBM (分形布朗运动)

叠加多层噪声创建更丰富的细节:

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

const baseNoise = createPerlinNoise(seed);

const fbm = createFBM(baseNoise, {
    octaves: 6,        // 层数(越多细节越丰富)
    lacunarity: 2.0,   // 频率倍增因子
    persistence: 0.5,  // 振幅衰减因子
    frequency: 1.0,    // 初始频率
    amplitude: 1.0     // 初始振幅
});

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

// Ridged FBM脊状适合山脉
const ridged = fbm.ridged2D(x, y);

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

// Billowed膨胀适合云朵
const cloud = fbm.billowed2D(x, y);

种子随机数 API

SeededRandom

基于 xorshift128+ 算法的确定性伪随机数生成器:

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

const rng = createSeededRandom(42);

基础方法

// [0, 1) 浮点数
rng.next();

// [min, max] 整数
rng.nextInt(1, 10);

// [min, max) 浮点数
rng.nextFloat(0, 100);

// 布尔值(可指定概率)
rng.nextBool();      // 50%
rng.nextBool(0.3);   // 30%

// 重置到初始状态
rng.reset();

分布方法

// 正态分布(高斯分布)
rng.nextGaussian();          // 均值 0, 标准差 1
rng.nextGaussian(100, 15);   // 均值 100, 标准差 15

// 指数分布
rng.nextExponential();       // λ = 1
rng.nextExponential(0.5);    // λ = 0.5

几何方法

// 圆内均匀分布的点
const point = rng.nextPointInCircle(50); // { x, y }

// 圆周上的点
const edge = rng.nextPointOnCircle(50);  // { x, y }

// 球内均匀分布的点
const point3D = rng.nextPointInSphere(50); // { x, y, z }

// 随机方向向量
const dir = rng.nextDirection2D(); // { x, y },长度为 1

加权随机 API

WeightedRandom

预计算累积权重,高效随机选择:

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);

// 使用 Math.random
const result2 = selector.pickRandom();

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

便捷函数

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);

洗牌和采样 API

shuffle / shuffleCopy

Fisher-Yates 洗牌算法:

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

const arr = [1, 2, 3, 4, 5];

// 原地洗牌
shuffle(arr, rng);

// 创建洗牌副本(不修改原数组)
const shuffled = shuffleCopy(arr, rng);

pickOne

随机选择一个元素:

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

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

sample / sampleWithReplacement

采样:

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

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// 采样 3 个不重复元素
const unique = sample(arr, 3, rng);

// 采样 5 个(可重复)
const withRep = sampleWithReplacement(arr, 5, rng);

randomIntegers

生成范围内的随机整数数组:

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

// 从 1-100 中随机选 5 个不重复的数
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];

// 按权重选 3 个
const selected = weightedSample(items, weights, 3, rng);

实际示例

程序化地形生成

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; // 归一化到 [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';
    }
}

战利品系统

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

interface LootItem {
    id: string;
    rarity: string;
}

class LootSystem {
    private rng: SeededRandom;
    private raritySelector: WeightedRandom<string>;
    private lootTables: Map<string, LootItem[]> = new Map();

    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 }
        ]);

        // 初始化战利品表
        this.lootTables.set('common', [/* ... */]);
        this.lootTables.set('rare', [/* ... */]);
        // ...
    }

    generateLoot(count: number): LootItem[] {
        const loot: LootItem[] = [];

        for (let i = 0; i < count; i++) {
            const rarity = this.raritySelector.pick(this.rng);
            const table = this.lootTables.get(rarity)!;
            const item = pickOne(table, this.rng);
            loot.push(item);
        }

        return loot;
    }

    // 保证可重现
    setSeed(seed: number): void {
        this.rng = createSeededRandom(seed);
    }
}

程序化敌人放置

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

class EnemySpawner {
    private rng: SeededRandom;

    constructor(seed: number) {
        this.rng = createSeededRandom(seed);
    }

    spawnEnemiesInArea(
        centerX: number,
        centerY: number,
        radius: number,
        count: number
    ): Array<{ x: number; y: number; type: string }> {
        const enemies: Array<{ x: number; y: number; type: string }> = [];

        for (let i = 0; i < count; i++) {
            // 在圆内生成位置
            const pos = this.rng.nextPointInCircle(radius);

            // 随机选择敌人类型
            const type = this.rng.nextBool(0.2) ? 'elite' : 'normal';

            enemies.push({
                x: centerX + pos.x,
                y: centerY + pos.y,
                type
            });
        }

        return enemies;
    }
}

程序化关卡布局

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

interface Room {
    x: number;
    y: number;
    width: number;
    height: number;
    type: 'start' | 'combat' | 'treasure' | 'boss';
}

class DungeonGenerator {
    private rng: SeededRandom;

    constructor(seed: number) {
        this.rng = createSeededRandom(seed);
    }

    generate(roomCount: number): Room[] {
        const rooms: Room[] = [];

        // 生成房间
        for (let i = 0; i < roomCount; i++) {
            rooms.push({
                x: this.rng.nextInt(0, 100),
                y: this.rng.nextInt(0, 100),
                width: this.rng.nextInt(5, 15),
                height: this.rng.nextInt(5, 15),
                type: 'combat'
            });
        }

        // 随机分配特殊房间
        shuffle(rooms, this.rng);
        rooms[0].type = 'start';
        rooms[1].type = 'treasure';
        rooms[rooms.length - 1].type = 'boss';

        return rooms;
    }
}

蓝图节点

Procgen 模块提供了可视化脚本支持的蓝图节点:

噪声节点

  • SampleNoise2D - 采样 2D 噪声
  • SampleFBM - 采样 FBM 噪声

随机节点

  • SeededRandom - 生成随机浮点数
  • SeededRandomInt - 生成随机整数
  • WeightedPick - 加权随机选择
  • ShuffleArray - 洗牌数组
  • PickRandom - 随机选择元素
  • SampleArray - 采样数组
  • RandomPointInCircle - 圆内随机点

最佳实践

  1. 使用种子保证可重现性

    // 保存种子以便重现相同结果
    const seed = Date.now();
    const rng = createSeededRandom(seed);
    saveSeed(seed);
    
  2. 预计算加权选择器

    // 好:创建一次,多次使用
    const selector = createWeightedRandom(items);
    for (let i = 0; i < 1000; i++) {
        selector.pick(rng);
    }
    
    // 不好:每次都创建
    for (let i = 0; i < 1000; i++) {
        weightedPick(items, rng);
    }
    
  3. 选择合适的噪声函数

    • Perlin平滑过渡的地形、云彩
    • Simplex性能要求高的场景
    • Worley细胞、石头纹理
    • FBM需要多层细节的自然效果
  4. 调整 FBM 参数

    • octaves:越多细节越丰富,但性能开销越大
    • persistence0.5 是常用值,越大高频细节越明显
    • lacunarity:通常为 2控制频率增长速度