跳转到内容

事件

事件让系统之间无需互相知晓即可通信。一个系统发送事件,其他任意数量的系统都可以读取——系统之间没有直接耦合。

定义事件

使用 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 都能看到上一帧的所有事件。

事件生命周期

事件使用双缓冲设计:

  1. 帧开始:缓冲区交换——上一帧写入的事件变为可读,旧的读取缓冲区被清空
  2. 帧内:writer 向写缓冲区推送,reader 从读缓冲区读取
  3. 下一帧:再次交换
帧 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 直接查询

使用事件使用查询
一次性通知(命中、死亡、得分)持续状态(位置、生命值)
多个消费者需要相同数据单个系统拥有逻辑
跨模块的解耦系统紧密相关的系统
时机很重要(这一帧发生的)当前值更重要

下一步