Skip to content

Split Bundle and StaticBundle #19761

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c53f10a
Introduce StaticBundle
SkiFire13 Jun 19, 2025
a015f7d
Implement StaticBundle for Components
SkiFire13 Jun 19, 2025
fd52029
Implement StaticBundle for tuples of StaticBundles
SkiFire13 Jun 19, 2025
9bb3f3c
Implement StaticBundle in the derive macro
SkiFire13 Jun 19, 2025
ebc4143
Implement StaticBundle for SpawnRelatedBundle and SpawnOneRelated
SkiFire13 Jun 19, 2025
e806f5e
Split Bundles methods for Bundle and StaticBundle
SkiFire13 Jun 20, 2025
07f6d1d
Switch Entity{Ref,Mut}Except to StaticBundle
SkiFire13 Jun 2, 2025
ddd69c5
Switch Observer and Trigger to StaticBundle
SkiFire13 Jun 2, 2025
9b0ab87
Require StaticBundle in ReflectBundle
SkiFire13 Jun 2, 2025
9a176a5
Switch EntityClonerBuilder to StaticBundle
SkiFire13 Jun 2, 2025
bd30a8b
Require StaticBundle in batch spawning
SkiFire13 Jun 2, 2025
28a21bd
Switch component removal to StaticBundle
SkiFire13 Jun 2, 2025
c33033a
Switch EntityWorldMut::retain to StaticBundle
SkiFire13 Jun 2, 2025
a44cf37
Require StaticBundle in ExtractComponent
SkiFire13 Jun 2, 2025
e683a10
Add support for #[bundle(dynamic)] attribute
SkiFire13 Jun 3, 2025
4a0f69e
Add migration guide
SkiFire13 Jun 20, 2025
ef88ad8
Add &self parameter to Bundle methods
SkiFire13 Jun 21, 2025
5942b7a
Fix warning
SkiFire13 Jun 21, 2025
20fef0c
Merge remote-tracking branch 'upstream/main' into split-bundle
SkiFire13 Jun 23, 2025
9711258
Apply suggestions
SkiFire13 Jun 23, 2025
f6fe7e8
Merge remote-tracking branch 'upstream/main' into split-bundle
SkiFire13 Jun 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions benches/benches/bevy_ecs/entity_cloning.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use core::hint::black_box;

use benches::bench;
use bevy_ecs::bundle::{Bundle, InsertMode};
use bevy_ecs::bundle::{Bundle, InsertMode, StaticBundle};
use bevy_ecs::component::ComponentCloneBehavior;
use bevy_ecs::entity::EntityCloner;
use bevy_ecs::hierarchy::ChildOf;
Expand All @@ -27,7 +27,7 @@ type ComplexBundle = (C<1>, C<2>, C<3>, C<4>, C<5>, C<6>, C<7>, C<8>, C<9>, C<10

/// Sets the [`ComponentCloneBehavior`] for all explicit and required components in a bundle `B` to
/// use the [`Reflect`] trait instead of [`Clone`].
fn reflection_cloner<B: Bundle + GetTypeRegistration>(
fn reflection_cloner<B: StaticBundle + GetTypeRegistration>(
world: &mut World,
linked_cloning: bool,
) -> EntityCloner {
Expand Down Expand Up @@ -65,7 +65,7 @@ fn reflection_cloner<B: Bundle + GetTypeRegistration>(
/// components (which is usually [`ComponentCloneBehavior::clone()`]). If `clone_via_reflect`
/// is true, it will overwrite the handler for all components in the bundle to be
/// [`ComponentCloneBehavior::reflect()`].
fn bench_clone<B: Bundle + Default + GetTypeRegistration>(
fn bench_clone<B: Bundle + StaticBundle + Default + GetTypeRegistration>(
b: &mut Bencher,
clone_via_reflect: bool,
) {
Expand Down Expand Up @@ -96,7 +96,7 @@ fn bench_clone<B: Bundle + Default + GetTypeRegistration>(
/// For example, setting `height` to 5 and `children` to 1 creates a single chain of entities with
/// no siblings. Alternatively, setting `height` to 1 and `children` to 5 will spawn 5 direct
/// children of the root entity.
fn bench_clone_hierarchy<B: Bundle + Default + GetTypeRegistration>(
fn bench_clone_hierarchy<B: Bundle + StaticBundle + Default + GetTypeRegistration>(
b: &mut Bencher,
height: usize,
children: usize,
Expand Down Expand Up @@ -268,7 +268,7 @@ const FILTER_SCENARIOS: [FilterScenario; 11] = [
///
/// The bundle must implement [`Default`], which is used to create the first entity that gets its components cloned
/// in the benchmark. It may also be used to populate the target entity depending on the scenario.
fn bench_filter<B: Bundle + Default>(b: &mut Bencher, scenario: FilterScenario) {
fn bench_filter<B: Bundle + StaticBundle + Default>(b: &mut Bencher, scenario: FilterScenario) {
let mut world = World::default();
let mut spawn = |empty| match empty {
false => world.spawn(B::default()).id(),
Expand Down
6 changes: 4 additions & 2 deletions benches/benches/bevy_ecs/world/world_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use core::hint::black_box;
use nonmax::NonMaxU32;

use bevy_ecs::{
bundle::{Bundle, NoBundleEffect},
bundle::{Bundle, NoBundleEffect, StaticBundle},
component::Component,
entity::{Entity, EntityRow},
system::{Query, SystemState},
Expand Down Expand Up @@ -37,7 +37,9 @@ fn setup<T: Component + Default>(entity_count: u32) -> World {
black_box(world)
}

fn setup_wide<T: Bundle<Effect: NoBundleEffect> + Default>(entity_count: u32) -> World {
fn setup_wide<T: Bundle<Effect: NoBundleEffect> + StaticBundle + Default>(
entity_count: u32,
) -> World {
let mut world = World::default();
world.spawn_batch((0..entity_count).map(|_| T::default()));
black_box(world)
Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use alloc::{
};
pub use bevy_derive::AppLabel;
use bevy_ecs::{
bundle::StaticBundle,
component::RequiredComponentsError,
error::{DefaultErrorHandler, ErrorHandler},
event::{event_update_system, EventCursor},
Expand Down Expand Up @@ -1340,7 +1341,7 @@ impl App {
/// }
/// });
/// ```
pub fn add_observer<E: Event, B: Bundle, M>(
pub fn add_observer<E: Event, B: StaticBundle, M>(
&mut self,
observer: impl IntoObserverSystem<E, B, M>,
) -> &mut Self {
Expand Down
52 changes: 47 additions & 5 deletions crates/bevy_ecs/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,21 @@ enum BundleFieldKind {
}

const BUNDLE_ATTRIBUTE_NAME: &str = "bundle";
const BUNDLE_ATTRIBUTE_DYNAMIC: &str = "dynamic";
const BUNDLE_ATTRIBUTE_IGNORE_NAME: &str = "ignore";
const BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS: &str = "ignore_from_components";

#[derive(Debug)]
struct BundleAttributes {
impl_from_components: bool,
dynamic: bool,
}

impl Default for BundleAttributes {
fn default() -> Self {
Self {
impl_from_components: true,
dynamic: false,
}
}
}
Expand All @@ -61,8 +64,12 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
attributes.impl_from_components = false;
return Ok(());
}
if meta.path.is_ident(BUNDLE_ATTRIBUTE_DYNAMIC) {
attributes.dynamic = true;
return Ok(());
}

Err(meta.error(format!("Invalid bundle container attribute. Allowed attributes: `{BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS}`")))
Err(meta.error(format!("Invalid bundle container attribute. Allowed attributes: `{BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS}`, `{BUNDLE_ATTRIBUTE_DYNAMIC}`")))
});

if let Err(error) = parsing {
Expand Down Expand Up @@ -139,6 +146,37 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let struct_name = &ast.ident;

let static_bundle_impl = (!attributes.dynamic).then(|| quote! {
// SAFETY:
// - all the active fields must implement `StaticBundle` for the function bodies to compile, and hence
// this bundle also represents a static set of components;
// - `component_ids` and `get_component_ids` delegate to the underlying implementation in the same order
// and hence are coherent;
#[allow(deprecated)]
unsafe impl #impl_generics #ecs_path::bundle::StaticBundle for #struct_name #ty_generics #where_clause {
fn component_ids(
components: &mut #ecs_path::component::ComponentsRegistrator,
ids: &mut impl FnMut(#ecs_path::component::ComponentId)
){
#(<#active_field_types as #ecs_path::bundle::StaticBundle>::component_ids(components, &mut *ids);)*
}

fn get_component_ids(
components: &#ecs_path::component::Components,
ids: &mut impl FnMut(Option<#ecs_path::component::ComponentId>)
){
#(<#active_field_types as #ecs_path::bundle::StaticBundle>::get_component_ids(components, &mut *ids);)*
}

fn register_required_components(
components: &mut #ecs_path::component::ComponentsRegistrator,
required_components: &mut #ecs_path::component::RequiredComponents
){
#(<#active_field_types as #ecs_path::bundle::StaticBundle>::register_required_components(components, &mut *required_components);)*
}
}
});

let bundle_impl = quote! {
// SAFETY:
// - ComponentId is returned in field-definition-order. [get_components] uses field-definition-order
Expand All @@ -147,24 +185,27 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
#[allow(deprecated)]
unsafe impl #impl_generics #ecs_path::bundle::Bundle for #struct_name #ty_generics #where_clause {
fn component_ids(
&self,
components: &mut #ecs_path::component::ComponentsRegistrator,
ids: &mut impl FnMut(#ecs_path::component::ComponentId)
) {
#(<#active_field_types as #ecs_path::bundle::Bundle>::component_ids(components, ids);)*
#(<#active_field_types as #ecs_path::bundle::Bundle>::component_ids(&self.#active_field_tokens, components, ids);)*
}

fn get_component_ids(
&self,
components: &#ecs_path::component::Components,
ids: &mut impl FnMut(Option<#ecs_path::component::ComponentId>)
) {
#(<#active_field_types as #ecs_path::bundle::Bundle>::get_component_ids(components, &mut *ids);)*
#(<#active_field_types as #ecs_path::bundle::Bundle>::get_component_ids(&self.#active_field_tokens, components, &mut *ids);)*
}

fn register_required_components(
&self,
components: &mut #ecs_path::component::ComponentsRegistrator,
required_components: &mut #ecs_path::component::RequiredComponents
) {
#(<#active_field_types as #ecs_path::bundle::Bundle>::register_required_components(components, required_components);)*
#(<#active_field_types as #ecs_path::bundle::Bundle>::register_required_components(&self.#active_field_tokens, components, required_components);)*
}
}
};
Expand All @@ -184,7 +225,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
}
};

let from_components_impl = attributes.impl_from_components.then(|| quote! {
let from_components_impl = (attributes.impl_from_components && !attributes.dynamic).then(|| quote! {
// SAFETY:
// - ComponentId is returned in field-definition-order. [from_components] uses field-definition-order
#[allow(deprecated)]
Expand All @@ -206,6 +247,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {

TokenStream::from(quote! {
#(#attribute_errors)*
#static_bundle_impl
#bundle_impl
#from_components_impl
#dynamic_bundle_impl
Expand Down
Loading