首页
/ Bevy 抽取系统泛化迁移指南:从 bevy_extract 到 AppLabel 的完整改造

Bevy 抽取系统泛化迁移指南:从 bevy_extract 到 AppLabel 的完整改造

2026-09-05 19:05:52作者:薛曦旖Francesca

本篇基于 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 的子应用(RenderAppAudioApp 或自定义 label)都可以作为抽取目标。

crates/bevy_extract/src/lib.rs 的模块文档可以看到新的设计目标:

  • 通过为指定 AppLabel 添加 ExtractPlugin 完成基础接入;
  • 派生 ExtractComponentExtractResource 时,必须用 extract_app 属性指明目标子应用;
  • 派生 ExtractComponent 会自动追加 SyncComponent 实现,先把主世界的实体同步到子世界,再把组件数据从主实体复制到子实体;
  • 子应用可以在 ExtractSchedule 中通过 Extract 参数访问主世界。

对应的 crate 结构为 crates/bevy_extract/srcextract_component.rs(组件抽取)、extract_resource.rs(资源抽取)、extract_instances.rs(高性能实例化抽取)、extract_param.rsExtract 系统参数)、extract_plugin.rs(插件与调度)、sync_component.rs(组件联动清理)、sync_world.rs(实体同步)。

二、核心迁移点:所有抽取 trait 必须携带 AppLabel

迁移文档列出的两条硬性规则:

  1. 使用 TemporaryRenderEntity::default() 取代 TemporaryRenderEntity 构造;
  2. 使用 SyncComponentExtractComponentExtractResource 等抽取相关 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.rselse { 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 记录,在同步阶段移除 Target Bundle 中声明的组件。

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()] 同样报错,要求至少一个 AppLabelL32-L44);
  • 宏对 extract_app 中的每个 label 分别生成一组 impl SyncComponent<L> + impl ExtractComponent<L>L84-L101),默认 QueryData = &'static SelfOut = Selfextract_component 返回 Some(item.clone())

此外宏还支持两个可选属性,便于自定义行为而无需手写 trait 实现:

  • #[extract_component_filter(<FilterType>)]:指定 QueryFilter(缺省为 ());
  • #[extract_component_sync_target(<BundleType>)]:指定 SyncComponent::Target(缺省为 Self)。

extract_component.rs 宏

3.2 双目标抽取有测试背书

多目标抽取行为在 crates/bevy_extract/src/extract_plugin.rs 的单元测试 dual_extraction_worksL392-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
  • MainEntityL157-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()TemporaryEntityL: Default 时派生了 Default)。同样的别名机制意味着你过去在 bevy_render 里写的 SyncToRenderWorldRenderEntity 等类型名保持不变,但底层已经参数化到 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 的再导出,逐项检查你的代码库:

  1. import 路径bevy_render::extract_plugin::extractbevy_extract::extract_plugin::extract(函数本身已搬家,见 crates/bevy_extract/src/extract_plugin.rs);
  2. trait 实现:为 SyncComponentExtractComponentExtractResource 及其插件类型 ExtractComponentPlugin / ExtractResourcePlugin / SyncComponentPlugin 补充 AppLabel 泛型参数,如 impl SyncComponent<RenderApp> for TemporalAntiAliasing
  3. derive 宏:所有 #[derive(ExtractComponent)] / #[derive(ExtractResource)] 必须追加 #[extract_app(...)],缺少时宏会给出明确的编译错误信息;需要多目标时列出多个 label;
  4. 临时实体TemporaryRenderEntity 构造改为 TemporaryRenderEntity::default()
  5. bevy_render 再导出兜底:大多数抽取类型仍由 bevy_render 再导出(lib.rs 中可见 ExtractComponentExtractPluginExtractResourceSyncComponentMainEntityMainEntityHashMapMainEntityHashSetSyncToRenderWorld / RenderEntity / TemporaryRenderEntity 别名),仅依赖渲染管线的代码可保持旧 import;但涉及 extract() 函数或直接对接非渲染子应用(如 AudioApp)的代码必须改用 bevy_extract
  6. 外部类型实现:若你的 trait 实现涉及第三方类型,利用 F 标记类型参数绕过孤儿规则,文档建议在调用 ExtractComponentPlugin 时传入本地类型(如插件自身类型)。

边界与限制

  • 本文档描述的是当前仓库 HEAD 的抽取 API 形态,适用于正在向此版本迁移的下游项目;
  • extract_component 返回 None 只移除 Target Bundle,不影响子世界实体本身——实体生命周期由 SyncWorldPlugin 依据 SyncToSubWorld<L> 的存在与否决定;
  • ExtractPlugin 注册时若找不到目标子应用(app.get_sub_app_mut(L::default())None),组件抽取系统不会挂载,注册顺序上应确保 ExtractPlugin::<L> 先于对应组件插件添加;
  • Extract 参数仅读主世界且只在 ExtractSchedule 内可用,不要在子应用的其他 schedule 中使用。

参考文件

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