系统
系统包含游戏逻辑。它们通过参数声明需要的数据,Estella 自动注入。系统自动操作场景中所有匹配的实体。
定义系统
import { defineSystem, Res, Time, Query, Mut, Transform, Velocity } from 'esengine';
const movementSystem = defineSystem( [Res(Time), Query(Mut(Transform), Velocity)], (time, query) => { for (const [entity, transform, velocity] of query) { transform.position.x += velocity.linear.x * time.delta; transform.position.y += velocity.linear.y * time.delta; } });此系统自动处理场景中所有同时拥有 Transform 和 Velocity 组件的实体。
defineSystem 接受两个参数:
- 参数列表 —
Query、Res、ResMut、Commands、GetWorld、EventWriter或EventReader描述符的数组 - 函数 — 按相同顺序接收解析后的参数
注册系统
使用顶层函数注册系统:
import { addSystem, addStartupSystem, addSystemToSchedule, Schedule } from 'esengine';
addStartupSystem(setupSystem); // Schedule.StartupaddSystem(movementSystem); // Schedule.UpdateaddSystemToSchedule(Schedule.FixedUpdate, physicsSystem);Schedule 类型
| Schedule | 运行时机 |
|---|---|
Startup | 启动时运行一次 |
First | 每帧最先运行 |
PreUpdate | 每帧 Update 之前 |
Update | 每帧运行(主要游戏逻辑) |
PostUpdate | 每帧 Update 之后 |
Last | 每帧最后运行 |
FixedPreUpdate | 固定间隔,FixedUpdate 之前 |
FixedUpdate | 固定间隔(物理) |
FixedPostUpdate | 固定间隔,FixedUpdate 之后 |
系统排序
同一 Schedule 内的多个系统默认按注册顺序执行。如果系统之间有执行顺序依赖,可以通过 defineSystem 的第三个参数指定 runBefore 或 runAfter:
import { defineSystem, Query, Res, Time, Mut, Transform, Velocity, Sprite } from 'esengine';
const movementSystem = defineSystem( [Res(Time), Query(Mut(Transform), Velocity)], (time, query) => { /* ... */ }, { name: 'movement' });
const renderSystem = defineSystem( [Query(Transform, Sprite)], (query) => { /* ... */ }, { name: 'render', runAfter: ['movement'] });runAfter: ['movement']— 当前系统在movement之后执行runBefore: ['render']— 当前系统在render之前执行
引擎使用拓扑排序确定最终执行顺序。如果排序约束形成循环依赖,启动时会抛出错误。
系统参数
Commands
在运行时创建、修改和销毁实体:
import { Commands, Sprite, Transform, Velocity, defineResource } from 'esengine';
const Score = defineResource({ value: 0 });
defineSystem([Commands()], (cmds) => { // 创建新实体并添加组件(可链式调用) const bullet = cmds.spawn() .insert(Transform, { position: { x: 0, y: 0, z: 0 } }) .insert(Sprite, { size: { x: 8, y: 8 } }) .id();
// 修改已有实体 cmds.entity(bullet) .insert(Velocity, { linear: { x: 100, y: 0, z: 0 } }) .remove(Sprite);
// 销毁实体 cmds.despawn(bullet);
// 插入资源 cmds.insertResource(Score, { value: 0 });});Commands API
| 方法 | 返回值 | 说明 |
|---|---|---|
cmds.spawn() | EntityCommands | 创建新实体,返回构建器 |
cmds.entity(entity) | EntityCommands | 获取已有实体的构建器 |
cmds.despawn(entity) | this | 将实体加入销毁队列 |
cmds.insertResource(res, value) | this | 插入或覆盖资源 |
EntityCommands API
spawn() 和 entity() 返回 EntityCommands 构建器,所有方法可链式调用:
| 方法 | 返回值 | 说明 |
|---|---|---|
.insert(component, data?) | this | 添加或更新组件 |
.remove(component) | this | 移除组件 |
.id() | Entity | 获取实体 ID |
Query
遍历具有特定组件的实体:
import { Query, Mut, Transform, Sprite } from 'esengine';
defineSystem([Query(Mut(Transform), Sprite)], (query) => { for (const [entity, transform, sprite] of query) { transform.position.x += 1; }});详见查询。
Res(只读资源)
import { Res, Time } from 'esengine';
defineSystem([Res(Time)], (time) => { console.log(`Delta: ${time.delta}s`);});ResMut(可变资源)
import { ResMut } from 'esengine';
defineSystem([ResMut(GameState)], (state) => { state.get().score += 10;});详见资源。
EventWriter
从系统中发送事件:
import { defineEvent, EventWriter } from 'esengine';
const DamageEvent = defineEvent<{ target: number; amount: number }>('Damage');
defineSystem([EventWriter(DamageEvent)], (writer) => { writer.send({ target: enemy, amount: 25 });});EventReader
接收其他系统发送的事件:
import { EventReader } from 'esengine';
defineSystem([EventReader(DamageEvent)], (reader) => { for (const event of reader) { console.log(`${event.target} took ${event.amount} damage`); }});详见事件。
GetWorld
直接访问 ECS World。可用 world.get() / world.set() 按实体 ID 进行 O(1) 组件访问,或配合 findEntityByName() 等工具函数:
import { GetWorld, findEntityByName, ProgressBar } from 'esengine';
defineSystem([GetWorld()], (world) => { const entity = findEntityByName(world, 'HealthBar'); if (entity) { const bar = world.get(entity, ProgressBar); bar.value = playerHealth / maxHealth; world.set(entity, ProgressBar, bar); }});World API
| 方法 | 返回值 | 说明 |
|---|---|---|
world.get(entity, Component) | 组件数据 | 读取组件(O(1)) |
world.set(entity, Component, data) | void | 写入组件(O(1)) |
world.has(entity, Component) | boolean | 检查实体是否有该组件 |
world.tryGet(entity, Component) | data | null | 安全读取,不存在返回 null |
world.valid(entity) | boolean | 检查实体是否存活 |
world.setParent(child, parent) | void | 设置父子关系(子实体 Transform 相对于父实体) |
world.removeParent(entity) | void | 移除父节点,使实体成为根实体 |
实体层级
使用 setParent / removeParent 建立父子关系。子实体的 Transform 相对于父实体计算。
import { GetWorld, Commands, Transform, Sprite } from 'esengine';
defineSystem([Commands(), GetWorld()], (cmds, world) => { const parent = cmds.spawn() .insert(Transform, { position: { x: 100, y: 100, z: 0 } }) .insert(Sprite, {}) .id();
const child = cmds.spawn() .insert(Transform, { position: { x: 50, y: 0, z: 0 } }) .insert(Sprite, {}) .id();
world.setParent(child, parent);
// 之后可以解除父子关系: // world.removeParent(child);});组合参数
defineSystem( [Commands(), Res(Time), Res(Input), GetWorld(), Query(Mut(Transform), Velocity)], (cmds, time, input, world, query) => { // 所有参数都可用 });示例:玩家移动
定义 Speed 组件,在编辑器中挂载到玩家实体上,然后编写系统:
import { defineComponent } from 'esengine';export const Speed = defineComponent('Speed', { value: 200 });import { defineSystem, addSystem, Res, Time, Input, Query, Mut, Transform } from 'esengine';import { Speed } from '../components/Speed';
addSystem(defineSystem( [Res(Time), Res(Input), Query(Mut(Transform), Speed)], (time, input, query) => { for (const [entity, transform, speed] of query) { if (input.isKeyDown('KeyD')) { transform.position.x += speed.value * time.delta; } if (input.isKeyDown('KeyA')) { transform.position.x -= speed.value * time.delta; } } }));异步系统
defineSystem 支持异步函数。适用于需要加载资源的启动系统,或调用异步 API 的系统:
import { defineSystem, addStartupSystem, Res, Prefabs } from 'esengine';
addStartupSystem(defineSystem( [Res(Prefabs)], async (prefabs) => { const { root } = await prefabs.instantiate('prefabs/Enemy.esprefab'); console.log('Spawned enemy:', root); }));异步系统在所有调度阶段均可使用。无需特殊声明——直接传入 async 函数即可,引擎会自动处理。Commands 会在异步函数完成后自动刷新。
错误处理
每个系统在独立的 try/catch 边界内运行。如果系统抛出错误,会被捕获并输出到控制台,不会导致整个游戏循环崩溃。其他系统继续正常执行。