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

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, FindKNearest
  • Raycast, RaycastFirst

AOI Nodes

  • GetEntitiesInView, GetObserversOf, CanSee
  • OnEntityEnterView, OnEntityExitView

Service Tokens

import { SpatialIndexToken, AOIManagerToken } from '@esengine/spatial';

services.register(SpatialIndexToken, createGridSpatialIndex(100));
services.register(AOIManagerToken, createGridAOI(100));