Bevy 抽取系统泛化迁移指南:从 bevy_extract 到 AppLabel 的完整改造
2026/9/6 22:38:16 网站建设 项目流程

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

【免费下载链接】bevyA refreshingly simple>项目地址: https://gitcode.com/GitHub_Trending/be/bevy

本篇基于 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/src:extract_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.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 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_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()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.1ExtractComponentPlugin的注册与"仅可见实体"变体

组件抽取通过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只移除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

【免费下载链接】bevyA refreshingly simple>项目地址: https://gitcode.com/GitHub_Trending/be/bevy

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询