Bevy 抽取系统泛化迁移指南:从 bevy_extract 到 AppLabel 的完整改造
本篇基于 Bevy 官方迁移文档 extract-extract 展开,讲解抽取(Extraction)系统从"仅限 Main World → Render World"升级为面向任意子应用(Sub App)泛化机制后的迁移方法。读完后你能掌握:如何在 SyncComponent / ExtractComponent / ExtractResource 上指定 AppLabel、如何用 #[extract_app] 宏把组件抽取到多个子应用,以及新 crate bevy_extract 中各 API 的迁移对照。
一、背景:抽取机制为什么要泛化
在旧版本中,抽取是"Main World 到 Render World 专属"的固定管线:渲染管线从主世界同步实体、提取组件数据到自己的子世界(sub world)。改造后,这套机制被抽象为通用的子应用间数据通道,任何带有 AppLabel 的子应用(RenderApp、AudioApp 或自定义 label)都可以作为抽取目标。
从 crates/bevy_extract/src/lib.rs 的模块文档可以看到新的设计目标:
- 通过为指定
AppLabel添加ExtractPlugin完成基础接入; - 派生
ExtractComponent或ExtractResource时,必须用extract_app属性指明目标子应用; - 派生
ExtractComponent会自动追加SyncComponent实现,先把主世界的实体同步到子世界,再把组件数据从主实体复制到子实体; - 子应用可以在
ExtractSchedule中通过Extract参数访问主世界。
对应的 crate 结构为 crates/bevy_extract/src:extract_component.rs(组件抽取)、extract_resource.rs(资源抽取)、extract_instances.rs(高性能实例化抽取)、extract_param.rs(Extract 系统参数)、extract_plugin.rs(插件与调度)、sync_component.rs(组件联动清理)、sync_world.rs(实体同步)。
二、核心迁移点:所有抽取 trait 必须携带 AppLabel
迁移文档列出的两条硬性规则:
- 使用
TemporaryRenderEntity::default()取代TemporaryRenderEntity构造; - 使用
SyncComponent、ExtractComponent、ExtractResource等抽取相关 trait 时,必须为它们指定目标世界的AppLabel。
2.1 前后写法对照
Before:
impl SyncComponent for TemporalAntiAliasing { ... }
#[derive(Component, ExtractComponent)]
pub struct Foo { ... }
After:
impl SyncComponent<RenderApp> for TemporalAntiAliasing { ... }
#[derive(Component, ExtractComponent)]
#[extract_app(RenderApp)]
pub struct Foo { ... }
2.2 trait 签名层面的具体变化
ExtractComponent 的新定义见 crates/bevy_extract/src/extract_component.rs:
pub trait ExtractComponent<L: AppLabel, F = ()>: SyncComponent<L, F> {
/// ECS [`ReadOnlyQueryData`] to fetch the components to extract.
type QueryData: ReadOnlyQueryData;
/// Filters the entities with additional constraints.
type QueryFilter: QueryFilter;
/// 抽取输出(可插入子世界实体的 Bundle)。
type Out: Bundle<Effect: NoBundleEffect>;
/// 返回 `None` 时,会从子世界实体上移除 `SyncComponent::Target`。
fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option<Self::Out>;
}
关键语义:
L是目标子应用的AppLabel(如RenderApp);F是绕过孤儿规则(orphan rules)的标记类型,默认为(),为外部类型实现该 trait 时可传入一个本地类型(例如调用方插件的类型);Out是插入子世界的Bundle;如果主世界组件被移除,SyncComponent::Target中声明的组件才会被自动清理(见下一节);extract_component返回Option,返回None即触发对子世界实体的Target移除——这一点在内置抽取系统extract_components中有直接体现(extract_component.rs 中else { commands.entity(entity).remove::<C::Target>(); })。
SyncComponent 定义在 crates/bevy_extract/src/sync_component.rs:
pub trait SyncComponent<L: AppLabel, F = ()>: Component {
/// 描述主世界组件被移除时,子世界实体上应移除哪些组件。
type Target: Bundle<Effect: NoBundleEffect>;
}
SyncComponentPlugin 会做两件事(见 sync_component.rs):
- 把
SyncToSubWorld<L>注册为C的必需组件(required component),使SyncWorldPlugin能感知该实体需要同步; - 注册一个
On<Remove<C>>观察者:当主世界组件被移除时,向子世界推送EntityRecord::ComponentRemoved记录,在同步阶段移除TargetBundle 中声明的组件。
ExtractResource 同样携带 L,定义见 crates/bevy_extract/src/extract_resource.rs:
pub trait ExtractResource<L: AppLabel, F = ()>: Resource {
type Source: Resource;
/// 定义资源如何从主世界转移到子世界。
fn extract_resource(source: &Self::Source) -> Self;
}
其提取系统 extract_resource 具有变更检测优化:仅当 Source 资源 is_changed() 时才重新计算并写入子世界目标资源(见 extract_resource.rs)。
三、derive 宏:#[extract_app] 指定一个或多个目标子应用
3.1 单目标与多目标抽取
单目标写法即迁移文档中的 #[extract_app(RenderApp)]。泛化带来的新能力是:同一个组件可以同时抽取到多个子应用,只需把多个 AppLabel 作为 extract_app 的参数列表:
#[derive(Component, Clone, Debug, ExtractComponent)]
#[extract_app(RenderApp, AudioApp)]
struct SomeComponent;
宏的实现位于 crates/bevy_extract/macros/src/extract_component.rs:
- 缺少
#[extract_app]属性时会直接编译报错:ExtractComponent requires #[extract_app(MyAppLabelA, MyAppLabelB)] to specify the target sub-app(s)(L23-L30); - 空参数列表
#[extract_app()]同样报错,要求至少一个AppLabel(L32-L44); - 宏对
extract_app中的每个 label 分别生成一组impl SyncComponent<L>+impl ExtractComponent<L>(L84-L101),默认QueryData = &'static Self、Out = Self、extract_component返回Some(item.clone())。
此外宏还支持两个可选属性,便于自定义行为而无需手写 trait 实现:
#[extract_component_filter(<FilterType>)]:指定QueryFilter(缺省为());#[extract_component_sync_target(<BundleType>)]:指定SyncComponent::Target(缺省为Self)。
3.2 双目标抽取有测试背书
多目标抽取行为在 crates/bevy_extract/src/extract_plugin.rs 的单元测试 dual_extraction_works(L392-L545)中得到验证:组件 RenderComponentDual 通过 #[extract_app(ExtractAppA, ExtractAppB)] 同时派生到两个子应用,测试断言两个子世界都包含该组件,且主世界移除后两个子世界各自按自身 Target 独立清理。单目标场景则由 extraction_works 测试(L217-L307)覆盖,验证了"主世界移除组件 → 子世界 Target 中声明的组件随之移除"的联动行为。
四、实体同步层的类型变化与 TemporaryRenderEntity::default()
4.1 同步类型现在带 AppLabel 参数
crates/bevy_extract/src/sync_world.rs 中同步相关组件全部参数化:
SyncToSubWorld<L>(L121-L123):标记实体需要同步到L对应的子世界,由ExtractComponentPlugin/SyncComponentPlugin自动注册为必需组件,通常无需手动插入;SubEntity<L>(L128-L130):挂主世界实体上,记录其对应的子世界Entity;MainEntity(L157-L159):挂子世界实体上,记录对应的主世界Entity;TemporaryEntity<L>(L194-L197):标记实体本帧结束时需要被 despawn。
SyncWorldPlugin<L> 通过主世界中的观察者把新增/移除记录累积到 PendingSyncEntity<L> 资源,再由 entity_sync_system 在每帧抽取前统一执行:在子世界 spawn 带 MainEntity 的新实体、在主世界回填 SubEntity,或按记录执行 despawn 与组件清理(sync_world.rs)。其文档注释还给出了主世界/子世界实体的对应关系示意与"每帧先 sync 后 extract"的时序图(L44-L69)。
4.2 为什么 TemporaryRenderEntity 必须改用 ::default()
TemporaryRenderEntity 现在不再是单元结构体,而是 crates/bevy_render/src/lib.rs 中的类型别名(L84-L90):
pub type SyncToRenderWorld = bevy_extract::sync_world::SyncToSubWorld<crate::RenderApp>;
pub type RenderEntity = bevy_extract::sync_world::SubEntity<crate::RenderApp>;
pub type TemporaryRenderEntity = bevy_extract::sync_world::TemporaryEntity<crate::RenderApp>;
由于 TemporaryEntity<L> 内部持有 PhantomData<L>,旧的 TemporaryRenderEntity 字面量构造不再合法,需要迁移文档要求的 TemporaryRenderEntity::default()(TemporaryEntity 在 L: Default 时派生了 Default)。同样的别名机制意味着你过去在 bevy_render 里写的 SyncToRenderWorld、RenderEntity 等类型名保持不变,但底层已经参数化到 RenderApp。
五、ExtractPlugin 与抽取流程(extract() 迁移)
迁移文档最后一项变更:bevy_render::extract_plugin::extract() 移到了 bevy_extract::extract_plugin::extract()。
extract() 函数(extract_plugin.rs)的工作原理值得理解,因为它解释了主世界为何"只读可查":
pub fn extract(main_world: &mut World, sub_world: &mut World) {
// 把主世界临时作为资源插入子世界
let scratch_world = main_world.remove_resource::<ScratchMainWorld>().unwrap();
let inserted_world = core::mem::replace(main_world, scratch_world.0);
sub_world.insert_resource(MainWorld(inserted_world));
sub_world.run_schedule(ExtractSchedule);
// 恢复主世界
let inserted_world = sub_world.remove_resource::<MainWorld>().unwrap();
...
}
即:把整个主世界作为一个 World 资源(MainWorld)挂进子世界,运行 ExtractSchedule,结束后再换回来。ScratchMainWorld 是预分配的"scratch"世界,避免每帧重新分配(L129-L132)。
配套要点:
ExtractSchedule文档明确提示"该步骤应尽量短,以提升流水线(pipelining)潜力"(L100-L108);ExtractPlugin在构建子应用时把 ExtractSchedule 的自动 deferred 命令应用关闭(auto_insert_apply_deferred: false),改为在子应用的普通 schedule 中通过apply_extract_commands执行,使命令应用可与主应用并行(L59-L80);- 在
ExtractSchedule中读取主世界数据的系统参数是Extract<P>(crates/bevy_extract/src/extract_param.rs),它要求内部参数是ReadOnlySystemParam(主世界不可被抽取阶段修改),文档示例演示了Extract<Query<SubEntity<ExtractApp>, With<Cloud>>>的用法(L33-L48)。
5.1 ExtractComponentPlugin 的注册与"仅可见实体"变体
组件抽取通过 ExtractComponentPlugin<C, L, F> 注册(extract_component.rs)。其 build 逻辑:先自动加入 SyncComponentPlugin(保证 Target 清理语义),再向子应用 ExtractSchedule 注册抽取系统。性能相关的一点:提供 ExtractComponentPlugin::extract_visible() 构造器(L72-L79),对应系统 extract_visible_components 会额外查询 ViewVisibility,只对当前对可见的实体做抽取(L118-L136),适合"渲染可见性"类组件的抽取场景。
六、迁移清单(Checklist)
结合迁移文档与 crates/bevy_render/src/lib.rs 的再导出,逐项检查你的代码库:
- import 路径:
bevy_render::extract_plugin::extract→bevy_extract::extract_plugin::extract(函数本身已搬家,见 crates/bevy_extract/src/extract_plugin.rs); - trait 实现:为
SyncComponent、ExtractComponent、ExtractResource及其插件类型ExtractComponentPlugin/ExtractResourcePlugin/SyncComponentPlugin补充AppLabel泛型参数,如impl SyncComponent<RenderApp> for TemporalAntiAliasing; - derive 宏:所有
#[derive(ExtractComponent)]/#[derive(ExtractResource)]必须追加#[extract_app(...)],缺少时宏会给出明确的编译错误信息;需要多目标时列出多个 label; - 临时实体:
TemporaryRenderEntity构造改为TemporaryRenderEntity::default(); bevy_render再导出兜底:大多数抽取类型仍由bevy_render再导出(lib.rs 中可见ExtractComponent、ExtractPlugin、ExtractResource、SyncComponent、MainEntity、MainEntityHashMap、MainEntityHashSet及SyncToRenderWorld/RenderEntity/TemporaryRenderEntity别名),仅依赖渲染管线的代码可保持旧 import;但涉及extract()函数或直接对接非渲染子应用(如AudioApp)的代码必须改用bevy_extract;- 外部类型实现:若你的 trait 实现涉及第三方类型,利用
F标记类型参数绕过孤儿规则,文档建议在调用ExtractComponentPlugin时传入本地类型(如插件自身类型)。
边界与限制
- 本文档描述的是当前仓库 HEAD 的抽取 API 形态,适用于正在向此版本迁移的下游项目;
extract_component返回None只移除TargetBundle,不影响子世界实体本身——实体生命周期由SyncWorldPlugin依据SyncToSubWorld<L>的存在与否决定;ExtractPlugin注册时若找不到目标子应用(app.get_sub_app_mut(L::default())为None),组件抽取系统不会挂载,注册顺序上应确保ExtractPlugin::<L>先于对应组件插件添加;Extract参数仅读主世界且只在ExtractSchedule内可用,不要在子应用的其他 schedule 中使用。
参考文件
- 迁移原文档:_release-content/migration-guides/extract-extract.md
- crate 入口与 README:crates/bevy_extract/src/lib.rs、crates/bevy_extract/README.md
- 组件/资源/同步实现:extract_component.rs、extract_resource.rs、sync_component.rs、sync_world.rs
- 插件与调度:extract_plugin.rs
- 派生宏:crates/bevy_extract/macros/src/extract_component.rs
- 渲染侧再导出:crates/bevy_render/src/lib.rs
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 StartedRust0623
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