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

13 KiB
Raw Blame History

空间索引系统 (Spatial)

@esengine/spatial 提供了高效的空间查询和索引功能,包括范围查询、最近邻查询、射线检测和 AOI兴趣区域管理。

安装

npm install @esengine/spatial

快速开始

空间索引

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 兴趣区域

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 边界框

interface IBounds {
    readonly minX: number;
    readonly minY: number;
    readonly maxX: number;
    readonly maxY: number;
}

IRaycastHit 射线检测结果

interface IRaycastHit<T> {
    readonly target: T;     // 命中的对象
    readonly point: IVector2; // 命中点坐标
    readonly normal: IVector2; // 命中点法线
    readonly distance: number; // 距离射线起点的距离
}

空间索引 API

createGridSpatialIndex

function createGridSpatialIndex<T>(cellSize?: number): GridSpatialIndex<T>

创建基于均匀网格的空间索引。

参数:

  • cellSize - 网格单元格大小(默认 100

选择合适的 cellSize

  • 太小:内存占用高,查询效率降低
  • 太大:单元格内对象过多,遍历耗时
  • 建议:设置为对象平均分布间距的 1-2 倍

管理方法

insert

插入对象到索引:

spatialIndex.insert(enemy, { x: 100, y: 200 });

remove

移除对象:

spatialIndex.remove(enemy);

update

更新对象位置:

spatialIndex.update(enemy, { x: 150, y: 250 });

clear

清空索引:

spatialIndex.clear();

查询方法

findInRadius

查找圆形范围内的所有对象:

// 查找中心点 (100, 200) 半径 50 内的所有敌人
const enemies = spatialIndex.findInRadius(
    { x: 100, y: 200 },
    50,
    (entity) => entity.type === 'enemy' // 可选过滤器
);

findInRect

查找矩形区域内的所有对象:

import { createBounds } from '@esengine/spatial';

const bounds = createBounds(0, 0, 200, 200);
const entities = spatialIndex.findInRect(bounds);

findNearest

查找最近的对象:

// 查找最近的敌人(最大搜索距离 500
const nearest = spatialIndex.findNearest(
    playerPosition,
    500, // maxDistance
    (entity) => entity.type === 'enemy'
);

if (nearest) {
    attackTarget(nearest);
}

findKNearest

查找最近的 K 个对象:

// 查找最近的 5 个敌人
const nearestEnemies = spatialIndex.findKNearest(
    playerPosition,
    5,    // k
    500,  // maxDistance
    (entity) => entity.type === 'enemy'
);

raycast

射线检测(返回所有命中):

const hits = spatialIndex.raycast(
    origin,      // 射线起点
    direction,   // 射线方向(应归一化)
    maxDistance, // 最大检测距离
    filter       // 可选过滤器
);

// hits 按距离排序
for (const hit of hits) {
    console.log(`命中 ${hit.target} at ${hit.point}, 距离 ${hit.distance}`);
}

raycastFirst

射线检测(仅返回第一个命中):

const hit = spatialIndex.raycastFirst(origin, direction, 1000);
if (hit) {
    dealDamage(hit.target, calculateDamage(hit.distance));
}

属性

// 获取索引中的对象数量
console.log(spatialIndex.count);

// 获取所有对象
const all = spatialIndex.getAll();

AOI 兴趣区域 API

createGridAOI

function createGridAOI<T>(cellSize?: number): GridAOI<T>

创建基于网格的 AOI 管理器。

参数:

  • cellSize - 网格单元格大小(建议为平均视野范围的 1-2 倍)

观察者管理

addObserver

添加观察者:

aoi.addObserver(player, position, {
    viewRange: 200,      // 视野范围
    observable: true     // 是否可被其他观察者看到(默认 true
});

// NPC 只观察不被观察
aoi.addObserver(camera, position, {
    viewRange: 500,
    observable: false
});

removeObserver

移除观察者:

aoi.removeObserver(player);

updatePosition

更新位置(自动触发进入/离开事件):

aoi.updatePosition(player, newPosition);

updateViewRange

更新视野范围:

// 获得增益后视野扩大
aoi.updateViewRange(player, 300);

查询方法

getEntitiesInView

获取观察者视野内的所有实体:

const visible = aoi.getEntitiesInView(player);
for (const entity of visible) {
    updateEntityForPlayer(player, entity);
}

getObserversOf

获取能看到指定实体的所有观察者:

const observers = aoi.getObserversOf(monster);
for (const observer of observers) {
    notifyMonsterMoved(observer, monster);
}

canSee

检查是否可见:

if (aoi.canSee(player, enemy)) {
    enemy.showHealthBar();
}

事件系统

全局事件监听

aoi.addListener((event) => {
    switch (event.type) {
        case 'enter':
            console.log(`${event.observer} 看到了 ${event.target}`);
            break;
        case 'exit':
            console.log(`${event.target} 离开了 ${event.observer} 的视野`);
            break;
    }
});

实体特定事件监听

// 只监听特定玩家的视野事件
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);
    }
});

事件类型

interface IAOIEvent<T> {
    type: 'enter' | 'exit' | 'update';
    observer: T;  // 观察者(谁看到了变化)
    target: T;    // 目标(发生变化的对象)
    position: IVector2; // 目标位置
}

工具函数

边界框创建

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);

几何检测

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

实际示例

范围攻击检测

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 同步系统

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 感知

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 - 离开视野事件

服务令牌

在依赖注入场景中使用:

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. 使用过滤器减少结果

    // 在空间查询阶段就过滤,而不是事后过滤
    spatialIndex.findInRadius(center, radius, (e) => e.type === 'enemy');
    
  3. 使用 distanceSquared 代替 distance

    // 避免 sqrt 计算
    if (distanceSquared(a, b) < threshold * threshold) { ... }
    
  4. 批量更新优化

    // 如果有大量对象同时移动,考虑禁用事件后批量更新