变更检测
变更检测让系统仅在组件被添加、修改或移除时才做出响应,而不是每帧处理所有实体。当场景中有大量实体但每帧只有少数发生变化时,这对性能至关重要。
Added
Added() 过滤查询,仅返回自系统上次运行以来新增了某组件的实体:
import { defineSystem, addSystem, Query, Added, Transform, Sprite } from 'esengine';
addSystem(defineSystem( [Query(Added(Sprite), Transform)], (query) => { for (const [entity, sprite, transform] of query) { // 仅对刚获得 Sprite 的实体执行 console.log('New sprite at', transform.position.x, transform.position.y); } }));实体在被创建或被插入组件的那一帧出现在 Added 查询中,之后不会再出现,除非组件被移除后重新添加。
Changed
Changed() 过滤查询,仅返回组件值被修改过的实体:
import { defineSystem, addSystem, Query, Changed, Mut, Sprite, defineComponent } from 'esengine';
const Health = defineComponent('Health', { current: 100, max: 100 });
const healthBarSystem = defineSystem( [Query(Changed(Health), Mut(Sprite))], (query) => { for (const [entity, health, sprite] of query) { // 仅在 Health 被修改时执行 sprite.size.x = (health.current / health.max) * 100; } });当 world.insert()、world.set() 或 Commands.insert() 被调用时,组件会被标记为已变更——即使新值与旧值相同。
Removed
Removed() 是独立的查询类型(不是 Query 的过滤器)。它返回被移除了特定组件的实体 ID:
import { defineSystem, addSystem, Removed, Commands, defineComponent } from 'esengine';
const Health = defineComponent('Health', { current: 100, max: 100 });
addSystem(defineSystem( [Removed(Health), Commands()], (removed, cmds) => { for (const entity of removed) { // Health 被移除了——销毁实体 cmds.despawn(entity); } }));移除记录在缓冲区中保留 2 个 tick,之后自动清理。这意味着每帧运行的系统总能及时看到移除事件。
组合过滤器
可以在同一个系统中混合使用 Added、Changed 和 Removed:
import { defineSystem, Query, Added, Changed, Removed, Mut, Transform, Velocity, Commands, defineComponent } from 'esengine';
const Health = defineComponent('Health', { current: 100, max: 100 });
defineSystem( [ Query(Added(Velocity), Transform), Query(Changed(Health), Mut(Sprite)), Removed(Health), Commands(), ], (newVelocities, damagedEntities, removedHealth, cmds) => { for (const [entity, velocity, transform] of newVelocities) { // 初始化移动 }
for (const [entity, health, sprite] of damagedEntities) { // 更新血条 }
for (const entity of removedHealth) { cmds.despawn(entity); } });变更过滤器也兼容 .with() 和 .without():
Query(Changed(Health)).with(Player) // 仅玩家的 Health 变更Query(Added(Sprite)).without(UIRect) // 新 Sprite 但非 UI 元素工作原理
Estella 使用基于 tick 的追踪:
- World 维护一个全局 tick 计数器,每帧递增一次
- 每次组件插入或修改都会记录当前 tick
- 每个系统记录其上次运行的 tick
- 系统运行时,变更过滤器比较:组件 tick > 系统上次运行 tick
这意味着:
- 每帧运行的系统会看到上一帧的变更
- 跳帧的系统(如通过
wrapSceneSystem)会看到自上次运行以来的所有累积变更 - 无需手动管理 “dirty flag”——完全自动
常用模式
同步 Transform 到渲染位置
const syncRenderSystem = defineSystem( [Query(Changed(Transform), Sprite)], (query) => { for (const [entity, transform, sprite] of query) { updateRenderPosition(entity, transform.position); } });响应实体生成
const onSpawnSystem = defineSystem( [Query(Added(Transform), Sprite)], (query) => { for (const [entity, transform, sprite] of query) { playSpawnAnimation(entity); } });组件移除时清理
const cleanupSystem = defineSystem( [Removed(RigidBody)], (removed) => { for (const entity of removed) { destroyPhysicsBody(entity); } });