跳转到内容

系统

系统是每帧运行的函数。你声明它需要什么——一个组件查询,加上资源和事件—— Estella 就会在调度中用恰好这些参数调用它。

import {
defineComponent, defineSystem, addSystem,
Query, Mut, Res, Time, Transform
} from 'esengine';
const Speed = defineComponent('Speed', { value: 200 });
addSystem(defineSystem(
[Res(Time), Query(Mut(Transform), Speed)],
(time, query) => {
for (const [entity, transform, speed] of query) {
transform.position.x += speed.value * time.delta;
}
}
));

系统的第一个参数是它的参数列表——它将按顺序收到的数据:

  • Query(...) —— 遍历同时拥有列出的每个组件的实体。每次迭代产出 [entity, ...组件]
  • Mut(Component) —— 系统会写入的组件。未包裹的组件是只读的。
  • Res(Resource) / ResMut(Resource) —— 一个共享资源,只读或可写(例如提供帧计时的 Time)。
  • Commands() —— 延迟的结构性改动(生成 / 销毁 / 增删组件),在下次 flush 时应用。
  • EventReader(E) / EventWriter(E) —— 读取或发送一条类型化事件流。

系统活在由有序阶段组成的**调度(schedule)**里。用 addSystem 注册进默认的 Update 阶段,用 addStartupSystem 注册进 Startup(只跑一次),或用 addSystemToSchedule(Schedule.X, system) 注册进任意阶段:

阶段 何时运行
Startup 首帧之前,一次。
FirstPreUpdateUpdatePostUpdateLast 每帧,按此顺序。
FixedPreUpdateFixedUpdateFixedPostUpdate 固定步长(物理 / 复制节拍)——每帧 0..n 次。

同一阶段内,系统按注册顺序运行。要约束顺序,在 App 上带排序选项注册——它们按系统 名字引用,所以给要排序的系统起名:

import { Schedule } from 'esengine';
const move = defineSystem([...], moveFn, { name: 'move' });
const camera = defineSystem([...], cameraFn, { name: 'camera' });
app.addSystemToSchedule(Schedule.Update, move);
app.addSystemToSchedule(Schedule.Update, camera, {
runAfter: ['move'], // 相机跟在移动之后
runIf: () => isPlaying, // 条件为 false 时跳过该系统
});

场景系统只在其所属场景处于激活状态时运行,因此多个场景可以共存于同一个世界而互不干扰。

查询可以在“拥有这些组件”之外进一步收窄,并对变更做出反应:

// 额外的存在性过滤——要求/排除但不读取的组件:
Query(Transform, Mut(Velocity)).without(Frozen).with(Enemy)
// 用 filter() 做布尔组合:
import { Query, With, Without, Or, Not } from 'esengine';
Query(Transform).filter(Or(With(Player), With(Ally)))
// 变更检测——只要该组件自本系统上次运行以来被添加/写过的实体:
import { Query, Added, Changed } from 'esengine';
Query(Added(Health)) // 本帧新增
Query(Changed(Transform)) // 上次运行后被写过

Removed(C) 是它自己的查询,产出丢失C 的实体:

import { defineSystem, Removed } from 'esengine';
defineSystem([Removed(Health)], (removed) => { // Health 是你自己的组件
for (const entity of removed) { /* 清理 */ }
});

查询结果在 for…of 之外还有便捷方法:

方法 返回
query.single() 唯一的匹配(否则 null)——用于单独的玩家/相机。
query.count() / query.isEmpty() 匹配数 / 是否为空。
query.toArray() 所有结果组成数组。
query.forEach((e, ...c) => …) 每个匹配调用一次回调。

事件是一条类型化、解耦的消息流——一个系统 send,另一个下一帧读取,两者之间无直接引用。 定义一个事件,在 app 上注册,然后把 EventWriter / EventReader 作为系统参数:

import { defineEvent, defineSystem, EventWriter, EventReader, Commands } from 'esengine';
const Damaged = defineEvent<{ entity: number; amount: number }>('Damaged');
app.addEvent(Damaged);
// 生产者:
addSystem(defineSystem([EventWriter(Damaged)], (damaged) => {
damaged.send({ entity: 12, amount: 5 });
}));
// 消费者(读取自上次运行以来发送的):
addSystem(defineSystem([EventReader(Damaged), Commands()], (damaged, cmds) => {
for (const evt of damaged) { /* evt.entity、evt.amount */ }
}));

reader 可迭代,还提供 .isEmpty() / .toArray()。完整的生产者/消费者模式见 event-system 示例。