Bevy 0.20 Observer 事件匹配迁移指南:`B: Bundle` 泛型从 `On<E, B>` 移入事件类型
本文基于 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: Bundle 从 On 上移除,生命周期事件(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 = ();
}
这解释了两件事:
- 普通事件无需改动。任何
#[derive(Event)]/#[derive(EntityEvent)]类型都会通过 blanket impl 隐式成为EventPattern,其Components为空 bundle,行为与 0.19 中不指定B的写法一致。 - 组件匹配从“观察者的参数”变成“模式的一部分”。
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;
}
Insert、Discard、Remove、Despawn 在 lifecycle.rs 中遵循完全相同的模式:结构体本身带 PhantomData<B> 并实现 EventPattern,而真正承载数据的 InsertEvent、DiscardEvent、RemoveEvent、DespawnEvent 各自只包含 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)> 会在实体上添加 A 或 B 中任意一个组件时触发。”如果你需要 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 完成。
底层原理速览:模式、事件与触发器如何协作
把本次变更放回整个事件系统看,数据流大致是:
World::trigger(event)被调用,事件的Event::Trigger决定匹配逻辑(GlobalTrigger、EntityTrigger、EntityComponentsTrigger、PropagateEntityTrigger,见 trigger.rs)。EntityComponentsTrigger读取EventPattern::Components(即Add<A>中的A),筛出携带这些组件的实体(OR 语义),为它们运行对应观察者。- 观察者闭包收到的
On<E>参数中,event字段类型为&mut E::Event(如&mut AddEvent),因此迁移后你在闭包里访问event.entity的写法保持不变。
此外,生命周期事件在 World 初始化时会注册固定的 EventKey(ADD/INSERT/DISCARD/REMOVE/DESPAWN,见 lifecycle.rs),用于热路径上跳过 TypeId 查找——这部分机制不受本次 API 变更影响。
迁移检查清单
按以下清单逐项排查代码库即可完成迁移:
- 全局搜索
On<Add,、On<Insert,、On<Discard,、On<Remove,、On<Despawn,:把第二个参数移入第一个参数内部,如On<Add, A>→On<Add<A>>。 - 裸生命周期观察者(配合
with_component的动态监听):On<Add>→On<Add<()>>。 - 自定义事件 + bundle 组合:拆分为“事件类型(derive Event)+ 模式类型(
PhantomData<B>+ 手写EventPattern)”,并将On<Foo, Bar>改为On<Foo<Bar>>。 - 纯事件观察者(如
On<Speak>):无需改动,blanket impl 已保证兼容。 - 迁移后确认 bundle 多组件的 OR 语义符合预期;如需 AND 语义请拆分注册。
迁移完成后,所有观察者闭包内部对事件数据的访问方式(event.entity、event.message 等)保持不变,行为上与 0.19 等价——本次变更只影响类型签名的位置,不改变事件的触发时机与执行顺序。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00