Skip to content

实体查询系统

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

核心概念

Matcher - 查询条件描述符

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

QuerySystem - 查询执行引擎

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

在 EntitySystem 中使用 Matcher

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

基础用法

typescript
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() - 必须包含所有组件

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

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

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

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

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

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

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

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

组合条件

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

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

按标签查询

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

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

按名称查询

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

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

直接使用 QuerySystem

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

基础查询方法

typescript
// 获取场景的查询系统
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} 个活着的实体`);

按标签查询

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

按名称查询

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

按单个组件查询

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

性能优化

自动缓存

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

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

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

实体变化自动更新

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

typescript
// 查询拥有武器的实体
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

查询性能统计

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

实际应用场景

场景1: 物理系统

typescript
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: 渲染系统

typescript
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: 碰撞检测

typescript
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: 一次性查询

typescript
// 在系统外部执行一次性查询
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

typescript
// 推荐: 使用 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() 排除条件

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

3. 使用标签优化查询

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

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

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

typescript
// 不推荐: 过于复杂
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. 查询结果是只读的

typescript
const result = querySystem.queryAll(PositionComponent);

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

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

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

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

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

3. Matcher 是不可变的

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

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

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

相关 API

Released under the MIT License.