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:
322
docs/en/modules/spatial/index.md
Normal file
322
docs/en/modules/spatial/index.md
Normal file
@@ -0,0 +1,322 @@
|
||||
# Spatial Index System
|
||||
|
||||
`@esengine/spatial` provides efficient spatial querying and indexing, including range queries, nearest neighbor queries, raycasting, and AOI (Area of Interest) management.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @esengine/spatial
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Spatial Index
|
||||
|
||||
```typescript
|
||||
import { createGridSpatialIndex } from '@esengine/spatial';
|
||||
|
||||
// Create spatial index (cell size 100)
|
||||
const spatialIndex = createGridSpatialIndex<Entity>(100);
|
||||
|
||||
// Insert objects
|
||||
spatialIndex.insert(player, { x: 100, y: 200 });
|
||||
spatialIndex.insert(enemy1, { x: 150, y: 250 });
|
||||
spatialIndex.insert(enemy2, { x: 500, y: 600 });
|
||||
|
||||
// Find objects within radius
|
||||
const nearby = spatialIndex.findInRadius({ x: 100, y: 200 }, 100);
|
||||
console.log(nearby); // [player, enemy1]
|
||||
|
||||
// Find nearest object
|
||||
const nearest = spatialIndex.findNearest({ x: 100, y: 200 });
|
||||
console.log(nearest); // enemy1
|
||||
|
||||
// Update position
|
||||
spatialIndex.update(player, { x: 120, y: 220 });
|
||||
```
|
||||
|
||||
### AOI (Area of Interest)
|
||||
|
||||
```typescript
|
||||
import { createGridAOI } from '@esengine/spatial';
|
||||
|
||||
// Create AOI manager
|
||||
const aoi = createGridAOI<Entity>(100);
|
||||
|
||||
// Add observers
|
||||
aoi.addObserver(player, { x: 100, y: 100 }, { viewRange: 200 });
|
||||
aoi.addObserver(npc, { x: 150, y: 150 }, { viewRange: 150 });
|
||||
|
||||
// Listen to enter/exit events
|
||||
aoi.addListener((event) => {
|
||||
if (event.type === 'enter') {
|
||||
console.log(`${event.observer} saw ${event.target}`);
|
||||
} else if (event.type === 'exit') {
|
||||
console.log(`${event.target} left ${event.observer}'s view`);
|
||||
}
|
||||
});
|
||||
|
||||
// Update position (triggers enter/exit events)
|
||||
aoi.updatePosition(player, { x: 200, y: 200 });
|
||||
|
||||
// Get visible entities
|
||||
const visible = aoi.getEntitiesInView(player);
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Spatial Index vs AOI
|
||||
|
||||
| Feature | SpatialIndex | AOI |
|
||||
|---------|--------------|-----|
|
||||
| Purpose | General spatial queries | Entity visibility tracking |
|
||||
| Events | No event notification | Enter/exit events |
|
||||
| Direction | One-way query | Two-way tracking |
|
||||
| Use Cases | Collision, range attacks | MMO sync, NPC AI perception |
|
||||
|
||||
### IBounds
|
||||
|
||||
```typescript
|
||||
interface IBounds {
|
||||
readonly minX: number;
|
||||
readonly minY: number;
|
||||
readonly maxX: number;
|
||||
readonly maxY: number;
|
||||
}
|
||||
```
|
||||
|
||||
### IRaycastHit
|
||||
|
||||
```typescript
|
||||
interface IRaycastHit<T> {
|
||||
readonly target: T; // Hit object
|
||||
readonly point: IVector2; // Hit point
|
||||
readonly normal: IVector2;// Hit normal
|
||||
readonly distance: number;// Distance from origin
|
||||
}
|
||||
```
|
||||
|
||||
## Spatial Index API
|
||||
|
||||
### createGridSpatialIndex
|
||||
|
||||
```typescript
|
||||
function createGridSpatialIndex<T>(cellSize?: number): GridSpatialIndex<T>
|
||||
```
|
||||
|
||||
**Choosing cellSize:**
|
||||
- Too small: High memory, reduced query efficiency
|
||||
- Too large: Many objects per cell, slow iteration
|
||||
- Recommended: 1-2x average object spacing
|
||||
|
||||
### Management Methods
|
||||
|
||||
```typescript
|
||||
spatialIndex.insert(entity, position);
|
||||
spatialIndex.remove(entity);
|
||||
spatialIndex.update(entity, newPosition);
|
||||
spatialIndex.clear();
|
||||
```
|
||||
|
||||
### Query Methods
|
||||
|
||||
#### findInRadius
|
||||
|
||||
```typescript
|
||||
const enemies = spatialIndex.findInRadius(
|
||||
{ x: 100, y: 200 },
|
||||
50,
|
||||
(entity) => entity.type === 'enemy' // Optional filter
|
||||
);
|
||||
```
|
||||
|
||||
#### findInRect
|
||||
|
||||
```typescript
|
||||
import { createBounds } from '@esengine/spatial';
|
||||
|
||||
const bounds = createBounds(0, 0, 200, 200);
|
||||
const entities = spatialIndex.findInRect(bounds);
|
||||
```
|
||||
|
||||
#### findNearest
|
||||
|
||||
```typescript
|
||||
const nearest = spatialIndex.findNearest(
|
||||
playerPosition,
|
||||
500, // maxDistance
|
||||
(entity) => entity.type === 'enemy'
|
||||
);
|
||||
```
|
||||
|
||||
#### findKNearest
|
||||
|
||||
```typescript
|
||||
const nearestEnemies = spatialIndex.findKNearest(
|
||||
playerPosition,
|
||||
5, // k
|
||||
500, // maxDistance
|
||||
(entity) => entity.type === 'enemy'
|
||||
);
|
||||
```
|
||||
|
||||
#### raycast / raycastFirst
|
||||
|
||||
```typescript
|
||||
const hits = spatialIndex.raycast(origin, direction, maxDistance);
|
||||
const firstHit = spatialIndex.raycastFirst(origin, direction, maxDistance);
|
||||
```
|
||||
|
||||
## AOI API
|
||||
|
||||
### createGridAOI
|
||||
|
||||
```typescript
|
||||
function createGridAOI<T>(cellSize?: number): GridAOI<T>
|
||||
```
|
||||
|
||||
### Observer Management
|
||||
|
||||
```typescript
|
||||
// Add observer
|
||||
aoi.addObserver(player, position, {
|
||||
viewRange: 200,
|
||||
observable: true // Can be seen by others
|
||||
});
|
||||
|
||||
// Remove observer
|
||||
aoi.removeObserver(player);
|
||||
|
||||
// Update position
|
||||
aoi.updatePosition(player, newPosition);
|
||||
|
||||
// Update view range
|
||||
aoi.updateViewRange(player, 300);
|
||||
```
|
||||
|
||||
### Query Methods
|
||||
|
||||
```typescript
|
||||
// Get entities in observer's view
|
||||
const visible = aoi.getEntitiesInView(player);
|
||||
|
||||
// Get observers who can see entity
|
||||
const observers = aoi.getObserversOf(monster);
|
||||
|
||||
// Check visibility
|
||||
if (aoi.canSee(player, enemy)) { ... }
|
||||
```
|
||||
|
||||
### Event System
|
||||
|
||||
```typescript
|
||||
// Global event listener
|
||||
aoi.addListener((event) => {
|
||||
switch (event.type) {
|
||||
case 'enter': /* entered view */ break;
|
||||
case 'exit': /* left view */ break;
|
||||
}
|
||||
});
|
||||
|
||||
// Entity-specific listener
|
||||
aoi.addEntityListener(player, (event) => {
|
||||
if (event.type === 'enter') {
|
||||
sendToClient(player, 'entity_enter', event.target);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Utility Functions
|
||||
|
||||
### Bounds Creation
|
||||
|
||||
```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);
|
||||
```
|
||||
|
||||
### Geometry Checks
|
||||
|
||||
```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); // Faster
|
||||
```
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Range Attack Detection
|
||||
|
||||
```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) {
|
||||
target.getComponent(HealthComponent).takeDamage(damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MMO Sync System
|
||||
|
||||
```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);
|
||||
});
|
||||
}
|
||||
|
||||
onPlayerMove(player: Player, newPosition: IVector2): void {
|
||||
this.aoi.updatePosition(player, newPosition);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Blueprint Nodes
|
||||
|
||||
### Spatial Query Nodes
|
||||
- `FindInRadius`, `FindInRect`, `FindNearest`, `FindKNearest`
|
||||
- `Raycast`, `RaycastFirst`
|
||||
|
||||
### AOI Nodes
|
||||
- `GetEntitiesInView`, `GetObserversOf`, `CanSee`
|
||||
- `OnEntityEnterView`, `OnEntityExitView`
|
||||
|
||||
## Service Tokens
|
||||
|
||||
```typescript
|
||||
import { SpatialIndexToken, AOIManagerToken } from '@esengine/spatial';
|
||||
|
||||
services.register(SpatialIndexToken, createGridSpatialIndex(100));
|
||||
services.register(AOIManagerToken, createGridAOI(100));
|
||||
```
|
||||
Reference in New Issue
Block a user