Files
esengine/docs/guide/entity-query.md
yhh 240b165970 chore: 更新仓库 URL (ecs-framework → esengine)
仓库已从 esengine/ecs-framework 重命名为 esengine/esengine
更新所有引用旧 URL 的文件
2025-12-08 21:23:37 +08:00

16 KiB

实体查询系统

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

核心概念

Matcher - 查询条件描述符

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

QuerySystem - 查询执行引擎

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

在 EntitySystem 中使用 Matcher

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

基础用法

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

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

最佳实践

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