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 编写。
This commit is contained in:
557
docs/modules/procgen/index.md
Normal file
557
docs/modules/procgen/index.md
Normal file
@@ -0,0 +1,557 @@
|
||||
# 程序化生成 (Procgen)
|
||||
|
||||
`@esengine/procgen` 提供了程序化内容生成的核心工具,包括噪声函数、种子随机数和各种随机工具。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/procgen
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 噪声生成
|
||||
|
||||
```typescript
|
||||
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);
|
||||
```
|
||||
|
||||
### 种子随机数
|
||||
|
||||
```typescript
|
||||
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
|
||||
```
|
||||
|
||||
### 加权随机
|
||||
|
||||
```typescript
|
||||
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]:
|
||||
|
||||
```typescript
|
||||
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 更快、更少方向性偏差:
|
||||
|
||||
```typescript
|
||||
import { createSimplexNoise } from '@esengine/procgen';
|
||||
|
||||
const simplex = createSimplexNoise(seed);
|
||||
|
||||
const value = simplex.noise2D(x, y);
|
||||
```
|
||||
|
||||
### Worley 噪声
|
||||
|
||||
基于细胞的噪声,适合生成石头、细胞等纹理:
|
||||
|
||||
```typescript
|
||||
import { createWorleyNoise } from '@esengine/procgen';
|
||||
|
||||
const worley = createWorleyNoise(seed);
|
||||
|
||||
// 返回到最近点的距离
|
||||
const distance = worley.noise2D(x, y);
|
||||
```
|
||||
|
||||
### FBM (分形布朗运动)
|
||||
|
||||
叠加多层噪声创建更丰富的细节:
|
||||
|
||||
```typescript
|
||||
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+ 算法的确定性伪随机数生成器:
|
||||
|
||||
```typescript
|
||||
import { createSeededRandom } from '@esengine/procgen';
|
||||
|
||||
const rng = createSeededRandom(42);
|
||||
```
|
||||
|
||||
### 基础方法
|
||||
|
||||
```typescript
|
||||
// [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();
|
||||
```
|
||||
|
||||
### 分布方法
|
||||
|
||||
```typescript
|
||||
// 正态分布(高斯分布)
|
||||
rng.nextGaussian(); // 均值 0, 标准差 1
|
||||
rng.nextGaussian(100, 15); // 均值 100, 标准差 15
|
||||
|
||||
// 指数分布
|
||||
rng.nextExponential(); // λ = 1
|
||||
rng.nextExponential(0.5); // λ = 0.5
|
||||
```
|
||||
|
||||
### 几何方法
|
||||
|
||||
```typescript
|
||||
// 圆内均匀分布的点
|
||||
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
|
||||
|
||||
预计算累积权重,高效随机选择:
|
||||
|
||||
```typescript
|
||||
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
|
||||
```
|
||||
|
||||
### 便捷函数
|
||||
|
||||
```typescript
|
||||
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 洗牌算法:
|
||||
|
||||
```typescript
|
||||
import { shuffle, shuffleCopy } from '@esengine/procgen';
|
||||
|
||||
const arr = [1, 2, 3, 4, 5];
|
||||
|
||||
// 原地洗牌
|
||||
shuffle(arr, rng);
|
||||
|
||||
// 创建洗牌副本(不修改原数组)
|
||||
const shuffled = shuffleCopy(arr, rng);
|
||||
```
|
||||
|
||||
### pickOne
|
||||
|
||||
随机选择一个元素:
|
||||
|
||||
```typescript
|
||||
import { pickOne } from '@esengine/procgen';
|
||||
|
||||
const items = ['a', 'b', 'c', 'd'];
|
||||
const item = pickOne(items, rng);
|
||||
```
|
||||
|
||||
### sample / sampleWithReplacement
|
||||
|
||||
采样:
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
生成范围内的随机整数数组:
|
||||
|
||||
```typescript
|
||||
import { randomIntegers } from '@esengine/procgen';
|
||||
|
||||
// 从 1-100 中随机选 5 个不重复的数
|
||||
const nums = randomIntegers(1, 100, 5, rng);
|
||||
```
|
||||
|
||||
### weightedSample
|
||||
|
||||
按权重采样(不重复):
|
||||
|
||||
```typescript
|
||||
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);
|
||||
```
|
||||
|
||||
## 实际示例
|
||||
|
||||
### 程序化地形生成
|
||||
|
||||
```typescript
|
||||
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';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 战利品系统
|
||||
|
||||
```typescript
|
||||
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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 程序化敌人放置
|
||||
|
||||
```typescript
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 程序化关卡布局
|
||||
|
||||
```typescript
|
||||
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. **使用种子保证可重现性**
|
||||
```typescript
|
||||
// 保存种子以便重现相同结果
|
||||
const seed = Date.now();
|
||||
const rng = createSeededRandom(seed);
|
||||
saveSeed(seed);
|
||||
```
|
||||
|
||||
2. **预计算加权选择器**
|
||||
```typescript
|
||||
// 好:创建一次,多次使用
|
||||
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`:越多细节越丰富,但性能开销越大
|
||||
- `persistence`:0.5 是常用值,越大高频细节越明显
|
||||
- `lacunarity`:通常为 2,控制频率增长速度
|
||||
Reference in New Issue
Block a user