跳转到内容

变更检测

变更检测让系统仅在组件被添加、修改或移除时才做出响应,而不是每帧处理所有实体。当场景中有大量实体但每帧只有少数发生变化时,这对性能至关重要。

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,之后自动清理。这意味着每帧运行的系统总能及时看到移除事件。

组合过滤器

可以在同一个系统中混合使用 AddedChangedRemoved

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 的追踪

  1. World 维护一个全局 tick 计数器,每帧递增一次
  2. 每次组件插入或修改都会记录当前 tick
  3. 每个系统记录其上次运行的 tick
  4. 系统运行时,变更过滤器比较:组件 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);
}
}
);

下一步

  • 事件 — 在系统之间发送和接收消息
  • 查询 — 基本查询过滤和迭代
  • 系统 — 系统定义和调度