首页
/ Bevy 0.20 Observer 事件匹配迁移指南:`B: Bundle` 泛型从 `On<E, B>` 移入事件类型

Bevy 0.20 Observer 事件匹配迁移指南:`B: Bundle` 泛型从 `On<E, B>` 移入事件类型

2026-09-07 16:55:32作者:滕妙奇

本文基于 Bevy 官方迁移文档 observer_event_matching.md(对应变更来自 PR #24013)展开,讲解 Bevy 0.19 到 0.20 中 Observer 系统的一项重要 API 变更:On<E, B> 中的 B: Bundle 类型参数被移除,组件匹配职责下放给事件类型本身。读完后,你将能够把旧版的生命周期观察者(Add/Insert/Discard/Remove/Despawn)与自定义事件观察者全部迁移到 0.20 的新签名,并理解 EventPattern 特性如何支撑这一设计。

变更概述:一个类型参数变成了两个层面的匹配

在 Bevy 0.19 中,Observer 系统参数 On 接受两个泛型参数:E 是事件类型,B: Bundle 是要匹配的组件包:

// Bevy 0.19
world.add_observer(|on: On<Add, A>| {
    // ...
});

在 Bevy 0.20 中,B: BundleOn 上移除,生命周期事件(Add/Insert/Discard/Remove/Despawn)自身携带了这个泛型参数:

// Bevy 0.20
world.add_observer(|on: On<Add<A>>| {
    // ...
});

从源码结构看,On 现在只约束一个泛型参数,且该参数必须实现 EventPattern 特性,定义见 system_param.rs

pub struct On<'w, 't, E: EventPattern> {
    observer: Entity,
    event: &'w mut E::Event,
    trigger: &'w mut EventPatternTrigger<'t, E>,
    trigger_context: &'w TriggerContext,
}

注意这里 event 字段的类型是 &mut E::Event 而非 &mut E——E 是“事件模式(EventPattern)”,真正被触发的事件数据是其关联类型 E::Event。这是本次重构的枢纽。

核心机制:EventPattern 特性的两个关联类型

EventPattern 特性定义在 event/mod.rs,它把“要监听的事件”和“要匹配的组件”统一成两个关联类型:

pub trait EventPattern: Send + Sync + 'static {
    /// 被观察的事件类型。
    type Event: Event;

    /// 用于匹配此事件的组件,供 EntityComponentsTrigger
    /// 判断该为哪些实体运行观察者。
    type Components: Bundle;
}

配套有一个关键的全局 blanket impl(同文件第 130–133 行):

// 所有事件隐式都是 EventPattern,且不带额外组件过滤
impl<E: Event> EventPattern for E {
    type Event = Self;
    type Components = ();
}

这解释了两件事:

  1. 普通事件无需改动。任何 #[derive(Event)] / #[derive(EntityEvent)] 类型都会通过 blanket impl 隐式成为 EventPattern,其 Components 为空 bundle,行为与 0.19 中不指定 B 的写法一致。
  2. 组件匹配从“观察者的参数”变成“模式的一部分”EntityComponentsTrigger(Bevy 组件生命周期事件所用的触发器)通过 E::Components 决定观察者为哪些实体运行,相关机制见 trigger.rs 中对 Trigger 的说明。

生命周期事件迁移:Add 变成 Add<A>

Bevy 内置的五个生命周期事件如今都是“带 B: Bundle 泛型的模式类型”,其内部通过 PhantomData<B> 持有类型信息。以 Add 为例,源码见 lifecycle.rs

/// AddEvent 的触发数据
#[derive(Debug, Clone, EntityEvent)]
#[entity_event(trigger = EntityComponentsTrigger<'a>)]
pub struct AddEvent {
    /// 组件被添加到的实体。
    pub entity: Entity,
}

/// 针对给定组件 bundle 的 AddEvent 的 EventPattern
#[doc(alias = "OnAdd")]
pub struct Add<B: Bundle>(PhantomData<B>);

impl<B: Bundle> EventPattern for Add<B> {
    type Event = AddEvent;
    type Components = B;
}

InsertDiscardRemoveDespawnlifecycle.rs 中遵循完全相同的模式:结构体本身带 PhantomData<B> 并实现 EventPattern,而真正承载数据的 InsertEventDiscardEventRemoveEventDespawnEvent 各自只包含 entity 字段。

迁移对照

Bevy 0.19 写法 Bevy 0.20 写法
On<Add, A> On<Add<A>>
On<Insert, (A, B)> On<Insert<(A, B)>>
On<Remove, A> On<Remove<A>>
On<Despawn, A> On<Despawn<A>>

一个容易踩坑的细节:bundle 中多个组件是 OR 关系而非 AND 关系。源码文档明确说明(lifecycle.rs):“Add<(A, B)> 会在实体上添加 AB 中任意一个组件时触发。”如果你需要 AND 语义,请分别注册两个观察者。

自定义事件迁移:拆分事件与模式两种类型

对于 0.19 中借助 On<Foo, Bar> 这种“事件 + 组件包”组合的自定义事件类型,官方推荐的做法是把事件类型和模式类型拆开:事件类型保留数据与 Event 派生,模式类型用 PhantomData<B> 承载组件约束并手动实现 EventPattern

// Bevy 0.19

#[derive(Event)]
pub struct Foo;

#[derive(Component)]
pub struct Bar;

world.add_observer(|on: On<Foo, Bar>| {
    // ...
});

// Bevy 0.20

#[derive(Event)]
pub struct FooEvent;

#[derive(Component)]
pub struct Bar;

pub struct Foo<B: Bundle>(PhantomData<B>);

impl<B: Bundle> EventPattern for Foo<B> {
    type Event = FooEvent;
    type Components = B;
}

world.add_observer(|on: On<Foo<Bar>>| {
    // ...
});

要点说明:

  • FooEvent 是真正被 world.trigger(...) 触发、携带数据的类型,必须实现 Event(通常通过 #[derive(Event)])。
  • Foo<B> 只是“匹配模板”,运行时不存储任何数据,PhantomData<B> 保证 B 参与单态化与类型检查。
  • type Components = B 决定了触发时组件匹配的过滤条件;若你的自定义事件不需要组件过滤,直接 #[derive(Event)] 后让 blanket impl 以 Components = () 生效即可,无需手写模式类型。
  • 这种拆分让“同一事件、不同组件匹配”成为可能(例如 On<Foo<Bar>>On<Foo<Baz>> 是相互独立的观察目标),这正是把泛型下沉到事件类型的收益。

动态组件观察者:On<Add> 需改为 On<Add<()>>

当 Observer 以“动态 ComponentId”方式监听生命周期事件时(即通过 Observer::with_component(component_id) 指定要观察的组件,而不写具体组件类型),迁移规则稍有不同:0.19 的裸 On<Add> 在 0.20 中必须写成 On<Add<()>>,用空 bundle 表示“组件由观察者实体上的动态配置决定”:

// Bevy 0.19
world.spawn(
    Observer::new(|_: On<Add>| {
        // ...
    })
    .with_component(component_id),
);

// Bevy 0.20
world.spawn(
    Observer::new(|_: On<Add<()>>| {
        // ...
    })
    .with_component(component_id),
);

仓库中的测试代码印证了这一用法,见 observer/mod.rs

Observer::new(|_: On<Add<()>>, mut res: ResMut<Order>| res.observed("event_a"))

需要注意 () 在此并非“匹配所有组件”,而是空 Bundle 的占位——真正的组件过滤仍由 .with_component(component_id) 提供的动态 ComponentId 完成。

底层原理速览:模式、事件与触发器如何协作

把本次变更放回整个事件系统看,数据流大致是:

  1. World::trigger(event) 被调用,事件的 Event::Trigger 决定匹配逻辑(GlobalTriggerEntityTriggerEntityComponentsTriggerPropagateEntityTrigger,见 trigger.rs)。
  2. EntityComponentsTrigger 读取 EventPattern::Components(即 Add<A> 中的 A),筛出携带这些组件的实体(OR 语义),为它们运行对应观察者。
  3. 观察者闭包收到的 On<E> 参数中,event 字段类型为 &mut E::Event(如 &mut AddEvent),因此迁移后你在闭包里访问 event.entity 的写法保持不变。

此外,生命周期事件在 World 初始化时会注册固定的 EventKeyADD/INSERT/DISCARD/REMOVE/DESPAWN,见 lifecycle.rs),用于热路径上跳过 TypeId 查找——这部分机制不受本次 API 变更影响。

迁移检查清单

按以下清单逐项排查代码库即可完成迁移:

  1. 全局搜索 On<Add, On<Insert, On<Discard, On<Remove, On<Despawn, :把第二个参数移入第一个参数内部,如 On<Add, A>On<Add<A>>
  2. 裸生命周期观察者(配合 with_component 的动态监听):On<Add>On<Add<()>>
  3. 自定义事件 + bundle 组合:拆分为“事件类型(derive Event)+ 模式类型(PhantomData<B> + 手写 EventPattern)”,并将 On<Foo, Bar> 改为 On<Foo<Bar>>
  4. 纯事件观察者(如 On<Speak>):无需改动,blanket impl 已保证兼容。
  5. 迁移后确认 bundle 多组件的 OR 语义符合预期;如需 AND 语义请拆分注册。

迁移完成后,所有观察者闭包内部对事件数据的访问方式(event.entityevent.message 等)保持不变,行为上与 0.19 等价——本次变更只影响类型签名的位置,不改变事件的触发时机与执行顺序。

登录后查看全文
热门项目推荐
相关项目推荐