Files
esengine/docs/guide/entity-query.md
YHH cd6ef222d1 feat(ecs): 核心系统改进 - 句柄、调度、变更检测与查询编译 (#304)
新增功能:
- EntityHandle: 轻量级实体句柄 (28位索引 + 20位代数)
- SystemScheduler: 声明式系统调度,支持 @Stage/@Before/@After/@InSet 装饰器
- EpochManager: 帧级变更检测
- CompiledQuery: 预编译类型安全查询

API 改进:
- EntitySystem 添加 getBefore()/getAfter()/getSets() getter 方法
- Entity 添加 markDirty() 辅助方法
- IScene 添加 epochManager 属性
- CommandBuffer.pendingCount 修正为返回实际操作数

文档更新:
- 更新系统调度和查询相关文档
2025-12-15 09:17:00 +08:00

20 KiB
Raw Blame History

实体查询系统

实体查询是 ECS 架构的核心功能之一。本指南将介绍如何使用 Matcher 和 QuerySystem 来查询和筛选实体。

核心概念

Matcher - 查询条件描述符

Matcher 是一个链式 API,用于描述实体查询条件。它本身不执行查询,而是作为条件传递给 EntitySystem 或 QuerySystem。

QuerySystem - 查询执行引擎

QuerySystem 负责实际执行查询,内部使用响应式查询机制自动优化性能。

在 EntitySystem 中使用 Matcher

这是最常见的使用方式。EntitySystem 通过 Matcher 自动筛选和处理符合条件的实体。

基础用法

import { EntitySystem, Matcher, Entity, Component } from '@esengine/ecs-framework';

class PositionComponent extends Component {
    public x: number = 0;
    public y: number = 0;
}

class VelocityComponent extends Component {
    public vx: number = 0;
    public vy: number = 0;
}

class MovementSystem extends EntitySystem {
    constructor() {
        // 方式1: 使用 Matcher.empty().all()
        super(Matcher.empty().all(PositionComponent, VelocityComponent));

        // 方式2: 直接使用 Matcher.all() (等价)
        // super(Matcher.all(PositionComponent, VelocityComponent));
    }

    protected process(entities: readonly Entity[]): void {
        for (const entity of entities) {
            const pos = entity.getComponent(PositionComponent)!;
            const vel = entity.getComponent(VelocityComponent)!;

            pos.x += vel.vx;
            pos.y += vel.vy;
        }
    }
}

// 添加到场景
scene.addEntityProcessor(new MovementSystem());

Matcher 链式 API

all() - 必须包含所有组件

class HealthSystem extends EntitySystem {
    constructor() {
        // 实体必须同时拥有 Health 和 Position 组件
        super(Matcher.empty().all(HealthComponent, PositionComponent));
    }

    protected process(entities: readonly Entity[]): void {
        // 只处理同时拥有两个组件的实体
    }
}

any() - 至少包含一个组件

class DamageableSystem extends EntitySystem {
    constructor() {
        // 实体至少拥有 Health 或 Shield 其中之一
        super(Matcher.any(HealthComponent, ShieldComponent));
    }

    protected process(entities: readonly Entity[]): void {
        // 处理拥有生命值或护盾的实体
    }
}

none() - 不能包含指定组件

class AliveEntitySystem extends EntitySystem {
    constructor() {
        // 实体不能拥有 DeadTag 组件
        super(Matcher.all(HealthComponent).none(DeadTag));
    }

    protected process(entities: readonly Entity[]): void {
        // 只处理活着的实体
    }
}

组合条件

class CombatSystem extends EntitySystem {
    constructor() {
        super(
            Matcher.empty()
                .all(PositionComponent, HealthComponent)  // 必须有位置和生命
                .any(WeaponComponent, MagicComponent)      // 至少有武器或魔法
                .none(DeadTag, FrozenTag)                  // 不能是死亡或冰冻状态
        );
    }

    protected process(entities: readonly Entity[]): void {
        // 处理可以战斗的活着的实体
    }
}

nothing() - 不匹配任何实体

用于创建只需要生命周期方法(onBeginonEnd)但不需要处理实体的系统。

class FrameTimerSystem extends EntitySystem {
    constructor() {
        // 不匹配任何实体
        super(Matcher.nothing());
    }

    protected onBegin(): void {
        // 每帧开始时执行
        Performance.markFrameStart();
    }

    protected process(entities: readonly Entity[]): void {
        // 永远不会被调用,因为没有匹配的实体
    }

    protected onEnd(): void {
        // 每帧结束时执行
        Performance.markFrameEnd();
    }
}

empty() vs nothing() 的区别

方法 行为 使用场景
Matcher.empty() 匹配所有实体 需要处理场景中所有实体
Matcher.nothing() 不匹配任何实体 只需要生命周期回调,不处理实体
// empty() - 返回场景中的所有实体
class AllEntitiesSystem extends EntitySystem {
    constructor() {
        super(Matcher.empty());
    }

    protected process(entities: readonly Entity[]): void {
        // entities 包含场景中的所有实体
        console.log(`场景中共有 ${entities.length} 个实体`);
    }
}

// nothing() - 不返回任何实体
class NoEntitiesSystem extends EntitySystem {
    constructor() {
        super(Matcher.nothing());
    }

    protected process(entities: readonly Entity[]): void {
        // entities 永远是空数组,此方法不会被调用
    }
}

按标签查询

class PlayerSystem extends EntitySystem {
    constructor() {
        // 查询特定标签的实体
        super(Matcher.empty().withTag(Tags.PLAYER));
    }

    protected process(entities: readonly Entity[]): void {
        // 只处理玩家实体
    }
}

按名称查询

class BossSystem extends EntitySystem {
    constructor() {
        // 查询特定名称的实体
        super(Matcher.empty().withName('Boss'));
    }

    protected process(entities: readonly Entity[]): void {
        // 只处理名为 'Boss' 的实体
    }
}

直接使用 QuerySystem

如果不需要创建系统,可以直接使用 Scene 的 querySystem 进行查询。

基础查询方法

// 获取场景的查询系统
const querySystem = scene.querySystem;

// 查询拥有所有指定组件的实体
const result1 = querySystem.queryAll(PositionComponent, VelocityComponent);
console.log(`找到 ${result1.count} 个移动实体`);
console.log(`查询耗时: ${result1.executionTime.toFixed(2)}ms`);

// 查询拥有任意指定组件的实体
const result2 = querySystem.queryAny(WeaponComponent, MagicComponent);
console.log(`找到 ${result2.count} 个战斗单位`);

// 查询不包含指定组件的实体
const result3 = querySystem.queryNone(DeadTag);
console.log(`找到 ${result3.count} 个活着的实体`);

按标签查询

const playerResult = querySystem.queryByTag(Tags.PLAYER);
for (const player of playerResult.entities) {
    console.log('玩家:', player.name);
}

按名称查询

const bossResult = querySystem.queryByName('Boss');
if (bossResult.count > 0) {
    const boss = bossResult.entities[0];
    console.log('找到Boss:', boss);
}

按单个组件查询

const healthResult = querySystem.queryByComponent(HealthComponent);
console.log(`有 ${healthResult.count} 个实体拥有生命值`);

性能优化

自动缓存

QuerySystem 内部使用响应式查询自动缓存结果,相同的查询条件会直接使用缓存:

// 第一次查询,执行实际查询
const result1 = querySystem.queryAll(PositionComponent);
console.log('fromCache:', result1.fromCache); // false

// 第二次相同查询,使用缓存
const result2 = querySystem.queryAll(PositionComponent);
console.log('fromCache:', result2.fromCache); // true

实体变化自动更新

当实体添加/移除组件时,查询缓存会自动更新:

// 查询拥有武器的实体
const before = querySystem.queryAll(WeaponComponent);
console.log('之前:', before.count); // 假设为 5

// 给实体添加武器
const enemy = scene.createEntity('Enemy');
enemy.addComponent(new WeaponComponent());

// 再次查询,自动包含新实体
const after = querySystem.queryAll(WeaponComponent);
console.log('之后:', after.count); // 现在是 6

查询性能统计

const stats = querySystem.getStats();
console.log('总查询次数:', stats.queryStats.totalQueries);
console.log('缓存命中率:', stats.queryStats.cacheHitRate);
console.log('缓存大小:', stats.cacheStats.size);

实际应用场景

场景1: 物理系统

class PhysicsSystem extends EntitySystem {
    constructor() {
        super(Matcher.empty().all(TransformComponent, RigidbodyComponent));
    }

    protected process(entities: readonly Entity[]): void {
        for (const entity of entities) {
            const transform = entity.getComponent(TransformComponent)!;
            const rigidbody = entity.getComponent(RigidbodyComponent)!;

            // 应用重力
            rigidbody.velocity.y -= 9.8 * Time.deltaTime;

            // 更新位置
            transform.position.x += rigidbody.velocity.x * Time.deltaTime;
            transform.position.y += rigidbody.velocity.y * Time.deltaTime;
        }
    }
}

场景2: 渲染系统

class RenderSystem extends EntitySystem {
    constructor() {
        super(
            Matcher.empty()
                .all(TransformComponent, SpriteComponent)
                .none(InvisibleTag)  // 排除不可见实体
        );
    }

    protected process(entities: readonly Entity[]): void {
        // 按 z-order 排序
        const sorted = entities.slice().sort((a, b) => {
            const zA = a.getComponent(TransformComponent)!.z;
            const zB = b.getComponent(TransformComponent)!.z;
            return zA - zB;
        });

        // 渲染实体
        for (const entity of sorted) {
            const transform = entity.getComponent(TransformComponent)!;
            const sprite = entity.getComponent(SpriteComponent)!;

            renderer.drawSprite(sprite.texture, transform.position);
        }
    }
}

场景3: 碰撞检测

class CollisionSystem extends EntitySystem {
    constructor() {
        super(Matcher.empty().all(TransformComponent, ColliderComponent));
    }

    protected process(entities: readonly Entity[]): void {
        // 简单的 O(n²) 碰撞检测
        for (let i = 0; i < entities.length; i++) {
            for (let j = i + 1; j < entities.length; j++) {
                this.checkCollision(entities[i], entities[j]);
            }
        }
    }

    private checkCollision(a: Entity, b: Entity): void {
        const transA = a.getComponent(TransformComponent)!;
        const transB = b.getComponent(TransformComponent)!;
        const colliderA = a.getComponent(ColliderComponent)!;
        const colliderB = b.getComponent(ColliderComponent)!;

        if (this.isOverlapping(transA, colliderA, transB, colliderB)) {
            // 触发碰撞事件
            scene.eventSystem.emit('collision', { entityA: a, entityB: b });
        }
    }

    private isOverlapping(...args: any[]): boolean {
        // 碰撞检测逻辑
        return false;
    }
}

场景4: 一次性查询

// 在系统外部执行一次性查询
class GameManager {
    private scene: Scene;

    public countEnemies(): number {
        const result = this.scene.querySystem.queryByTag(Tags.ENEMY);
        return result.count;
    }

    public findNearestEnemy(playerPos: Vector2): Entity | null {
        const enemies = this.scene.querySystem.queryByTag(Tags.ENEMY);

        let nearest: Entity | null = null;
        let minDistance = Infinity;

        for (const enemy of enemies.entities) {
            const transform = enemy.getComponent(TransformComponent);
            if (!transform) continue;

            const distance = Vector2.distance(playerPos, transform.position);
            if (distance < minDistance) {
                minDistance = distance;
                nearest = enemy;
            }
        }

        return nearest;
    }
}

编译查询 (CompiledQuery)

v2.4.0+

CompiledQuery 是一个轻量级的查询工具,提供类型安全的组件访问和变更检测支持。适合临时查询、工具开发和简单的迭代场景。

基本用法

// 创建编译查询
const query = scene.querySystem.compile(Position, Velocity);

// 类型安全的遍历 - 组件参数自动推断类型
query.forEach((entity, pos, vel) => {
    pos.x += vel.vx * deltaTime;
    pos.y += vel.vy * deltaTime;
});

// 获取实体数量
console.log(`匹配实体数: ${query.count}`);

// 获取第一个匹配的实体
const first = query.first();
if (first) {
    const [entity, pos, vel] = first;
    console.log(`第一个实体: ${entity.name}`);
}

变更检测

CompiledQuery 支持基于 epoch 的变更检测:

class RenderSystem extends EntitySystem {
    private _query: CompiledQuery<[typeof Transform, typeof Sprite]>;
    private _lastEpoch = 0;

    protected onInitialize(): void {
        this._query = this.scene!.querySystem.compile(Transform, Sprite);
    }

    protected process(entities: readonly Entity[]): void {
        // 只处理 Transform 或 Sprite 发生变化的实体
        this._query.forEachChanged(this._lastEpoch, (entity, transform, sprite) => {
            this.updateRenderData(entity, transform, sprite);
        });

        // 保存当前 epoch 作为下次检查的起点
        this._lastEpoch = this.scene!.epochManager.current;
    }

    private updateRenderData(entity: Entity, transform: Transform, sprite: Sprite): void {
        // 更新渲染数据
    }
}

函数式 API

CompiledQuery 提供了丰富的函数式 API

const query = scene.querySystem.compile(Position, Health);

// map - 转换实体数据
const positions = query.map((entity, pos, health) => ({
    x: pos.x,
    y: pos.y,
    healthPercent: health.current / health.max
}));

// filter - 过滤实体
const lowHealthEntities = query.filter((entity, pos, health) => {
    return health.current < health.max * 0.2;
});

// find - 查找第一个匹配的实体
const target = query.find((entity, pos, health) => {
    return health.current > 0 && pos.x > 100;
});

// toArray - 转换为数组
const allData = query.toArray();
for (const [entity, pos, health] of allData) {
    console.log(`${entity.name}: ${pos.x}, ${pos.y}`);
}

// any/empty - 检查是否有匹配
if (query.any()) {
    console.log('有匹配的实体');
}
if (query.empty()) {
    console.log('没有匹配的实体');
}

CompiledQuery vs EntitySystem

特性 CompiledQuery EntitySystem
用途 轻量级查询工具 完整的系统逻辑
生命周期 完整 (onInitialize, onDestroy 等)
调度集成 支持 @Stage, @Before, @After
变更检测 forEachChanged forEachChanged
事件监听 addEventListener
命令缓冲 this.commands
类型安全组件 forEach 参数自动推断 需要手动 getComponent
适用场景 临时查询、工具、原型 核心游戏逻辑

选择建议

  • 使用 EntitySystem 处理核心游戏逻辑移动、战斗、AI 等)
  • 使用 CompiledQuery 进行一次性查询、工具开发或简单迭代

CompiledQuery API 参考

方法 说明
forEach(callback) 遍历所有匹配实体,类型安全的组件参数
forEachChanged(sinceEpoch, callback) 只遍历变更的实体
first() 获取第一个匹配的实体和组件
toArray() 转换为 [entity, ...components] 数组
map(callback) 映射转换
filter(predicate) 过滤实体
find(predicate) 查找第一个满足条件的实体
any() 是否有任何匹配
empty() 是否没有匹配
count 匹配的实体数量
entities 匹配的实体列表(只读)

最佳实践

1. 优先使用 EntitySystem

// 推荐: 使用 EntitySystem
class GoodSystem extends EntitySystem {
    constructor() {
        super(Matcher.empty().all(HealthComponent));
    }

    protected process(entities: readonly Entity[]): void {
        // 自动获得符合条件的实体,每帧自动更新
    }
}

// 不推荐: 在 update 中手动查询
class BadSystem extends EntitySystem {
    constructor() {
        super(Matcher.empty());
    }

    protected process(entities: readonly Entity[]): void {
        // 每帧手动查询,浪费性能
        const result = this.scene!.querySystem.queryAll(HealthComponent);
        for (const entity of result.entities) {
            // ...
        }
    }
}

2. 合理使用 none() 排除条件

// 排除已死亡的敌人
class EnemyAISystem extends EntitySystem {
    constructor() {
        super(
            Matcher.empty()
                .all(EnemyTag, AIComponent)
                .none(DeadTag)  // 不处理死亡的敌人
        );
    }
}

3. 使用标签优化查询

// 不好: 查询所有实体再过滤
const allEntities = scene.querySystem.getAllEntities();
const players = allEntities.filter(e => e.hasComponent(PlayerTag));

// 好: 直接按标签查询
const players = scene.querySystem.queryByTag(Tags.PLAYER).entities;

4. 避免过于复杂的查询条件

// 不推荐: 过于复杂
super(
    Matcher.empty()
        .all(A, B, C, D)
        .any(E, F, G)
        .none(H, I, J)
);

// 推荐: 拆分成多个简单系统
class SystemAB extends EntitySystem {
    constructor() {
        super(Matcher.empty().all(A, B));
    }
}

class SystemCD extends EntitySystem {
    constructor() {
        super(Matcher.empty().all(C, D));
    }
}

注意事项

1. 查询结果是只读的

const result = querySystem.queryAll(PositionComponent);

// 不要修改返回的数组
result.entities.push(someEntity);  // 错误!

// 如果需要修改,先复制
const mutableArray = [...result.entities];
mutableArray.push(someEntity);  // 正确

2. 组件添加/移除后的查询时机

// 创建实体并添加组件
const entity = scene.createEntity('Player');
entity.addComponent(new PositionComponent());

// 立即查询可能获取到新实体
const result = scene.querySystem.queryAll(PositionComponent);
// result.entities 包含新创建的实体

3. Matcher 是不可变的

const matcher = Matcher.empty().all(PositionComponent);

// 链式调用返回新的 Matcher 实例
const matcher2 = matcher.any(VelocityComponent);

// matcher 本身不变
console.log(matcher === matcher2); // false

Matcher API 快速参考

静态创建方法

方法 说明 示例
Matcher.all(...types) 必须包含所有指定组件 Matcher.all(Position, Velocity)
Matcher.any(...types) 至少包含一个指定组件 Matcher.any(Health, Shield)
Matcher.none(...types) 不能包含任何指定组件 Matcher.none(Dead)
Matcher.byTag(tag) 按标签查询 Matcher.byTag(1)
Matcher.byName(name) 按名称查询 Matcher.byName("Player")
Matcher.byComponent(type) 按单个组件查询 Matcher.byComponent(Health)
Matcher.empty() 创建空匹配器(匹配所有实体) Matcher.empty()
Matcher.nothing() 不匹配任何实体 Matcher.nothing()
Matcher.complex() 创建复杂查询构建器 Matcher.complex()

链式方法

方法 说明 示例
.all(...types) 添加必须包含的组件 .all(Position)
.any(...types) 添加可选组件(至少一个) .any(Weapon, Magic)
.none(...types) 添加排除的组件 .none(Dead)
.exclude(...types) .none() 的别名 .exclude(Disabled)
.one(...types) .any() 的别名 .one(Player, Enemy)
.withTag(tag) 添加标签条件 .withTag(1)
.withName(name) 添加名称条件 .withName("Boss")
.withComponent(type) 添加单组件条件 .withComponent(Health)

实用方法

方法 说明
.getCondition() 获取查询条件(只读)
.isEmpty() 检查是否为空条件
.isNothing() 检查是否为 nothing 匹配器
.clone() 克隆匹配器
.reset() 重置所有条件
.toString() 获取字符串表示

常用组合示例

// 基础移动系统
Matcher.all(Position, Velocity)

// 可攻击的活着的实体
Matcher.all(Position, Health)
    .any(Weapon, Magic)
    .none(Dead, Disabled)

// 所有带标签的敌人
Matcher.byTag(Tags.ENEMY)
    .all(AIComponent)

// 只需要生命周期的系统
Matcher.nothing()

相关 API