* 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 编写。
7.0 KiB
7.0 KiB
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
npm install @esengine/spatial
Quick Start
Spatial Index
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)
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
interface IBounds {
readonly minX: number;
readonly minY: number;
readonly maxX: number;
readonly maxY: number;
}
IRaycastHit
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
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
spatialIndex.insert(entity, position);
spatialIndex.remove(entity);
spatialIndex.update(entity, newPosition);
spatialIndex.clear();
Query Methods
findInRadius
const enemies = spatialIndex.findInRadius(
{ x: 100, y: 200 },
50,
(entity) => entity.type === 'enemy' // Optional filter
);
findInRect
import { createBounds } from '@esengine/spatial';
const bounds = createBounds(0, 0, 200, 200);
const entities = spatialIndex.findInRect(bounds);
findNearest
const nearest = spatialIndex.findNearest(
playerPosition,
500, // maxDistance
(entity) => entity.type === 'enemy'
);
findKNearest
const nearestEnemies = spatialIndex.findKNearest(
playerPosition,
5, // k
500, // maxDistance
(entity) => entity.type === 'enemy'
);
raycast / raycastFirst
const hits = spatialIndex.raycast(origin, direction, maxDistance);
const firstHit = spatialIndex.raycastFirst(origin, direction, maxDistance);
AOI API
createGridAOI
function createGridAOI<T>(cellSize?: number): GridAOI<T>
Observer Management
// 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
// 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
// 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
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
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
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
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,FindKNearestRaycast,RaycastFirst
AOI Nodes
GetEntitiesInView,GetObserversOf,CanSeeOnEntityEnterView,OnEntityExitView
Service Tokens
import { SpatialIndexToken, AOIManagerToken } from '@esengine/spatial';
services.register(SpatialIndexToken, createGridSpatialIndex(100));
services.register(AOIManagerToken, createGridAOI(100));