事件
事件让系统之间无需互相知晓即可通信。一个系统发送事件,其他任意数量的系统都可以读取——系统之间没有直接耦合。
定义事件
使用 defineEvent 和 TypeScript 接口定义事件:
import { defineEvent } from 'esengine';
interface DamageEvent { target: Entity; amount: number; source?: Entity;}
const Damage = defineEvent<DamageEvent>('Damage');发送事件
使用 EventWriter 作为系统参数来发送事件:
import { defineSystem, addSystem, EventWriter, Query, defineComponent, defineTag } from 'esengine';
const Health = defineComponent('Health', { current: 100, max: 100 });const Enemy = defineTag('Enemy');
addSystem(defineSystem( [EventWriter(Damage), Query(Health, Enemy)], (writer, query) => { for (const [entity, health] of query) { if (health.current <= 0) { writer.send({ target: entity, amount: 0 }); } } }));在一个系统中可以多次调用 writer.send()——所有事件都会被收集。
读取事件
使用 EventReader 作为系统参数来接收事件:
import { defineSystem, addSystem, EventReader, Commands } from 'esengine';
addSystem(defineSystem( [EventReader(Damage)], (events) => { for (const event of events) { console.log(`Entity ${event.target} took ${event.amount} damage`); } }));EventReader 是可迭代的——使用 for...of 处理每个事件。它还提供工具方法:
events.isEmpty() // 本帧是否没有事件events.toArray() // 将所有事件收集为数组多个系统可以读取同一事件类型,每个 reader 都能看到上一帧的所有事件。
事件生命周期
事件使用双缓冲设计:
- 帧开始:缓冲区交换——上一帧写入的事件变为可读,旧的读取缓冲区被清空
- 帧内:writer 向写缓冲区推送,reader 从读缓冲区读取
- 下一帧:再次交换
帧 1: systemA 发送 Damage ──────────────────┐帧 2: systemB 读取 Damage(来自帧 1)◄───────┘ systemA 发送新 Damage ────────────────┐帧 3: systemB 读取 Damage(来自帧 2)◄──────┘ 帧 1 的事件已消失实战示例:伤害系统
一个完整的示例,包含三个解耦的系统:
import { defineEvent, defineSystem, addSystem, EventWriter, EventReader, GetWorld, Query, Mut, Commands, Sprite, defineComponent, defineTag } from 'esengine';
const Health = defineComponent('Health', { current: 100, max: 100 });const Projectile = defineComponent('Projectile', { radius: 5, power: 10 });const Enemy = defineTag('Enemy');
// 1. 定义事件interface DamageEvent { target: Entity; amount: number;}const Damage = defineEvent<DamageEvent>('Damage');
// 2. 战斗系统——检测命中并发送伤害事件const combatSystem = defineSystem( [EventWriter(Damage), Query(Transform, Projectile), Query(Transform, Health, Enemy)], (damage, projectiles, enemies) => { for (const [_, pTransform, proj] of projectiles) { for (const [enemy, eTransform] of enemies) { if (distance(pTransform, eTransform) < proj.radius) { damage.send({ target: enemy, amount: proj.power }); } } } });
// 3. 生命系统——应用伤害const healthSystem = defineSystem( [EventReader(Damage), GetWorld()], (events, world) => { for (const { target, amount } of events) { const health = world.tryGet(target, Health); if (health) { health.current -= amount; world.set(target, Health, health); } } });
// 4. 特效系统——显示命中效果(读取相同事件)const hitVFXSystem = defineSystem( [EventReader(Damage), GetWorld()], (events, world) => { for (const { target } of events) { const transform = world.tryGet(target, Transform); if (transform) { spawnHitParticle(transform.position); } } });三个系统,一种事件类型,彼此之间零直接依赖。
多种事件类型
一个系统可以写入和读取多种事件类型:
const PlayerDied = defineEvent<{ entity: Entity }>('PlayerDied');const ScoreChanged = defineEvent<{ delta: number }>('ScoreChanged');
defineSystem( [EventReader(Damage), EventWriter(PlayerDied), EventWriter(ScoreChanged)], (damageEvents, deathWriter, scoreWriter) => { for (const { target, amount } of damageEvents) { if (isPlayer(target) && getHealth(target) <= 0) { deathWriter.send({ entity: target }); scoreWriter.send({ delta: -100 }); } } });何时使用事件 vs 直接查询
| 使用事件 | 使用查询 |
|---|---|
| 一次性通知(命中、死亡、得分) | 持续状态(位置、生命值) |
| 多个消费者需要相同数据 | 单个系统拥有逻辑 |
| 跨模块的解耦系统 | 紧密相关的系统 |
| 时机很重要(这一帧发生的) | 当前值更重要 |