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:
600
docs/modules/spatial/index.md
Normal file
600
docs/modules/spatial/index.md
Normal file
@@ -0,0 +1,600 @@
|
||||
# 空间索引系统 (Spatial)
|
||||
|
||||
`@esengine/spatial` 提供了高效的空间查询和索引功能,包括范围查询、最近邻查询、射线检测和 AOI(兴趣区域)管理。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
npm install @esengine/spatial
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 空间索引
|
||||
|
||||
```typescript
|
||||
import { createGridSpatialIndex } from '@esengine/spatial';
|
||||
|
||||
// 创建空间索引(网格单元格大小为 100)
|
||||
const spatialIndex = createGridSpatialIndex<Entity>(100);
|
||||
|
||||
// 插入对象
|
||||
spatialIndex.insert(player, { x: 100, y: 200 });
|
||||
spatialIndex.insert(enemy1, { x: 150, y: 250 });
|
||||
spatialIndex.insert(enemy2, { x: 500, y: 600 });
|
||||
|
||||
// 查找半径内的对象
|
||||
const nearby = spatialIndex.findInRadius({ x: 100, y: 200 }, 100);
|
||||
console.log(nearby); // [player, enemy1]
|
||||
|
||||
// 查找最近的对象
|
||||
const nearest = spatialIndex.findNearest({ x: 100, y: 200 });
|
||||
console.log(nearest); // enemy1
|
||||
|
||||
// 更新位置
|
||||
spatialIndex.update(player, { x: 120, y: 220 });
|
||||
```
|
||||
|
||||
### AOI 兴趣区域
|
||||
|
||||
```typescript
|
||||
import { createGridAOI } from '@esengine/spatial';
|
||||
|
||||
// 创建 AOI 管理器
|
||||
const aoi = createGridAOI<Entity>(100);
|
||||
|
||||
// 添加观察者(玩家)
|
||||
aoi.addObserver(player, { x: 100, y: 100 }, { viewRange: 200 });
|
||||
aoi.addObserver(npc, { x: 150, y: 150 }, { viewRange: 150 });
|
||||
|
||||
// 监听进入/离开事件
|
||||
aoi.addListener((event) => {
|
||||
if (event.type === 'enter') {
|
||||
console.log(`${event.observer} 看到了 ${event.target}`);
|
||||
} else if (event.type === 'exit') {
|
||||
console.log(`${event.target} 离开了 ${event.observer} 的视野`);
|
||||
}
|
||||
});
|
||||
|
||||
// 更新位置(会自动触发进入/离开事件)
|
||||
aoi.updatePosition(player, { x: 200, y: 200 });
|
||||
|
||||
// 获取视野内的实体
|
||||
const visible = aoi.getEntitiesInView(player);
|
||||
```
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 空间索引 vs AOI
|
||||
|
||||
| 特性 | 空间索引 (SpatialIndex) | AOI (Area of Interest) |
|
||||
|------|------------------------|------------------------|
|
||||
| 用途 | 通用空间查询 | 实体可见性追踪 |
|
||||
| 事件 | 无事件通知 | 进入/离开事件 |
|
||||
| 方向 | 单向查询 | 双向追踪(谁看到谁) |
|
||||
| 场景 | 碰撞检测、范围攻击 | MMO 同步、NPC AI 感知 |
|
||||
|
||||
### IBounds 边界框
|
||||
|
||||
```typescript
|
||||
interface IBounds {
|
||||
readonly minX: number;
|
||||
readonly minY: number;
|
||||
readonly maxX: number;
|
||||
readonly maxY: number;
|
||||
}
|
||||
```
|
||||
|
||||
### IRaycastHit 射线检测结果
|
||||
|
||||
```typescript
|
||||
interface IRaycastHit<T> {
|
||||
readonly target: T; // 命中的对象
|
||||
readonly point: IVector2; // 命中点坐标
|
||||
readonly normal: IVector2; // 命中点法线
|
||||
readonly distance: number; // 距离射线起点的距离
|
||||
}
|
||||
```
|
||||
|
||||
## 空间索引 API
|
||||
|
||||
### createGridSpatialIndex
|
||||
|
||||
```typescript
|
||||
function createGridSpatialIndex<T>(cellSize?: number): GridSpatialIndex<T>
|
||||
```
|
||||
|
||||
创建基于均匀网格的空间索引。
|
||||
|
||||
**参数:**
|
||||
- `cellSize` - 网格单元格大小(默认 100)
|
||||
|
||||
**选择合适的 cellSize:**
|
||||
- 太小:内存占用高,查询效率降低
|
||||
- 太大:单元格内对象过多,遍历耗时
|
||||
- 建议:设置为对象平均分布间距的 1-2 倍
|
||||
|
||||
### 管理方法
|
||||
|
||||
#### insert
|
||||
|
||||
插入对象到索引:
|
||||
|
||||
```typescript
|
||||
spatialIndex.insert(enemy, { x: 100, y: 200 });
|
||||
```
|
||||
|
||||
#### remove
|
||||
|
||||
移除对象:
|
||||
|
||||
```typescript
|
||||
spatialIndex.remove(enemy);
|
||||
```
|
||||
|
||||
#### update
|
||||
|
||||
更新对象位置:
|
||||
|
||||
```typescript
|
||||
spatialIndex.update(enemy, { x: 150, y: 250 });
|
||||
```
|
||||
|
||||
#### clear
|
||||
|
||||
清空索引:
|
||||
|
||||
```typescript
|
||||
spatialIndex.clear();
|
||||
```
|
||||
|
||||
### 查询方法
|
||||
|
||||
#### findInRadius
|
||||
|
||||
查找圆形范围内的所有对象:
|
||||
|
||||
```typescript
|
||||
// 查找中心点 (100, 200) 半径 50 内的所有敌人
|
||||
const enemies = spatialIndex.findInRadius(
|
||||
{ x: 100, y: 200 },
|
||||
50,
|
||||
(entity) => entity.type === 'enemy' // 可选过滤器
|
||||
);
|
||||
```
|
||||
|
||||
#### findInRect
|
||||
|
||||
查找矩形区域内的所有对象:
|
||||
|
||||
```typescript
|
||||
import { createBounds } from '@esengine/spatial';
|
||||
|
||||
const bounds = createBounds(0, 0, 200, 200);
|
||||
const entities = spatialIndex.findInRect(bounds);
|
||||
```
|
||||
|
||||
#### findNearest
|
||||
|
||||
查找最近的对象:
|
||||
|
||||
```typescript
|
||||
// 查找最近的敌人(最大搜索距离 500)
|
||||
const nearest = spatialIndex.findNearest(
|
||||
playerPosition,
|
||||
500, // maxDistance
|
||||
(entity) => entity.type === 'enemy'
|
||||
);
|
||||
|
||||
if (nearest) {
|
||||
attackTarget(nearest);
|
||||
}
|
||||
```
|
||||
|
||||
#### findKNearest
|
||||
|
||||
查找最近的 K 个对象:
|
||||
|
||||
```typescript
|
||||
// 查找最近的 5 个敌人
|
||||
const nearestEnemies = spatialIndex.findKNearest(
|
||||
playerPosition,
|
||||
5, // k
|
||||
500, // maxDistance
|
||||
(entity) => entity.type === 'enemy'
|
||||
);
|
||||
```
|
||||
|
||||
#### raycast
|
||||
|
||||
射线检测(返回所有命中):
|
||||
|
||||
```typescript
|
||||
const hits = spatialIndex.raycast(
|
||||
origin, // 射线起点
|
||||
direction, // 射线方向(应归一化)
|
||||
maxDistance, // 最大检测距离
|
||||
filter // 可选过滤器
|
||||
);
|
||||
|
||||
// hits 按距离排序
|
||||
for (const hit of hits) {
|
||||
console.log(`命中 ${hit.target} at ${hit.point}, 距离 ${hit.distance}`);
|
||||
}
|
||||
```
|
||||
|
||||
#### raycastFirst
|
||||
|
||||
射线检测(仅返回第一个命中):
|
||||
|
||||
```typescript
|
||||
const hit = spatialIndex.raycastFirst(origin, direction, 1000);
|
||||
if (hit) {
|
||||
dealDamage(hit.target, calculateDamage(hit.distance));
|
||||
}
|
||||
```
|
||||
|
||||
### 属性
|
||||
|
||||
```typescript
|
||||
// 获取索引中的对象数量
|
||||
console.log(spatialIndex.count);
|
||||
|
||||
// 获取所有对象
|
||||
const all = spatialIndex.getAll();
|
||||
```
|
||||
|
||||
## AOI 兴趣区域 API
|
||||
|
||||
### createGridAOI
|
||||
|
||||
```typescript
|
||||
function createGridAOI<T>(cellSize?: number): GridAOI<T>
|
||||
```
|
||||
|
||||
创建基于网格的 AOI 管理器。
|
||||
|
||||
**参数:**
|
||||
- `cellSize` - 网格单元格大小(建议为平均视野范围的 1-2 倍)
|
||||
|
||||
### 观察者管理
|
||||
|
||||
#### addObserver
|
||||
|
||||
添加观察者:
|
||||
|
||||
```typescript
|
||||
aoi.addObserver(player, position, {
|
||||
viewRange: 200, // 视野范围
|
||||
observable: true // 是否可被其他观察者看到(默认 true)
|
||||
});
|
||||
|
||||
// NPC 只观察不被观察
|
||||
aoi.addObserver(camera, position, {
|
||||
viewRange: 500,
|
||||
observable: false
|
||||
});
|
||||
```
|
||||
|
||||
#### removeObserver
|
||||
|
||||
移除观察者:
|
||||
|
||||
```typescript
|
||||
aoi.removeObserver(player);
|
||||
```
|
||||
|
||||
#### updatePosition
|
||||
|
||||
更新位置(自动触发进入/离开事件):
|
||||
|
||||
```typescript
|
||||
aoi.updatePosition(player, newPosition);
|
||||
```
|
||||
|
||||
#### updateViewRange
|
||||
|
||||
更新视野范围:
|
||||
|
||||
```typescript
|
||||
// 获得增益后视野扩大
|
||||
aoi.updateViewRange(player, 300);
|
||||
```
|
||||
|
||||
### 查询方法
|
||||
|
||||
#### getEntitiesInView
|
||||
|
||||
获取观察者视野内的所有实体:
|
||||
|
||||
```typescript
|
||||
const visible = aoi.getEntitiesInView(player);
|
||||
for (const entity of visible) {
|
||||
updateEntityForPlayer(player, entity);
|
||||
}
|
||||
```
|
||||
|
||||
#### getObserversOf
|
||||
|
||||
获取能看到指定实体的所有观察者:
|
||||
|
||||
```typescript
|
||||
const observers = aoi.getObserversOf(monster);
|
||||
for (const observer of observers) {
|
||||
notifyMonsterMoved(observer, monster);
|
||||
}
|
||||
```
|
||||
|
||||
#### canSee
|
||||
|
||||
检查是否可见:
|
||||
|
||||
```typescript
|
||||
if (aoi.canSee(player, enemy)) {
|
||||
enemy.showHealthBar();
|
||||
}
|
||||
```
|
||||
|
||||
### 事件系统
|
||||
|
||||
#### 全局事件监听
|
||||
|
||||
```typescript
|
||||
aoi.addListener((event) => {
|
||||
switch (event.type) {
|
||||
case 'enter':
|
||||
console.log(`${event.observer} 看到了 ${event.target}`);
|
||||
break;
|
||||
case 'exit':
|
||||
console.log(`${event.target} 离开了 ${event.observer} 的视野`);
|
||||
break;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 实体特定事件监听
|
||||
|
||||
```typescript
|
||||
// 只监听特定玩家的视野事件
|
||||
aoi.addEntityListener(player, (event) => {
|
||||
if (event.type === 'enter') {
|
||||
sendToClient(player, 'entity_enter', event.target);
|
||||
} else if (event.type === 'exit') {
|
||||
sendToClient(player, 'entity_exit', event.target);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 事件类型
|
||||
|
||||
```typescript
|
||||
interface IAOIEvent<T> {
|
||||
type: 'enter' | 'exit' | 'update';
|
||||
observer: T; // 观察者(谁看到了变化)
|
||||
target: T; // 目标(发生变化的对象)
|
||||
position: IVector2; // 目标位置
|
||||
}
|
||||
```
|
||||
|
||||
## 工具函数
|
||||
|
||||
### 边界框创建
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createBounds,
|
||||
createBoundsFromCenter,
|
||||
createBoundsFromCircle
|
||||
} from '@esengine/spatial';
|
||||
|
||||
// 从角点创建
|
||||
const bounds1 = createBounds(0, 0, 100, 100);
|
||||
|
||||
// 从中心点和尺寸创建
|
||||
const bounds2 = createBoundsFromCenter({ x: 50, y: 50 }, 100, 100);
|
||||
|
||||
// 从圆形创建(包围盒)
|
||||
const bounds3 = createBoundsFromCircle({ x: 50, y: 50 }, 50);
|
||||
```
|
||||
|
||||
### 几何检测
|
||||
|
||||
```typescript
|
||||
import {
|
||||
isPointInBounds,
|
||||
boundsIntersect,
|
||||
boundsIntersectsCircle,
|
||||
distance,
|
||||
distanceSquared
|
||||
} from '@esengine/spatial';
|
||||
|
||||
// 点在边界内?
|
||||
if (isPointInBounds(point, bounds)) { ... }
|
||||
|
||||
// 两个边界框相交?
|
||||
if (boundsIntersect(boundsA, boundsB)) { ... }
|
||||
|
||||
// 边界框与圆形相交?
|
||||
if (boundsIntersectsCircle(bounds, center, radius)) { ... }
|
||||
|
||||
// 距离计算
|
||||
const dist = distance(pointA, pointB);
|
||||
const distSq = distanceSquared(pointA, pointB); // 更快,避免 sqrt
|
||||
```
|
||||
|
||||
## 实际示例
|
||||
|
||||
### 范围攻击检测
|
||||
|
||||
```typescript
|
||||
class CombatSystem {
|
||||
private spatialIndex: ISpatialIndex<Entity>;
|
||||
|
||||
dealAreaDamage(center: IVector2, radius: number, damage: number): void {
|
||||
const targets = this.spatialIndex.findInRadius(
|
||||
center,
|
||||
radius,
|
||||
(entity) => entity.hasComponent(HealthComponent)
|
||||
);
|
||||
|
||||
for (const target of targets) {
|
||||
const health = target.getComponent(HealthComponent);
|
||||
health.takeDamage(damage);
|
||||
}
|
||||
}
|
||||
|
||||
findNearestEnemy(position: IVector2, team: string): Entity | null {
|
||||
return this.spatialIndex.findNearest(
|
||||
position,
|
||||
undefined, // 无距离限制
|
||||
(entity) => {
|
||||
const teamComp = entity.getComponent(TeamComponent);
|
||||
return teamComp && teamComp.team !== team;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MMO 同步系统
|
||||
|
||||
```typescript
|
||||
class SyncSystem {
|
||||
private aoi: IAOIManager<Player>;
|
||||
|
||||
constructor() {
|
||||
this.aoi = createGridAOI<Player>(100);
|
||||
|
||||
// 监听进入/离开事件
|
||||
this.aoi.addListener((event) => {
|
||||
const packet = this.createSyncPacket(event);
|
||||
this.sendToPlayer(event.observer, packet);
|
||||
});
|
||||
}
|
||||
|
||||
onPlayerJoin(player: Player): void {
|
||||
this.aoi.addObserver(player, player.position, {
|
||||
viewRange: player.viewRange
|
||||
});
|
||||
}
|
||||
|
||||
onPlayerMove(player: Player, newPosition: IVector2): void {
|
||||
this.aoi.updatePosition(player, newPosition);
|
||||
}
|
||||
|
||||
onPlayerLeave(player: Player): void {
|
||||
this.aoi.removeObserver(player);
|
||||
}
|
||||
|
||||
// 广播给所有能看到某玩家的其他玩家
|
||||
broadcastToObservers(player: Player, packet: Packet): void {
|
||||
const observers = this.aoi.getObserversOf(player);
|
||||
for (const observer of observers) {
|
||||
this.sendToPlayer(observer, packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### NPC AI 感知
|
||||
|
||||
```typescript
|
||||
class AIPerceptionSystem {
|
||||
private aoi: IAOIManager<Entity>;
|
||||
|
||||
constructor() {
|
||||
this.aoi = createGridAOI<Entity>(50);
|
||||
}
|
||||
|
||||
setupNPC(npc: Entity): void {
|
||||
const perception = npc.getComponent(PerceptionComponent);
|
||||
|
||||
this.aoi.addObserver(npc, npc.position, {
|
||||
viewRange: perception.range
|
||||
});
|
||||
|
||||
// 监听该 NPC 的感知事件
|
||||
this.aoi.addEntityListener(npc, (event) => {
|
||||
const ai = npc.getComponent(AIComponent);
|
||||
|
||||
if (event.type === 'enter') {
|
||||
ai.onTargetDetected(event.target);
|
||||
} else if (event.type === 'exit') {
|
||||
ai.onTargetLost(event.target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
update(): void {
|
||||
// 更新所有 NPC 位置
|
||||
for (const npc of this.npcs) {
|
||||
this.aoi.updatePosition(npc, npc.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 蓝图节点
|
||||
|
||||
### 空间查询节点
|
||||
|
||||
- `FindInRadius` - 查找半径内的对象
|
||||
- `FindInRect` - 查找矩形内的对象
|
||||
- `FindNearest` - 查找最近的对象
|
||||
- `FindKNearest` - 查找最近的 K 个对象
|
||||
- `Raycast` - 射线检测
|
||||
- `RaycastFirst` - 射线检测(仅第一个)
|
||||
|
||||
### AOI 节点
|
||||
|
||||
- `GetEntitiesInView` - 获取视野内实体
|
||||
- `GetObserversOf` - 获取观察者
|
||||
- `CanSee` - 检查可见性
|
||||
- `OnEntityEnterView` - 进入视野事件
|
||||
- `OnEntityExitView` - 离开视野事件
|
||||
|
||||
## 服务令牌
|
||||
|
||||
在依赖注入场景中使用:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
SpatialIndexToken,
|
||||
SpatialQueryToken,
|
||||
AOIManagerToken,
|
||||
createGridSpatialIndex,
|
||||
createGridAOI
|
||||
} from '@esengine/spatial';
|
||||
|
||||
// 注册服务
|
||||
services.register(SpatialIndexToken, createGridSpatialIndex(100));
|
||||
services.register(AOIManagerToken, createGridAOI(100));
|
||||
|
||||
// 获取服务
|
||||
const spatialIndex = services.get(SpatialIndexToken);
|
||||
const aoiManager = services.get(AOIManagerToken);
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
1. **选择合适的 cellSize**
|
||||
- 太小:内存占用高,单元格数量多
|
||||
- 太大:单元格内对象多,遍历慢
|
||||
- 经验法则:对象平均间距的 1-2 倍
|
||||
|
||||
2. **使用过滤器减少结果**
|
||||
```typescript
|
||||
// 在空间查询阶段就过滤,而不是事后过滤
|
||||
spatialIndex.findInRadius(center, radius, (e) => e.type === 'enemy');
|
||||
```
|
||||
|
||||
3. **使用 distanceSquared 代替 distance**
|
||||
```typescript
|
||||
// 避免 sqrt 计算
|
||||
if (distanceSquared(a, b) < threshold * threshold) { ... }
|
||||
```
|
||||
|
||||
4. **批量更新优化**
|
||||
```typescript
|
||||
// 如果有大量对象同时移动,考虑禁用事件后批量更新
|
||||
```
|
||||
Reference in New Issue
Block a user