Bevy ECS 事件时序问题

Bevy ECS 事件系统的时序语义与常见直觉存在系统性偏差,理解这些偏差对避免运行时 bug 至关重要。

核心问题:双缓冲队列的帧延迟

传统 EventWriter<T> / EventReader<T> 采用双缓冲队列。这意味着:

  • 事件至少保留两帧——生产者写入的事件不会立即被同一帧的消费者看到
  • 天然延迟:消费者通常在下帧(甚至隔帧)才读到事件
  • 同帧竞争:若生产者和消费者在同一 Update 调度中并行运行,消费者是否读到取决于系统执行顺序,结果非确定性

run_if 陷阱

给 event reader 所在的系统附加 run_if 条件时,事件可能在条件未满足的那帧被静默丢弃:

// 危险:当 run_if 不满足时,该帧事件丢失
my_system
    .run_if(some_condition)
    .after(event_producer)

因为 EventReader 消费的是上一帧的缓冲区,如果当前帧系统未运行,上一帧的事件就错过了消费窗口。

0.17 的概念澄清:Message vs Event

Bevy 0.17 将原有的 Event 拆分为两个概念:

概念机制时序特性
Message缓冲队列(MessageReader延迟消费,至少一帧缓冲
EventObserver 触发同帧即时响应,但引入递归风险

这一拆分让开发者可以显式选择语义:需要缓冲时用 Message,需要即时响应时用 Event + Observer。

Observer 递归风险

Observer 模式在同帧即时响应方面优于缓冲队列,但引入了递归栈溢出风险。Chris Biscardi 的视频专门讨论了这一陷阱:Observer 处理中又触发新 Observer,可能导致无限递归或深层调用栈溢出。

应对策略

  1. 显式排序:用 .before() / .after() 确保生产者先于消费者
  2. 避免 run_if 覆盖 EventReader:把条件判断移到系统内部,而非系统调度条件
  3. 使用 Message 接受延迟:如果业务逻辑允许一帧延迟,主动用 Message 语义
  4. Observer 防递归:在 Observer 回调中设置标志位或深度限制

相关页面