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

601 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 空间索引系统 (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
// 如果有大量对象同时移动,考虑禁用事件后批量更新
```