diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index 7e264b04..f7eb604d 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -66,6 +66,7 @@ export default defineConfig({ items: [ { text: '实体类 (Entity)', link: '/guide/entity' }, { text: '组件系统 (Component)', link: '/guide/component' }, + { text: '实体查询系统', link: '/guide/entity-query' }, { text: '系统架构 (System)', link: '/guide/system', diff --git a/docs/guide/entity-query.md b/docs/guide/entity-query.md new file mode 100644 index 00000000..020f6907 --- /dev/null +++ b/docs/guide/entity-query.md @@ -0,0 +1,501 @@ +# 实体查询系统 + +实体查询是 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 + +- [Matcher](../api/classes/Matcher.md) - 查询条件描述符 API 参考 +- [QuerySystem](../api/classes/QuerySystem.md) - 查询系统 API 参考 +- [EntitySystem](../api/classes/EntitySystem.md) - 实体系统 API 参考 +- [Entity](../api/classes/Entity.md) - 实体 API 参考 diff --git a/packages/core/src/ECS/Decorators/TypeDecorators.ts b/packages/core/src/ECS/Decorators/TypeDecorators.ts index 9f11991e..285e37fa 100644 --- a/packages/core/src/ECS/Decorators/TypeDecorators.ts +++ b/packages/core/src/ECS/Decorators/TypeDecorators.ts @@ -75,7 +75,7 @@ export interface SystemMetadata { * @ECSSystem('Physics', { updateOrder: 10 }) * class PhysicsSystem extends EntitySystem { * constructor(@Inject(CollisionSystem) private collision: CollisionSystem) { - * super(Matcher.of(Transform, RigidBody)); + * super(Matcher.empty().all(Transform, RigidBody)); * } * } * ``` diff --git a/packages/core/src/ECS/Scene.ts b/packages/core/src/ECS/Scene.ts index 8c51d4b8..d0681889 100644 --- a/packages/core/src/ECS/Scene.ts +++ b/packages/core/src/ECS/Scene.ts @@ -527,7 +527,7 @@ export class Scene implements IScene { * @Injectable() * class PhysicsSystem extends EntitySystem { * constructor(@Inject(CollisionSystem) private collision: CollisionSystem) { - * super(Matcher.of(Transform)); + * super(Matcher.empty().all(Transform)); * } * } * scene.addEntityProcessor(PhysicsSystem); @@ -613,7 +613,7 @@ export class Scene implements IScene { * @Injectable() * @ECSSystem('Collision', { updateOrder: 5 }) * class CollisionSystem extends EntitySystem implements IService { - * constructor() { super(Matcher.of(Collider)); } + * constructor() { super(Matcher.empty().all(Collider)); } * dispose() {} * } * @@ -621,7 +621,7 @@ export class Scene implements IScene { * @ECSSystem('Physics', { updateOrder: 10 }) * class PhysicsSystem extends EntitySystem implements IService { * constructor(@Inject(CollisionSystem) private collision: CollisionSystem) { - * super(Matcher.of(Transform, RigidBody)); + * super(Matcher.empty().all(Transform, RigidBody)); * } * dispose() {} * } diff --git a/packages/core/src/ECS/Systems/EntitySystem.ts b/packages/core/src/ECS/Systems/EntitySystem.ts index 96dd35cf..569fc3ac 100644 --- a/packages/core/src/ECS/Systems/EntitySystem.ts +++ b/packages/core/src/ECS/Systems/EntitySystem.ts @@ -36,7 +36,7 @@ interface EventListenerRecord { * // 传统方式 * class MovementSystem extends EntitySystem { * constructor() { - * super(Matcher.of(Transform, Velocity)); + * super(Matcher.empty().all(Transform, Velocity)); * } * * protected process(entities: readonly Entity[]): void { @@ -51,7 +51,7 @@ interface EventListenerRecord { * // 类型安全方式 * class MovementSystem extends EntitySystem<[typeof Transform, typeof Velocity]> { * constructor() { - * super(Matcher.of(Transform, Velocity)); + * super(Matcher.empty().all(Transform, Velocity)); * } * * protected process(entities: readonly Entity[]): void {