查询
查询让你遍历具有特定组件的实体,是系统访问场景中实体数据的主要方式。
基本查询
查询匹配场景中所有拥有全部列出组件的实体:
import { defineSystem, addSystem, Query, Mut, Transform, Sprite } from 'esengine';
addSystem(defineSystem([Query(Mut(Transform), Sprite)], (query) => { for (const [entity, transform, sprite] of query) { transform.position.x += 1; }}));for...of 循环按查询参数的顺序返回 [entity, ...components] 元组。
可变查询
默认情况下,查询结果是只读的。使用 Mut() 获取特定组件的可变访问:
import { Query, Mut, Transform, Sprite } from 'esengine';
defineSystem( [Query(Mut(Transform), Sprite)], (query) => { for (const [entity, transform, sprite] of query) { transform.position.x += 1; // 可写 // sprite 是只读的 } });迭代方式
for…of
for (const [entity, transform, sprite] of query) { transform.position.x += 1;}forEach
query.forEach((entity, transform, sprite) => { transform.position.x += 1;});single
获取第一个匹配的实体。如果没有匹配的实体则返回 null。
const result = query.single();if (result) { const [entity, transform, sprite] = result; // ...}工具方法
query.isEmpty(); // 没有实体匹配时返回 truequery.count(); // 匹配的实体数量query.toArray(); // 将所有结果收集到数组中查询过滤器
使用 .with() 和 .without() 按组件的有无过滤实体,且不将这些组件包含在结果元组中:
import { Query, Transform, Sprite, defineTag } from 'esengine';
const Hidden = defineTag('Hidden');const Disabled = defineTag('Disabled');
// 匹配有 Transform 和 Sprite 的实体,但只要同时有 Player 的defineSystem( [Query(Mut(Transform), Sprite).with(Player)], (query) => { for (const [entity, transform, sprite] of query) { // 保证有 Player,但不在元组中 } });
// 匹配有 Transform 的实体,排除有 Hidden 的defineSystem( [Query(Transform).without(Hidden)], (query) => { for (const [entity, transform] of query) { // 只有没有 Hidden 组件的实体 } });可以链式调用多个 .with() 和 .without():
Query(Mut(Transform), Sprite).with(Player).without(Hidden, Disabled)变更检测过滤器
用 Added()、Changed() 包裹组件,或使用 Removed() 来响应组件的生命周期事件。
Added
匹配自上次系统运行以来新增了组件的实体:
import { Query, Added, Transform } from 'esengine';
defineSystem([Query(Added(Transform))], (query) => { for (const [entity, transform] of query) { console.log('New transform on', entity); }});Changed
匹配自上次 tick 以来组件数据被修改(通过 Mut())的实体:
import { Query, Changed, Mut, Text } from 'esengine';
defineSystem([Query(Changed(Text))], (query) => { for (const [entity, text] of query) { console.log('Text changed on', entity, text.content); }});Removed
Removed() 是独立参数(不在 Query 内部)。它返回自上次 tick 以来被移除了指定组件的实体:
import { Removed } from 'esengine';import { Health } from './components';
defineSystem([Removed(Health)], (removed) => { for (const entity of removed) { console.log('Health removed from', entity); }});详见变更检测了解基于 tick 的追踪机制。
多个查询
一个系统可以有多个查询来交叉引用不同类型的实体:
import { defineTag } from 'esengine';
const Player = defineTag('Player');const Enemy = defineTag('Enemy');
defineSystem( [Query(Transform, Player), Query(Transform, Enemy)], (players, enemies) => { for (const [_, playerTransform] of players) { for (const [_, enemyTransform] of enemies) { // 检查玩家和每个敌人之间的距离 } } });与资源结合
查询经常与资源一起使用:
import { Res, Time, Input, Query, Mut, Transform } from 'esengine';
defineSystem( [Res(Time), Res(Input), Query(Mut(Transform), Player)], (time, input, query) => { for (const [entity, transform] of query) { if (input.isKeyDown('KeyD')) { transform.position.x += 200 * time.delta; } } });示例:碰撞检测
定义 Hitbox、Player 和 Enemy 组件,在场景编辑器中挂载到实体上,然后查询:
import { defineSystem, defineComponent, defineTag, addSystem, Query, Transform } from 'esengine';
const Hitbox = defineComponent('Hitbox', { width: 50, height: 50 });const Player = defineTag('Player');const Enemy = defineTag('Enemy');
addSystem(defineSystem( [Query(Transform, Hitbox, Player), Query(Transform, Hitbox, Enemy)], (players, enemies) => { for (const [_, pPos, pBox] of players) { for (const [_, ePos, eBox] of enemies) { const dx = Math.abs(pPos.position.x - ePos.position.x); const dy = Math.abs(pPos.position.y - ePos.position.y);
if (dx < (pBox.width + eBox.width) / 2 && dy < (pBox.height + eBox.height) / 2) { // 碰撞! } } } }));迭代安全
在查询迭代过程中,不能直接创建或销毁实体,也不能插入或移除组件。这些操作会改变底层数据结构,导致迭代器失效。引擎会在迭代期间检测到这类操作并抛出错误,防止数据损坏。
正确做法是使用 Commands 将修改延迟到迭代结束后执行:
import { defineSystem, addSystem, Commands, Query, defineComponent } from 'esengine';
const Health = defineComponent('Health', { value: 100 });
const despawnDead = defineSystem( [Commands(), Query(Health)], (commands, query) => { for (const [entity, health] of query) { if (health.value <= 0) { commands.despawn(entity); // 延迟执行,安全 } } });Commands 的操作会在当前系统函数返回后统一刷新(flush),此时迭代已结束,修改不会影响正在进行的遍历。