* 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 编写。
12 KiB
12 KiB
程序化生成 (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- 圆内随机点
最佳实践
-
使用种子保证可重现性
// 保存种子以便重现相同结果 const seed = Date.now(); const rng = createSeededRandom(seed); saveSeed(seed); -
预计算加权选择器
// 好:创建一次,多次使用 const selector = createWeightedRandom(items); for (let i = 0; i < 1000; i++) { selector.pick(rng); } // 不好:每次都创建 for (let i = 0; i < 1000; i++) { weightedPick(items, rng); } -
选择合适的噪声函数
- Perlin:平滑过渡的地形、云彩
- Simplex:性能要求高的场景
- Worley:细胞、石头纹理
- FBM:需要多层细节的自然效果
-
调整 FBM 参数
octaves:越多细节越丰富,但性能开销越大persistence:0.5 是常用值,越大高频细节越明显lacunarity:通常为 2,控制频率增长速度