From c53f10a7655028ee53eb44d463ba0f17956688ea Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Thu, 19 Jun 2025 08:26:53 +0200 Subject: [PATCH 01/20] Introduce StaticBundle --- crates/bevy_ecs/src/bundle.rs | 59 ++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 8efdc60ad9345..b350215900c26 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -81,11 +81,12 @@ use bevy_utils::TypeIdMap; use core::{any::TypeId, ptr::NonNull}; use variadics_please::all_tuples; -/// The `Bundle` trait enables insertion and removal of [`Component`]s from an entity. +/// The `Bundle` trait enables insertion of [`Component`]s to an entity. +/// For the removal of [`Component`]s from an entity see the [`StaticBundle`]`trait`. /// /// Implementers of the `Bundle` trait are called 'bundles'. /// -/// Each bundle represents a static set of [`Component`] types. +/// Each bundle represents a possibly dynamic set of [`Component`] types. /// Currently, bundles can only contain one of each [`Component`], and will /// panic once initialized if this is not met. /// @@ -115,15 +116,6 @@ use variadics_please::all_tuples; /// contains the components of a bundle. /// Queries should instead only select the components they logically operate on. /// -/// ## Removal -/// -/// Bundles are also used when removing components from an entity. -/// -/// Removing a bundle from an entity will remove any of its components attached -/// to the entity from the entity. -/// That is, if the entity does not have all the components of the bundle, those -/// which are present will be removed. -/// /// # Implementers /// /// Every type which implements [`Component`] also implements `Bundle`, since @@ -215,7 +207,50 @@ pub unsafe trait Bundle: DynamicBundle + Send + Sync + 'static { ); } -/// Creates a [`Bundle`] by taking it from internal storage. +/// Each bundle represents a static and fixed set of [`Component`] types. +/// See the [`Bundle`] trait for a possibly dynamic set of [`Component`] types. +/// +/// Implementers of the `Bundle` trait are called 'static bundles'. +/// +/// ## Removal +/// +/// Static bundles are used when removing components from an entity. +/// +/// Removing a bundle from an entity will remove any of its components attached +/// to the entity from the entity. +/// That is, if the entity does not have all the components of the bundle, those +/// which are present will be removed. +/// +/// # Safety +/// +/// Manual implementations of this trait are unsupported. +/// That is, there is no safe way to implement this trait, and you must not do so. +/// If you want a type to implement [`StaticBundle`], you must use [`derive@Bundle`](derive@Bundle). +// Some safety points: +// - [`StaticBundle::component_ids`] and [`StaticBundle::get_component_ids`] must match the behavior of [`Bundle::component_ids`] +#[diagnostic::on_unimplemented( + message = "`{Self}` is not a `StaticBundle`", + label = "invalid `StaticBundle`", + note = "consider annotating `{Self}` with `#[derive(Component)]` or `#[derive(Bundle)]`" +)] +pub unsafe trait StaticBundle: Send + Sync + 'static { + /// Gets this [`StaticBundle`]'s component ids, in the order of this bundle's [`Component`]s + #[doc(hidden)] + fn component_ids(components: &mut ComponentsRegistrator, ids: &mut impl FnMut(ComponentId)); + + /// Gets this [`StaticBundle`]'s component ids. This will be [`None`] if the component has not been registered. + #[doc(hidden)] + fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option)); + + /// Registers components that are required by the components in this [`StaticBundle`]. + #[doc(hidden)] + fn register_required_components( + _components: &mut ComponentsRegistrator, + _required_components: &mut RequiredComponents, + ); +} + +/// Creates a bundle by taking it from the internal storage. /// /// # Safety /// From a015f7da1ecc0ba3ffcc685970dc8be5c8205c14 Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Thu, 19 Jun 2025 08:29:08 +0200 Subject: [PATCH 02/20] Implement StaticBundle for Components --- crates/bevy_ecs/src/bundle.rs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index b350215900c26..f9157bad70155 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -307,13 +307,17 @@ pub trait BundleEffect { } // SAFETY: -// - `Bundle::component_ids` calls `ids` for C's component id (and nothing else) -// - `Bundle::get_components` is called exactly once for C and passes the component's storage type based on its associated constant. -unsafe impl Bundle for C { +// - `C` always represents the set of components containing just `C` +// - `component_ids` and `get_component_ids` both call `ids` just once for C's component id (and nothing else). +unsafe impl StaticBundle for C { fn component_ids(components: &mut ComponentsRegistrator, ids: &mut impl FnMut(ComponentId)) { ids(components.register_component::()); } + fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option)) { + ids(components.get_id(TypeId::of::())); + } + fn register_required_components( components: &mut ComponentsRegistrator, required_components: &mut RequiredComponents, @@ -327,9 +331,25 @@ unsafe impl Bundle for C { &mut Vec::new(), ); } +} + +// SAFETY: +// - `component_ids` calls `ids` for C's component id (and nothing else) +// - `get_components` is called exactly once for C and passes the component's storage type based on its associated constant. +unsafe impl Bundle for C { + fn component_ids(components: &mut ComponentsRegistrator, ids: &mut impl FnMut(ComponentId)) { + ::component_ids(components, ids); + } fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option)) { - ids(components.get_id(TypeId::of::())); + ::get_component_ids(components, ids); + } + + fn register_required_components( + components: &mut ComponentsRegistrator, + required_components: &mut RequiredComponents, + ) { + ::register_required_components(components, required_components); } } From fd5202989c6b6bbf0194915d78ee21655ebf9ab3 Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Thu, 19 Jun 2025 08:30:41 +0200 Subject: [PATCH 03/20] Implement StaticBundle for tuples of StaticBundles --- crates/bevy_ecs/src/bundle.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index f9157bad70155..7d230b31c7aed 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -378,6 +378,37 @@ impl DynamicBundle for C { macro_rules! tuple_impl { ($(#[$meta:meta])* $($name: ident),*) => { + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such, the lints below may not always apply." + )] + #[allow( + unused_mut, + unused_variables, + reason = "Zero-length tuples won't use any of the parameters." + )] + $(#[$meta])* + // SAFETY: + // - all the sub-bundles are static, and hence their combination is static too; + // - `component_ids` and `get_component_ids` both delegate to the sub-bundle's methods + // exactly once per sub-bundle, hence they are coherent. + unsafe impl<$($name: StaticBundle),*> StaticBundle for ($($name,)*) { + fn component_ids(components: &mut ComponentsRegistrator, ids: &mut impl FnMut(ComponentId)){ + $(<$name as StaticBundle>::component_ids(components, ids);)* + } + + fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option)){ + $(<$name as StaticBundle>::get_component_ids(components, ids);)* + } + + fn register_required_components( + components: &mut ComponentsRegistrator, + required_components: &mut RequiredComponents, + ) { + $(<$name as StaticBundle>::register_required_components(components, required_components);)* + } + } + #[expect( clippy::allow_attributes, reason = "This is a tuple-related macro; as such, the lints below may not always apply." From 9bb3f3c50ba1853948fc77d22f25fb67097bd7ea Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Thu, 19 Jun 2025 08:32:42 +0200 Subject: [PATCH 04/20] Implement StaticBundle in the derive macro --- crates/bevy_ecs/macros/src/lib.rs | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 7750f972592c2..f6ba72f10c110 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -139,6 +139,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 = 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 @@ -206,6 +237,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { TokenStream::from(quote! { #(#attribute_errors)* + #static_bundle_impl #bundle_impl #from_components_impl #dynamic_bundle_impl From ebc414351559992d3413618e2009df6abab96904 Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Thu, 19 Jun 2025 08:39:10 +0200 Subject: [PATCH 05/20] Implement StaticBundle for SpawnRelatedBundle and SpawnOneRelated --- crates/bevy_ecs/src/spawn.rs | 73 ++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/crates/bevy_ecs/src/spawn.rs b/crates/bevy_ecs/src/spawn.rs index d5014f22409fd..1c27927061cf9 100644 --- a/crates/bevy_ecs/src/spawn.rs +++ b/crates/bevy_ecs/src/spawn.rs @@ -2,7 +2,7 @@ //! for the best entry points into these APIs and examples of how to use them. use crate::{ - bundle::{Bundle, BundleEffect, DynamicBundle, NoBundleEffect}, + bundle::{Bundle, BundleEffect, DynamicBundle, NoBundleEffect, StaticBundle}, entity::Entity, relationship::{RelatedSpawner, Relationship, RelationshipTarget}, world::{EntityWorldMut, World}, @@ -182,34 +182,60 @@ impl> BundleEffect for SpawnRelatedBundle + Send + Sync + 'static> Bundle +// SAFETY: This internally relies on the RelationshipTarget's StaticBundle implementation, which is sound. +unsafe impl + Send + Sync + 'static> StaticBundle for SpawnRelatedBundle { fn component_ids( components: &mut crate::component::ComponentsRegistrator, ids: &mut impl FnMut(crate::component::ComponentId), ) { - ::component_ids(components, ids); + ::component_ids(components, ids); } fn get_component_ids( components: &crate::component::Components, ids: &mut impl FnMut(Option), ) { - ::get_component_ids(components, ids); + ::get_component_ids(components, ids); } fn register_required_components( components: &mut crate::component::ComponentsRegistrator, required_components: &mut crate::component::RequiredComponents, ) { - ::register_required_components( + ::register_required_components( components, required_components, ); } } + +// SAFETY: This internally relies on the RelationshipTarget's Bundle implementation, which is sound. +unsafe impl + Send + Sync + 'static> Bundle + for SpawnRelatedBundle +{ + fn component_ids( + components: &mut crate::component::ComponentsRegistrator, + ids: &mut impl FnMut(crate::component::ComponentId), + ) { + ::component_ids(components, ids); + } + + fn get_component_ids( + components: &crate::component::Components, + ids: &mut impl FnMut(Option), + ) { + ::get_component_ids(components, ids); + } + + fn register_required_components( + components: &mut crate::component::ComponentsRegistrator, + required_components: &mut crate::component::RequiredComponents, + ) { + ::register_required_components(components, required_components); + } +} impl> DynamicBundle for SpawnRelatedBundle { type Effect = Self; @@ -250,34 +276,57 @@ impl DynamicBundle for SpawnOneRelated { self } } - -// SAFETY: This internally relies on the RelationshipTarget's Bundle implementation, which is sound. -unsafe impl Bundle for SpawnOneRelated { +// SAFETY: This internally relies on the RelationshipTarget's StaticBundle implementation, which is sound. +unsafe impl StaticBundle for SpawnOneRelated { fn component_ids( components: &mut crate::component::ComponentsRegistrator, ids: &mut impl FnMut(crate::component::ComponentId), ) { - ::component_ids(components, ids); + ::component_ids(components, ids); } fn get_component_ids( components: &crate::component::Components, ids: &mut impl FnMut(Option), ) { - ::get_component_ids(components, ids); + ::get_component_ids(components, ids); } fn register_required_components( components: &mut crate::component::ComponentsRegistrator, required_components: &mut crate::component::RequiredComponents, ) { - ::register_required_components( + ::register_required_components( components, required_components, ); } } +// SAFETY: This internally relies on the RelationshipTarget's Bundle implementation, which is sound. +unsafe impl Bundle for SpawnOneRelated { + fn component_ids( + components: &mut crate::component::ComponentsRegistrator, + ids: &mut impl FnMut(crate::component::ComponentId), + ) { + ::component_ids(components, ids); + } + + fn get_component_ids( + components: &crate::component::Components, + ids: &mut impl FnMut(Option), + ) { + ::get_component_ids(components, ids); + } + + fn register_required_components( + components: &mut crate::component::ComponentsRegistrator, + required_components: &mut crate::component::RequiredComponents, + ) { + ::register_required_components(components, required_components); + } +} + /// [`RelationshipTarget`] methods that create a [`Bundle`] with a [`DynamicBundle::Effect`] that: /// /// 1. Contains the [`RelationshipTarget`] component, pre-allocated with the necessary space for spawned entities. From e806f5e5b0004a7e2cfe38db7d7e3b2bf7f489d6 Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Fri, 20 Jun 2025 07:49:20 +0200 Subject: [PATCH 06/20] Split Bundles methods for Bundle and StaticBundle --- crates/bevy_ecs/src/bundle.rs | 53 ++++++++++++++++++++----- crates/bevy_ecs/src/world/entity_ref.rs | 2 +- crates/bevy_ecs/src/world/mod.rs | 6 +-- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 7d230b31c7aed..bd29888e6ff61 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -1143,6 +1143,7 @@ pub(crate) enum ArchetypeMoveType { impl<'w> BundleInserter<'w> { #[inline] pub(crate) fn new( + bundle: &T, world: &'w mut World, archetype_id: ArchetypeId, change_tick: Tick, @@ -1150,9 +1151,10 @@ impl<'w> BundleInserter<'w> { // SAFETY: These come from the same world. `world.components_registrator` can't be used since we borrow other fields too. let mut registrator = unsafe { ComponentsRegistrator::new(&mut world.components, &mut world.component_ids) }; - let bundle_id = world - .bundles - .register_info::(&mut registrator, &mut world.storages); + let bundle_id = + world + .bundles + .register_info::(&bundle, &mut registrator, &mut world.storages); // SAFETY: We just ensured this bundle exists unsafe { Self::new_with_id(world, archetype_id, bundle_id, change_tick) } } @@ -1535,7 +1537,7 @@ impl<'w> BundleRemover<'w> { /// # Safety /// Caller must ensure that `archetype_id` is valid #[inline] - pub(crate) unsafe fn new( + pub(crate) unsafe fn new( world: &'w mut World, archetype_id: ArchetypeId, require_all: bool, @@ -1545,7 +1547,7 @@ impl<'w> BundleRemover<'w> { unsafe { ComponentsRegistrator::new(&mut world.components, &mut world.component_ids) }; let bundle_id = world .bundles - .register_info::(&mut registrator, &mut world.storages); + .register_static_info::(&mut registrator, &mut world.storages); // SAFETY: we initialized this bundle_id in `init_info`, and caller ensures archetype is valid. unsafe { Self::new_with_id(world, archetype_id, bundle_id, require_all) } } @@ -1806,14 +1808,25 @@ pub(crate) struct BundleSpawner<'w> { } impl<'w> BundleSpawner<'w> { + pub fn new(bundle: &T, world: &'w mut World, change_tick: Tick) -> Self { + // SAFETY: These come from the same world. `world.components_registrator` can't be used since we borrow other fields too. + let mut registrator = + unsafe { ComponentsRegistrator::new(&mut world.components, &mut world.component_ids) }; + let bundle_id = world + .bundles + .register_info(bundle, &mut registrator, &mut world.storages); + // SAFETY: we initialized this bundle_id in `init_info` + unsafe { Self::new_with_id(world, bundle_id, change_tick) } + } + #[inline] - pub fn new(world: &'w mut World, change_tick: Tick) -> Self { + pub fn new_static(world: &'w mut World, change_tick: Tick) -> Self { // SAFETY: These come from the same world. `world.components_registrator` can't be used since we borrow other fields too. let mut registrator = unsafe { ComponentsRegistrator::new(&mut world.components, &mut world.component_ids) }; let bundle_id = world .bundles - .register_info::(&mut registrator, &mut world.storages); + .register_static_info::(&mut registrator, &mut world.storages); // SAFETY: we initialized this bundle_id in `init_info` unsafe { Self::new_with_id(world, bundle_id, change_tick) } } @@ -2020,11 +2033,33 @@ impl Bundles { self.bundle_ids.get(&type_id).cloned() } + pub(crate) fn register_static_info( + &mut self, + components: &mut ComponentsRegistrator, + storages: &mut Storages, + ) -> BundleId { + let bundle_infos = &mut self.bundle_infos; + *self.bundle_ids.entry(TypeId::of::()).or_insert_with(|| { + let mut component_ids= Vec::new(); + T::component_ids(components, &mut |id| component_ids.push(id)); + let id = BundleId(bundle_infos.len()); + let bundle_info = + // SAFETY: T::component_id ensures: + // - its info was created + // - appropriate storage for it has been initialized. + // - it was created in the same order as the components in T + unsafe { BundleInfo::new(core::any::type_name::(), storages, components, component_ids, id) }; + bundle_infos.push(bundle_info); + id + }) + } + /// Registers a new [`BundleInfo`] for a statically known type. /// /// Also registers all the components in the bundle. pub(crate) fn register_info( &mut self, + #[expect(unused, reason = "will be used for dynamic bundles support")] bundle: &T, components: &mut ComponentsRegistrator, storages: &mut Storages, ) -> BundleId { @@ -2047,7 +2082,7 @@ impl Bundles { /// Registers a new [`BundleInfo`], which contains both explicit and required components for a statically known type. /// /// Also registers all the components in the bundle. - pub(crate) fn register_contributed_bundle_info( + pub(crate) fn register_contributed_bundle_info( &mut self, components: &mut ComponentsRegistrator, storages: &mut Storages, @@ -2055,7 +2090,7 @@ impl Bundles { if let Some(id) = self.contributed_bundle_ids.get(&TypeId::of::()).cloned() { id } else { - let explicit_bundle_id = self.register_info::(components, storages); + let explicit_bundle_id = self.register_static_info::(components, storages); // SAFETY: reading from `explicit_bundle_id` and creating new bundle in same time. Its valid because bundle hashmap allow this let id = unsafe { let (ptr, len) = { diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index d29c3db4283ca..cb0c51a6fbe52 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -1849,7 +1849,7 @@ impl<'w> EntityWorldMut<'w> { let location = self.location(); let change_tick = self.world.change_tick(); let mut bundle_inserter = - BundleInserter::new::(self.world, location.archetype_id, change_tick); + BundleInserter::new::(&bundle, self.world, location.archetype_id, change_tick); // SAFETY: location matches current entity. `T` matches `bundle_info` let (location, after_effect) = unsafe { bundle_inserter.insert( diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index fca9091dd5f96..8263d28715207 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -2437,7 +2437,7 @@ impl World { unsafe { ComponentsRegistrator::new(&mut self.components, &mut self.component_ids) }; let bundle_id = self .bundles - .register_info::(&mut registrator, &mut self.storages); + .register_static_info::(&mut registrator, &mut self.storages); let mut invalid_entities = Vec::::new(); let mut batch_iter = batch.into_iter(); @@ -3020,13 +3020,13 @@ impl World { /// This is largely equivalent to calling [`register_component`](Self::register_component) on each /// component in the bundle. #[inline] - pub fn register_bundle(&mut self) -> &BundleInfo { + pub fn register_bundle(&mut self) -> &BundleInfo { // SAFETY: These come from the same world. `Self.components_registrator` can't be used since we borrow other fields too. let mut registrator = unsafe { ComponentsRegistrator::new(&mut self.components, &mut self.component_ids) }; let id = self .bundles - .register_info::(&mut registrator, &mut self.storages); + .register_static_info::(&mut registrator, &mut self.storages); // SAFETY: We just initialized the bundle so its id should definitely be valid. unsafe { self.bundles.get(id).debug_checked_unwrap() } } From 07f6d1df90d49204a6c7f52c2e05036d175b9c98 Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Mon, 2 Jun 2025 16:14:21 +0200 Subject: [PATCH 07/20] Switch Entity{Ref,Mut}Except to StaticBundle --- crates/bevy_ecs/src/query/fetch.rs | 12 +++--- crates/bevy_ecs/src/query/iter.rs | 6 +-- crates/bevy_ecs/src/world/entity_ref.rs | 50 ++++++++++++------------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 0a13b6181974d..aafbe94e3386d 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -1,6 +1,6 @@ use crate::{ archetype::{Archetype, Archetypes}, - bundle::Bundle, + bundle::StaticBundle, change_detection::{MaybeLocation, Ticks, TicksMut}, component::{Component, ComponentId, Components, Mutable, StorageType, Tick}, entity::{Entities, Entity, EntityLocation}, @@ -1168,7 +1168,7 @@ unsafe impl<'a> QueryData for FilteredEntityMut<'a> { /// are rejected. unsafe impl<'a, B> WorldQuery for EntityRefExcept<'a, B> where - B: Bundle, + B: StaticBundle, { type Fetch<'w> = EntityFetch<'w>; type State = SmallVec<[ComponentId; 4]>; @@ -1243,7 +1243,7 @@ where /// SAFETY: `Self` is the same as `Self::ReadOnly`. unsafe impl<'a, B> QueryData for EntityRefExcept<'a, B> where - B: Bundle, + B: StaticBundle, { const IS_READ_ONLY: bool = true; type ReadOnly = Self; @@ -1271,14 +1271,14 @@ where /// SAFETY: `EntityRefExcept` enforces read-only access to its contained /// components. -unsafe impl<'a, B> ReadOnlyQueryData for EntityRefExcept<'a, B> where B: Bundle {} +unsafe impl<'a, B> ReadOnlyQueryData for EntityRefExcept<'a, B> where B: StaticBundle {} /// SAFETY: `EntityMutExcept` guards access to all components in the bundle `B` /// and populates `Access` values so that queries that conflict with this access /// are rejected. unsafe impl<'a, B> WorldQuery for EntityMutExcept<'a, B> where - B: Bundle, + B: StaticBundle, { type Fetch<'w> = EntityFetch<'w>; type State = SmallVec<[ComponentId; 4]>; @@ -1354,7 +1354,7 @@ where /// `EntityMutExcept` provides. unsafe impl<'a, B> QueryData for EntityMutExcept<'a, B> where - B: Bundle, + B: StaticBundle, { const IS_READ_ONLY: bool = false; type ReadOnly = EntityRefExcept<'a, B>; diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index eb49204434b6f..b2ea5eada7009 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -1,7 +1,7 @@ use super::{QueryData, QueryFilter, ReadOnlyQueryData}; use crate::{ archetype::{Archetype, ArchetypeEntity, Archetypes}, - bundle::Bundle, + bundle::StaticBundle, component::Tick, entity::{ContainsEntity, Entities, Entity, EntityEquivalent, EntitySet, EntitySetIterator}, query::{ArchetypeFilter, DebugCheckedUnwrap, QueryState, StorageId}, @@ -969,13 +969,13 @@ unsafe impl<'w, 's, F: QueryFilter> EntitySetIterator } // SAFETY: [`QueryIter`] is guaranteed to return every matching entity once and only once. -unsafe impl<'w, 's, F: QueryFilter, B: Bundle> EntitySetIterator +unsafe impl<'w, 's, F: QueryFilter, B: StaticBundle> EntitySetIterator for QueryIter<'w, 's, EntityRefExcept<'_, B>, F> { } // SAFETY: [`QueryIter`] is guaranteed to return every matching entity once and only once. -unsafe impl<'w, 's, F: QueryFilter, B: Bundle> EntitySetIterator +unsafe impl<'w, 's, F: QueryFilter, B: StaticBundle> EntitySetIterator for QueryIter<'w, 's, EntityMutExcept<'_, B>, F> { } diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index cb0c51a6fbe52..04b2f947b0811 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -2,7 +2,7 @@ use crate::{ archetype::Archetype, bundle::{ Bundle, BundleEffect, BundleFromComponents, BundleInserter, BundleRemover, DynamicBundle, - InsertMode, + InsertMode, StaticBundle, }, change_detection::{MaybeLocation, MutUntyped}, component::{ @@ -3471,7 +3471,7 @@ impl<'a> From<&'a EntityWorldMut<'_>> for FilteredEntityRef<'a> { } } -impl<'a, B: Bundle> From<&'a EntityRefExcept<'_, B>> for FilteredEntityRef<'a> { +impl<'a, B: StaticBundle> From<&'a EntityRefExcept<'_, B>> for FilteredEntityRef<'a> { fn from(value: &'a EntityRefExcept<'_, B>) -> Self { // SAFETY: // - The FilteredEntityRef has the same component access as the given EntityRefExcept. @@ -3819,7 +3819,7 @@ impl<'a> From<&'a mut EntityWorldMut<'_>> for FilteredEntityMut<'a> { } } -impl<'a, B: Bundle> From<&'a EntityMutExcept<'_, B>> for FilteredEntityMut<'a> { +impl<'a, B: StaticBundle> From<&'a EntityMutExcept<'_, B>> for FilteredEntityMut<'a> { fn from(value: &'a EntityMutExcept<'_, B>) -> Self { // SAFETY: // - The FilteredEntityMut has the same component access as the given EntityMutExcept. @@ -3893,7 +3893,7 @@ pub enum TryFromFilteredError { /// for an explicitly-enumerated set. pub struct EntityRefExcept<'w, B> where - B: Bundle, + B: StaticBundle, { entity: UnsafeEntityCell<'w>, phantom: PhantomData, @@ -3901,7 +3901,7 @@ where impl<'w, B> EntityRefExcept<'w, B> where - B: Bundle, + B: StaticBundle, { /// # Safety /// Other users of `UnsafeEntityCell` must only have mutable access to the components in `B`. @@ -4062,7 +4062,7 @@ where impl<'a, B> From<&'a EntityMutExcept<'_, B>> for EntityRefExcept<'a, B> where - B: Bundle, + B: StaticBundle, { fn from(entity: &'a EntityMutExcept<'_, B>) -> Self { // SAFETY: All accesses that `EntityRefExcept` provides are also @@ -4071,23 +4071,23 @@ where } } -impl Clone for EntityRefExcept<'_, B> { +impl Clone for EntityRefExcept<'_, B> { fn clone(&self) -> Self { *self } } -impl Copy for EntityRefExcept<'_, B> {} +impl Copy for EntityRefExcept<'_, B> {} -impl PartialEq for EntityRefExcept<'_, B> { +impl PartialEq for EntityRefExcept<'_, B> { fn eq(&self, other: &Self) -> bool { self.entity() == other.entity() } } -impl Eq for EntityRefExcept<'_, B> {} +impl Eq for EntityRefExcept<'_, B> {} -impl PartialOrd for EntityRefExcept<'_, B> { +impl PartialOrd for EntityRefExcept<'_, B> { /// [`EntityRefExcept`]'s comparison trait implementations match the underlying [`Entity`], /// and cannot discern between different worlds. fn partial_cmp(&self, other: &Self) -> Option { @@ -4095,26 +4095,26 @@ impl PartialOrd for EntityRefExcept<'_, B> { } } -impl Ord for EntityRefExcept<'_, B> { +impl Ord for EntityRefExcept<'_, B> { fn cmp(&self, other: &Self) -> Ordering { self.entity().cmp(&other.entity()) } } -impl Hash for EntityRefExcept<'_, B> { +impl Hash for EntityRefExcept<'_, B> { fn hash(&self, state: &mut H) { self.entity().hash(state); } } -impl ContainsEntity for EntityRefExcept<'_, B> { +impl ContainsEntity for EntityRefExcept<'_, B> { fn entity(&self) -> Entity { self.id() } } // SAFETY: This type represents one Entity. We implement the comparison traits based on that Entity. -unsafe impl EntityEquivalent for EntityRefExcept<'_, B> {} +unsafe impl EntityEquivalent for EntityRefExcept<'_, B> {} /// Provides mutable access to all components of an entity, with the exception /// of an explicit set. @@ -4126,7 +4126,7 @@ unsafe impl EntityEquivalent for EntityRefExcept<'_, B> {} /// [`crate::query::Without`] filter. pub struct EntityMutExcept<'w, B> where - B: Bundle, + B: StaticBundle, { entity: UnsafeEntityCell<'w>, phantom: PhantomData, @@ -4134,7 +4134,7 @@ where impl<'w, B> EntityMutExcept<'w, B> where - B: Bundle, + B: StaticBundle, { /// # Safety /// Other users of `UnsafeEntityCell` must not have access to any components not in `B`. @@ -4294,15 +4294,15 @@ where } } -impl PartialEq for EntityMutExcept<'_, B> { +impl PartialEq for EntityMutExcept<'_, B> { fn eq(&self, other: &Self) -> bool { self.entity() == other.entity() } } -impl Eq for EntityMutExcept<'_, B> {} +impl Eq for EntityMutExcept<'_, B> {} -impl PartialOrd for EntityMutExcept<'_, B> { +impl PartialOrd for EntityMutExcept<'_, B> { /// [`EntityMutExcept`]'s comparison trait implementations match the underlying [`Entity`], /// and cannot discern between different worlds. fn partial_cmp(&self, other: &Self) -> Option { @@ -4310,30 +4310,30 @@ impl PartialOrd for EntityMutExcept<'_, B> { } } -impl Ord for EntityMutExcept<'_, B> { +impl Ord for EntityMutExcept<'_, B> { fn cmp(&self, other: &Self) -> Ordering { self.entity().cmp(&other.entity()) } } -impl Hash for EntityMutExcept<'_, B> { +impl Hash for EntityMutExcept<'_, B> { fn hash(&self, state: &mut H) { self.entity().hash(state); } } -impl ContainsEntity for EntityMutExcept<'_, B> { +impl ContainsEntity for EntityMutExcept<'_, B> { fn entity(&self) -> Entity { self.id() } } // SAFETY: This type represents one Entity. We implement the comparison traits based on that Entity. -unsafe impl EntityEquivalent for EntityMutExcept<'_, B> {} +unsafe impl EntityEquivalent for EntityMutExcept<'_, B> {} fn bundle_contains_component(components: &Components, query_id: ComponentId) -> bool where - B: Bundle, + B: StaticBundle, { let mut found = false; B::get_component_ids(components, &mut |maybe_id| { From ddd69c56c46ab0d1ef717361083aa9a5209ef1cd Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Mon, 2 Jun 2025 16:24:28 +0200 Subject: [PATCH 08/20] Switch Observer and Trigger to StaticBundle --- crates/bevy_app/src/app.rs | 3 ++- crates/bevy_ecs/src/observer/mod.rs | 17 +++++++++-------- crates/bevy_ecs/src/observer/runner.rs | 7 ++++--- .../src/system/commands/entity_command.rs | 4 ++-- crates/bevy_ecs/src/system/commands/mod.rs | 6 +++--- crates/bevy_ecs/src/system/input.rs | 4 ++-- crates/bevy_ecs/src/system/observer_system.rs | 17 +++++++++-------- crates/bevy_ecs/src/world/entity_ref.rs | 4 ++-- 8 files changed, 33 insertions(+), 29 deletions(-) diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index af5183b159f6b..0702248f78ab1 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -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}, @@ -1340,7 +1341,7 @@ impl App { /// } /// }); /// ``` - pub fn add_observer( + pub fn add_observer( &mut self, observer: impl IntoObserverSystem, ) -> &mut Self { diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index b3a8b3b5cd2c7..06f1e600aa7a3 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -138,6 +138,7 @@ use variadics_please::all_tuples; use crate::{ archetype::ArchetypeFlags, + bundle::StaticBundle, change_detection::MaybeLocation, component::ComponentId, entity::EntityHashMap, @@ -159,7 +160,7 @@ use smallvec::SmallVec; /// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. It also /// contains event propagation information. See [`On::propagate`] for more information. /// -/// The generic `B: Bundle` is used to modify the further specialize the events that this observer is interested in. +/// The generic `B: StaticBundle` is used to modify the further specialize the events that this observer is interested in. /// The entity involved *does not* have to have these components, but the observer will only be /// triggered if the event matches the components in `B`. /// @@ -169,7 +170,7 @@ use smallvec::SmallVec; /// Providing multiple components in this bundle will cause this event to be triggered by any /// matching component in the bundle, /// [rather than requiring all of them to be present](https://github.com/bevyengine/bevy/issues/15325). -pub struct On<'w, E, B: Bundle = ()> { +pub struct On<'w, E, B: StaticBundle = ()> { event: &'w mut E, propagate: &'w mut bool, trigger: ObserverTrigger, @@ -180,7 +181,7 @@ pub struct On<'w, E, B: Bundle = ()> { #[deprecated(since = "0.17.0", note = "Renamed to `On`.")] pub type Trigger<'w, E, B = ()> = On<'w, E, B>; -impl<'w, E, B: Bundle> On<'w, E, B> { +impl<'w, E, B: StaticBundle> On<'w, E, B> { /// Creates a new instance of [`On`] for the given event and observer information. pub fn new(event: &'w mut E, propagate: &'w mut bool, trigger: ObserverTrigger) -> Self { Self { @@ -250,7 +251,7 @@ impl<'w, E, B: Bundle> On<'w, E, B> { } } -impl<'w, E: EntityEvent, B: Bundle> On<'w, E, B> { +impl<'w, E: EntityEvent, B: StaticBundle> On<'w, E, B> { /// Returns the [`Entity`] that was targeted by the `event` that triggered this observer. /// /// Note that if event propagation is enabled, this may not be the same as the original target of the event, @@ -294,7 +295,7 @@ impl<'w, E: EntityEvent, B: Bundle> On<'w, E, B> { } } -impl<'w, E: Debug, B: Bundle> Debug for On<'w, E, B> { +impl<'w, E: Debug, B: StaticBundle> Debug for On<'w, E, B> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("On") .field("event", &self.event) @@ -305,7 +306,7 @@ impl<'w, E: Debug, B: Bundle> Debug for On<'w, E, B> { } } -impl<'w, E, B: Bundle> Deref for On<'w, E, B> { +impl<'w, E, B: StaticBundle> Deref for On<'w, E, B> { type Target = E; fn deref(&self) -> &Self::Target { @@ -313,7 +314,7 @@ impl<'w, E, B: Bundle> Deref for On<'w, E, B> { } } -impl<'w, E, B: Bundle> DerefMut for On<'w, E, B> { +impl<'w, E, B: StaticBundle> DerefMut for On<'w, E, B> { fn deref_mut(&mut self) -> &mut Self::Target { self.event } @@ -767,7 +768,7 @@ impl World { /// # Panics /// /// Panics if the given system is an exclusive system. - pub fn add_observer( + pub fn add_observer( &mut self, system: impl IntoObserverSystem, ) -> EntityWorldMut { diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index d6bffd8f22ddf..49a50ccafd2af 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -3,6 +3,7 @@ use bevy_utils::prelude::DebugName; use core::any::Any; use crate::{ + bundle::StaticBundle, component::{ComponentId, Mutable, StorageType}, error::{ErrorContext, ErrorHandler}, lifecycle::{ComponentHook, HookContext}, @@ -209,7 +210,7 @@ impl Observer { /// # Panics /// /// Panics if the given system is an exclusive system. - pub fn new>(system: I) -> Self { + pub fn new>(system: I) -> Self { let system = Box::new(IntoObserverSystem::into_system(system)); assert!( !system.is_exclusive(), @@ -336,7 +337,7 @@ impl Component for Observer { } } -fn observer_system_runner>( +fn observer_system_runner>( mut world: DeferredWorld, observer_trigger: ObserverTrigger, ptr: PtrMut, @@ -438,7 +439,7 @@ impl AnyNamedSystem for T { /// The type parameters of this function _must_ match those used to create the [`Observer`]. /// As such, it is recommended to only use this function within the [`Observer::new`] method to /// ensure type parameters match. -fn hook_on_add>( +fn hook_on_add>( mut world: DeferredWorld<'_>, HookContext { entity, .. }: HookContext, ) { diff --git a/crates/bevy_ecs/src/system/commands/entity_command.rs b/crates/bevy_ecs/src/system/commands/entity_command.rs index 87bd2d858b27c..03b578b066484 100644 --- a/crates/bevy_ecs/src/system/commands/entity_command.rs +++ b/crates/bevy_ecs/src/system/commands/entity_command.rs @@ -8,7 +8,7 @@ use alloc::vec::Vec; use log::info; use crate::{ - bundle::{Bundle, InsertMode}, + bundle::{Bundle, InsertMode, StaticBundle}, change_detection::MaybeLocation, component::{Component, ComponentId, ComponentInfo}, entity::{Entity, EntityClonerBuilder}, @@ -218,7 +218,7 @@ pub fn despawn() -> impl EntityCommand { /// An [`EntityCommand`] that creates an [`Observer`](crate::observer::Observer) /// listening for events of type `E` targeting an entity #[track_caller] -pub fn observe( +pub fn observe( observer: impl IntoObserverSystem, ) -> impl EntityCommand { let caller = MaybeLocation::caller(); diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 84f9784228ff4..331c233af20e4 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -15,7 +15,7 @@ use core::marker::PhantomData; use crate::{ self as bevy_ecs, - bundle::{Bundle, InsertMode, NoBundleEffect}, + bundle::{Bundle, InsertMode, NoBundleEffect, StaticBundle}, change_detection::{MaybeLocation, Mut}, component::{Component, ComponentId, Mutable}, entity::{Entities, Entity, EntityClonerBuilder, EntityDoesNotExistError}, @@ -1114,7 +1114,7 @@ impl<'w, 's> Commands<'w, 's> { /// Panics if the given system is an exclusive system. /// /// [`On`]: crate::observer::On - pub fn add_observer( + pub fn add_observer( &mut self, observer: impl IntoObserverSystem, ) -> EntityCommands { @@ -1968,7 +1968,7 @@ impl<'a> EntityCommands<'a> { } /// Creates an [`Observer`] listening for events of type `E` targeting this entity. - pub fn observe( + pub fn observe( &mut self, observer: impl IntoObserverSystem, ) -> &mut Self { diff --git a/crates/bevy_ecs/src/system/input.rs b/crates/bevy_ecs/src/system/input.rs index cb75016ee93b4..336b1ac6bc213 100644 --- a/crates/bevy_ecs/src/system/input.rs +++ b/crates/bevy_ecs/src/system/input.rs @@ -2,7 +2,7 @@ use core::ops::{Deref, DerefMut}; use variadics_please::all_tuples; -use crate::{bundle::Bundle, prelude::On, system::System}; +use crate::{bundle::StaticBundle, prelude::On, system::System}; /// Trait for types that can be used as input to [`System`]s. /// @@ -222,7 +222,7 @@ impl<'i, T: ?Sized> DerefMut for InMut<'i, T> { /// Used for [`ObserverSystem`]s. /// /// [`ObserverSystem`]: crate::system::ObserverSystem -impl SystemInput for On<'_, E, B> { +impl SystemInput for On<'_, E, B> { type Param<'i> = On<'i, E, B>; type Inner<'i> = On<'i, E, B>; diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index 8e927d9529a32..cd253e4866c01 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -3,10 +3,11 @@ use bevy_utils::prelude::DebugName; use core::marker::PhantomData; use crate::{ + bundle::StaticBundle, component::{CheckChangeTicks, ComponentId, Tick}, error::Result, never::Never, - prelude::{Bundle, On}, + prelude::On, query::FilteredAccessSet, schedule::{Fallible, Infallible}, system::{input::SystemIn, System}, @@ -16,12 +17,12 @@ use crate::{ use super::{IntoSystem, SystemParamValidationError}; /// Implemented for [`System`]s that have [`On`] as the first argument. -pub trait ObserverSystem: +pub trait ObserverSystem: System, Out = Out> + Send + 'static { } -impl ObserverSystem for T where +impl ObserverSystem for T where T: System, Out = Out> + Send + 'static { } @@ -38,7 +39,7 @@ impl ObserverSystem for T where label = "the trait `IntoObserverSystem` is not implemented", note = "for function `ObserverSystem`s, ensure the first argument is `On` and any subsequent ones are `SystemParam`" )] -pub trait IntoObserverSystem: Send + 'static { +pub trait IntoObserverSystem: Send + 'static { /// The type of [`System`] that this instance converts into. type System: ObserverSystem; @@ -51,7 +52,7 @@ where S: IntoSystem, Out, M> + Send + 'static, S::System: ObserverSystem, E: 'static, - B: Bundle, + B: StaticBundle, { type System = S::System; @@ -65,7 +66,7 @@ where S: IntoSystem, (), M> + Send + 'static, S::System: ObserverSystem, E: Send + Sync + 'static, - B: Bundle, + B: StaticBundle, { type System = InfallibleObserverWrapper; @@ -77,7 +78,7 @@ impl IntoObserverSystem for S where S: IntoSystem, Never, M> + Send + 'static, E: Send + Sync + 'static, - B: Bundle, + B: StaticBundle, { type System = InfallibleObserverWrapper; @@ -106,7 +107,7 @@ impl System for InfallibleObserverWrapper where S: ObserverSystem, E: Send + Sync + 'static, - B: Bundle, + B: StaticBundle, Out: Send + Sync + 'static, { type In = On<'static, E, B>; diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 04b2f947b0811..a758ceb820ed1 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -2649,14 +2649,14 @@ impl<'w> EntityWorldMut<'w> { /// /// Panics if the given system is an exclusive system. #[track_caller] - pub fn observe( + pub fn observe( &mut self, observer: impl IntoObserverSystem, ) -> &mut Self { self.observe_with_caller(observer, MaybeLocation::caller()) } - pub(crate) fn observe_with_caller( + pub(crate) fn observe_with_caller( &mut self, observer: impl IntoObserverSystem, caller: MaybeLocation, From 9b0ab87373bd5cc3e144cf646f4945cb921e4a86 Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Mon, 2 Jun 2025 20:57:02 +0200 Subject: [PATCH 09/20] Require StaticBundle in ReflectBundle --- crates/bevy_ecs/src/reflect/bundle.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/reflect/bundle.rs b/crates/bevy_ecs/src/reflect/bundle.rs index 133591c405ebf..d08a9f65e42db 100644 --- a/crates/bevy_ecs/src/reflect/bundle.rs +++ b/crates/bevy_ecs/src/reflect/bundle.rs @@ -9,7 +9,7 @@ use bevy_utils::prelude::DebugName; use core::any::{Any, TypeId}; use crate::{ - bundle::BundleFromComponents, + bundle::{BundleFromComponents, StaticBundle}, entity::EntityMapper, prelude::Bundle, relationship::RelationshipHookMode, @@ -57,7 +57,7 @@ impl ReflectBundleFns { /// /// This is useful if you want to start with the default implementation before overriding some /// of the functions to create a custom implementation. - pub fn new() -> Self { + pub fn new() -> Self { >::from_type().0 } } @@ -148,7 +148,9 @@ impl ReflectBundle { } } -impl FromType for ReflectBundle { +impl FromType + for ReflectBundle +{ fn from_type() -> Self { ReflectBundle(ReflectBundleFns { insert: |entity, reflected_bundle, registry| { From 9a176a51715648889ac5f5b9ea34aaef43d3712c Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Mon, 2 Jun 2025 16:18:04 +0200 Subject: [PATCH 10/20] Switch EntityClonerBuilder to StaticBundle --- benches/benches/bevy_ecs/entity_cloning.rs | 8 ++++---- crates/bevy_ecs/src/entity/clone_entities.rs | 6 +++--- crates/bevy_ecs/src/system/commands/entity_command.rs | 4 ++-- crates/bevy_ecs/src/system/commands/mod.rs | 4 ++-- crates/bevy_ecs/src/world/entity_ref.rs | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/benches/benches/bevy_ecs/entity_cloning.rs b/benches/benches/bevy_ecs/entity_cloning.rs index 0eaae27ce4b00..2db72b834ba90 100644 --- a/benches/benches/bevy_ecs/entity_cloning.rs +++ b/benches/benches/bevy_ecs/entity_cloning.rs @@ -1,7 +1,7 @@ use core::hint::black_box; use benches::bench; -use bevy_ecs::bundle::Bundle; +use bevy_ecs::bundle::{Bundle, StaticBundle}; use bevy_ecs::component::ComponentCloneBehavior; use bevy_ecs::entity::EntityCloner; use bevy_ecs::hierarchy::ChildOf; @@ -53,7 +53,7 @@ type ComplexBundle = (C1, C2, C3, C4, C5, C6, C7, C8, C9, C10); /// Sets the [`ComponentCloneHandler`] for all explicit and required components in a bundle `B` to /// use the [`Reflect`] trait instead of [`Clone`]. -fn reflection_cloner( +fn reflection_cloner( world: &mut World, linked_cloning: bool, ) -> EntityCloner { @@ -92,7 +92,7 @@ fn reflection_cloner( /// components (which is usually [`ComponentCloneHandler::clone_handler()`]). If `clone_via_reflect` /// is true, it will overwrite the handler for all components in the bundle to be /// [`ComponentCloneHandler::reflect_handler()`]. -fn bench_clone( +fn bench_clone( b: &mut Bencher, clone_via_reflect: bool, ) { @@ -124,7 +124,7 @@ fn bench_clone( /// 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( +fn bench_clone_hierarchy( b: &mut Bencher, height: usize, children: usize, diff --git a/crates/bevy_ecs/src/entity/clone_entities.rs b/crates/bevy_ecs/src/entity/clone_entities.rs index 02d2491b7a6c3..776145be75eb5 100644 --- a/crates/bevy_ecs/src/entity/clone_entities.rs +++ b/crates/bevy_ecs/src/entity/clone_entities.rs @@ -7,7 +7,7 @@ use core::any::TypeId; use crate::{ archetype::Archetype, - bundle::Bundle, + bundle::StaticBundle, component::{Component, ComponentCloneBehavior, ComponentCloneFn, ComponentId, ComponentInfo}, entity::{hash_map::EntityHashMap, Entities, Entity, EntityMapper}, query::DebugCheckedUnwrap, @@ -681,7 +681,7 @@ impl<'w> EntityClonerBuilder<'w> { /// /// Note that all components are allowed by default, to clone only explicitly allowed components make sure to call /// [`deny_all`](`Self::deny_all`) before calling any of the `allow` methods. - pub fn allow(&mut self) -> &mut Self { + pub fn allow(&mut self) -> &mut Self { let bundle = self.world.register_bundle::(); let ids = bundle.explicit_components().to_owned(); for id in ids { @@ -722,7 +722,7 @@ impl<'w> EntityClonerBuilder<'w> { } /// Disallows all components of the bundle from being cloned. - pub fn deny(&mut self) -> &mut Self { + pub fn deny(&mut self) -> &mut Self { let bundle = self.world.register_bundle::(); let ids = bundle.explicit_components().to_owned(); for id in ids { diff --git a/crates/bevy_ecs/src/system/commands/entity_command.rs b/crates/bevy_ecs/src/system/commands/entity_command.rs index 03b578b066484..f31680a49729c 100644 --- a/crates/bevy_ecs/src/system/commands/entity_command.rs +++ b/crates/bevy_ecs/src/system/commands/entity_command.rs @@ -254,7 +254,7 @@ pub fn clone_with( /// An [`EntityCommand`] that clones the specified components of an entity /// and inserts them into another entity. -pub fn clone_components(target: Entity) -> impl EntityCommand { +pub fn clone_components(target: Entity) -> impl EntityCommand { move |mut entity: EntityWorldMut| { entity.clone_components::(target); } @@ -262,7 +262,7 @@ pub fn clone_components(target: Entity) -> impl EntityCommand { /// An [`EntityCommand`] that clones the specified components of an entity /// and inserts them into another entity, then removes them from the original entity. -pub fn move_components(target: Entity) -> impl EntityCommand { +pub fn move_components(target: Entity) -> impl EntityCommand { move |mut entity: EntityWorldMut| { entity.move_components::(target); } diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 331c233af20e4..88f4b5265c9fd 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -2109,7 +2109,7 @@ impl<'a> EntityCommands<'a> { /// # Panics /// /// The command will panic when applied if the target entity does not exist. - pub fn clone_components(&mut self, target: Entity) -> &mut Self { + pub fn clone_components(&mut self, target: Entity) -> &mut Self { self.queue(entity_command::clone_components::(target)) } @@ -2122,7 +2122,7 @@ impl<'a> EntityCommands<'a> { /// # Panics /// /// The command will panic when applied if the target entity does not exist. - pub fn move_components(&mut self, target: Entity) -> &mut Self { + pub fn move_components(&mut self, target: Entity) -> &mut Self { self.queue(entity_command::move_components::(target)) } } diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index a758ceb820ed1..47e7855021f03 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -2783,7 +2783,7 @@ impl<'w> EntityWorldMut<'w> { /// /// - If this entity has been despawned while this `EntityWorldMut` is still alive. /// - If the target entity does not exist. - pub fn clone_components(&mut self, target: Entity) -> &mut Self { + pub fn clone_components(&mut self, target: Entity) -> &mut Self { self.assert_not_despawned(); EntityCloner::build(self.world) @@ -2806,7 +2806,7 @@ impl<'w> EntityWorldMut<'w> { /// /// - If this entity has been despawned while this `EntityWorldMut` is still alive. /// - If the target entity does not exist. - pub fn move_components(&mut self, target: Entity) -> &mut Self { + pub fn move_components(&mut self, target: Entity) -> &mut Self { self.assert_not_despawned(); EntityCloner::build(self.world) From bd30a8b2199550ba536791deaa752f18f3f96d09 Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Mon, 2 Jun 2025 18:43:06 +0200 Subject: [PATCH 11/20] Require StaticBundle in batch spawning --- benches/benches/bevy_ecs/world/world_get.rs | 6 ++++-- crates/bevy_ecs/src/spawn.rs | 4 +++- .../bevy_ecs/src/system/commands/command.rs | 6 +++--- crates/bevy_ecs/src/system/commands/mod.rs | 10 +++++----- crates/bevy_ecs/src/world/mod.rs | 20 +++++++++---------- crates/bevy_ecs/src/world/spawn_batch.rs | 18 ++++++++--------- 6 files changed, 34 insertions(+), 30 deletions(-) diff --git a/benches/benches/bevy_ecs/world/world_get.rs b/benches/benches/bevy_ecs/world/world_get.rs index e6e2a0bb903ef..23f1dd9b9271c 100644 --- a/benches/benches/bevy_ecs/world/world_get.rs +++ b/benches/benches/bevy_ecs/world/world_get.rs @@ -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}, @@ -37,7 +37,9 @@ fn setup(entity_count: u32) -> World { black_box(world) } -fn setup_wide + Default>(entity_count: u32) -> World { +fn setup_wide + StaticBundle + Default>( + entity_count: u32, +) -> World { let mut world = World::default(); world.spawn_batch((0..entity_count).map(|_| T::default())); black_box(world) diff --git a/crates/bevy_ecs/src/spawn.rs b/crates/bevy_ecs/src/spawn.rs index 1c27927061cf9..f75888f8e7886 100644 --- a/crates/bevy_ecs/src/spawn.rs +++ b/crates/bevy_ecs/src/spawn.rs @@ -46,7 +46,9 @@ pub trait SpawnableList { fn size_hint(&self) -> usize; } -impl> SpawnableList for Vec { +impl + StaticBundle> SpawnableList + for Vec +{ fn spawn(self, world: &mut World, entity: Entity) { let mapped_bundles = self.into_iter().map(|b| (R::from(entity), b)); world.spawn_batch(mapped_bundles); diff --git a/crates/bevy_ecs/src/system/commands/command.rs b/crates/bevy_ecs/src/system/commands/command.rs index f3fc677e471b1..de371205bf80d 100644 --- a/crates/bevy_ecs/src/system/commands/command.rs +++ b/crates/bevy_ecs/src/system/commands/command.rs @@ -5,7 +5,7 @@ //! [`Commands`](crate::system::Commands). use crate::{ - bundle::{Bundle, InsertMode, NoBundleEffect}, + bundle::{Bundle, InsertMode, NoBundleEffect, StaticBundle}, change_detection::MaybeLocation, entity::Entity, error::Result, @@ -70,7 +70,7 @@ where pub fn spawn_batch(bundles_iter: I) -> impl Command where I: IntoIterator + Send + Sync + 'static, - I::Item: Bundle, + I::Item: Bundle + StaticBundle, { let caller = MaybeLocation::caller(); move |world: &mut World| { @@ -88,7 +88,7 @@ where pub fn insert_batch(batch: I, insert_mode: InsertMode) -> impl Command where I: IntoIterator + Send + Sync + 'static, - B: Bundle, + B: Bundle + StaticBundle, { let caller = MaybeLocation::caller(); move |world: &mut World| -> Result { diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 88f4b5265c9fd..93e31dfb064e3 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -533,7 +533,7 @@ impl<'w, 's> Commands<'w, 's> { pub fn spawn_batch(&mut self, batch: I) where I: IntoIterator + Send + Sync + 'static, - I::Item: Bundle, + I::Item: Bundle + StaticBundle, { self.queue(command::spawn_batch(batch)); } @@ -681,7 +681,7 @@ impl<'w, 's> Commands<'w, 's> { pub fn insert_batch(&mut self, batch: I) where I: IntoIterator + Send + Sync + 'static, - B: Bundle, + B: Bundle + StaticBundle, { self.queue(command::insert_batch(batch, InsertMode::Replace)); } @@ -712,7 +712,7 @@ impl<'w, 's> Commands<'w, 's> { pub fn insert_batch_if_new(&mut self, batch: I) where I: IntoIterator + Send + Sync + 'static, - B: Bundle, + B: Bundle + StaticBundle, { self.queue(command::insert_batch(batch, InsertMode::Keep)); } @@ -742,7 +742,7 @@ impl<'w, 's> Commands<'w, 's> { pub fn try_insert_batch(&mut self, batch: I) where I: IntoIterator + Send + Sync + 'static, - B: Bundle, + B: Bundle + StaticBundle, { self.queue(command::insert_batch(batch, InsertMode::Replace).handle_error_with(warn)); } @@ -773,7 +773,7 @@ impl<'w, 's> Commands<'w, 's> { pub fn try_insert_batch_if_new(&mut self, batch: I) where I: IntoIterator + Send + Sync + 'static, - B: Bundle, + B: Bundle + StaticBundle, { self.queue(command::insert_batch(batch, InsertMode::Keep).handle_error_with(warn)); } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 8263d28715207..83a6aeec8fc0a 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -40,7 +40,7 @@ use crate::{ archetype::{ArchetypeId, Archetypes}, bundle::{ Bundle, BundleEffect, BundleInfo, BundleInserter, BundleSpawner, Bundles, InsertMode, - NoBundleEffect, + NoBundleEffect, StaticBundle, }, change_detection::{MaybeLocation, MutUntyped, TicksMut}, component::{ @@ -1163,7 +1163,7 @@ impl World { self.flush(); let change_tick = self.change_tick(); let entity = self.entities.alloc(); - let mut bundle_spawner = BundleSpawner::new::(self, change_tick); + let mut bundle_spawner = BundleSpawner::new(&bundle, self, change_tick); // SAFETY: bundle's type matches `bundle_info`, entity is allocated but non-existent let (entity_location, after_effect) = unsafe { bundle_spawner.spawn_non_existent(entity, bundle, caller) }; @@ -1229,7 +1229,7 @@ impl World { pub fn spawn_batch(&mut self, iter: I) -> SpawnBatchIter<'_, I::IntoIter> where I: IntoIterator, - I::Item: Bundle, + I::Item: Bundle + StaticBundle, { SpawnBatchIter::new(self, iter.into_iter(), MaybeLocation::caller()) } @@ -2234,7 +2234,7 @@ impl World { where I: IntoIterator, I::IntoIter: Iterator, - B: Bundle, + B: Bundle + StaticBundle, { self.insert_batch_with_caller(batch, InsertMode::Replace, MaybeLocation::caller()); } @@ -2259,7 +2259,7 @@ impl World { where I: IntoIterator, I::IntoIter: Iterator, - B: Bundle, + B: Bundle + StaticBundle, { self.insert_batch_with_caller(batch, InsertMode::Keep, MaybeLocation::caller()); } @@ -2278,7 +2278,7 @@ impl World { ) where I: IntoIterator, I::IntoIter: Iterator, - B: Bundle, + B: Bundle + StaticBundle, { struct InserterArchetypeCache<'w> { inserter: BundleInserter<'w>, @@ -2292,7 +2292,7 @@ impl World { unsafe { ComponentsRegistrator::new(&mut self.components, &mut self.component_ids) }; let bundle_id = self .bundles - .register_info::(&mut registrator, &mut self.storages); + .register_static_info::(&mut registrator, &mut self.storages); let mut batch_iter = batch.into_iter(); @@ -2377,7 +2377,7 @@ impl World { where I: IntoIterator, I::IntoIter: Iterator, - B: Bundle, + B: Bundle + StaticBundle, { self.try_insert_batch_with_caller(batch, InsertMode::Replace, MaybeLocation::caller()) } @@ -2399,7 +2399,7 @@ impl World { where I: IntoIterator, I::IntoIter: Iterator, - B: Bundle, + B: Bundle + StaticBundle, { self.try_insert_batch_with_caller(batch, InsertMode::Keep, MaybeLocation::caller()) } @@ -2423,7 +2423,7 @@ impl World { where I: IntoIterator, I::IntoIter: Iterator, - B: Bundle, + B: Bundle + StaticBundle, { struct InserterArchetypeCache<'w> { inserter: BundleInserter<'w>, diff --git a/crates/bevy_ecs/src/world/spawn_batch.rs b/crates/bevy_ecs/src/world/spawn_batch.rs index 16bd9bb8059b4..c2bbd8c0d05fd 100644 --- a/crates/bevy_ecs/src/world/spawn_batch.rs +++ b/crates/bevy_ecs/src/world/spawn_batch.rs @@ -1,5 +1,5 @@ use crate::{ - bundle::{Bundle, BundleSpawner, NoBundleEffect}, + bundle::{Bundle, BundleSpawner, NoBundleEffect, StaticBundle}, change_detection::MaybeLocation, entity::{Entity, EntitySetIterator}, world::World, @@ -13,7 +13,7 @@ use core::iter::FusedIterator; pub struct SpawnBatchIter<'w, I> where I: Iterator, - I::Item: Bundle, + I::Item: Bundle + StaticBundle, { inner: I, spawner: BundleSpawner<'w>, @@ -23,7 +23,7 @@ where impl<'w, I> SpawnBatchIter<'w, I> where I: Iterator, - I::Item: Bundle, + I::Item: Bundle + StaticBundle, { #[inline] #[track_caller] @@ -38,7 +38,7 @@ where let length = upper.unwrap_or(lower); world.entities.reserve(length as u32); - let mut spawner = BundleSpawner::new::(world, change_tick); + let mut spawner = BundleSpawner::new_static::(world, change_tick); spawner.reserve_storage(length); Self { @@ -52,7 +52,7 @@ where impl Drop for SpawnBatchIter<'_, I> where I: Iterator, - I::Item: Bundle, + I::Item: Bundle + StaticBundle, { fn drop(&mut self) { // Iterate through self in order to spawn remaining bundles. @@ -66,7 +66,7 @@ where impl Iterator for SpawnBatchIter<'_, I> where I: Iterator, - I::Item: Bundle, + I::Item: Bundle + StaticBundle, { type Item = Entity; @@ -84,7 +84,7 @@ where impl ExactSizeIterator for SpawnBatchIter<'_, I> where I: ExactSizeIterator, - T: Bundle, + T: Bundle + StaticBundle, { fn len(&self) -> usize { self.inner.len() @@ -94,7 +94,7 @@ where impl FusedIterator for SpawnBatchIter<'_, I> where I: FusedIterator, - T: Bundle, + T: Bundle + StaticBundle, { } @@ -102,6 +102,6 @@ where unsafe impl EntitySetIterator for SpawnBatchIter<'_, I> where I: FusedIterator, - T: Bundle, + T: Bundle + StaticBundle, { } From 28a21bdaf39455b8bb270ffb22eaf1a08fdadfa2 Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Mon, 2 Jun 2025 18:40:20 +0200 Subject: [PATCH 12/20] Switch component removal to StaticBundle --- crates/bevy_ecs/src/relationship/related_methods.rs | 6 +++--- .../bevy_ecs/src/system/commands/entity_command.rs | 4 ++-- crates/bevy_ecs/src/system/commands/mod.rs | 13 ++++++++----- crates/bevy_ecs/src/world/entity_ref.rs | 13 ++++++++----- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/crates/bevy_ecs/src/relationship/related_methods.rs b/crates/bevy_ecs/src/relationship/related_methods.rs index 1983b6b37c11b..7b6619c319fd1 100644 --- a/crates/bevy_ecs/src/relationship/related_methods.rs +++ b/crates/bevy_ecs/src/relationship/related_methods.rs @@ -1,5 +1,5 @@ use crate::{ - bundle::Bundle, + bundle::{Bundle, StaticBundle}, entity::{hash_set::EntityHashSet, Entity}, prelude::Children, relationship::{ @@ -339,7 +339,7 @@ impl<'w> EntityWorldMut<'w> { /// /// This method should only be called on relationships that form a tree-like structure. /// Any cycles will cause this method to loop infinitely. - pub fn remove_recursive(&mut self) -> &mut Self { + pub fn remove_recursive(&mut self) -> &mut Self { self.remove::(); if let Some(relationship_target) = self.get::() { let related_vec: Vec = relationship_target.iter().collect(); @@ -500,7 +500,7 @@ impl<'a> EntityCommands<'a> { /// /// This method should only be called on relationships that form a tree-like structure. /// Any cycles will cause this method to loop infinitely. - pub fn remove_recursive(&mut self) -> &mut Self { + pub fn remove_recursive(&mut self) -> &mut Self { self.queue(move |mut entity: EntityWorldMut| { entity.remove_recursive::(); }) diff --git a/crates/bevy_ecs/src/system/commands/entity_command.rs b/crates/bevy_ecs/src/system/commands/entity_command.rs index f31680a49729c..b2cc0cdb721a6 100644 --- a/crates/bevy_ecs/src/system/commands/entity_command.rs +++ b/crates/bevy_ecs/src/system/commands/entity_command.rs @@ -154,7 +154,7 @@ pub fn insert_from_world(mode: InsertMode) -> impl Ent /// An [`EntityCommand`] that removes the components in a [`Bundle`] from an entity. #[track_caller] -pub fn remove() -> impl EntityCommand { +pub fn remove() -> impl EntityCommand { let caller = MaybeLocation::caller(); move |mut entity: EntityWorldMut| { entity.remove_with_caller::(caller); @@ -164,7 +164,7 @@ pub fn remove() -> impl EntityCommand { /// An [`EntityCommand`] that removes the components in a [`Bundle`] from an entity, /// as well as the required components for each component removed. #[track_caller] -pub fn remove_with_requires() -> impl EntityCommand { +pub fn remove_with_requires() -> impl EntityCommand { let caller = MaybeLocation::caller(); move |mut entity: EntityWorldMut| { entity.remove_with_requires_with_caller::(caller); diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 93e31dfb064e3..b0c757dc07552 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1622,7 +1622,7 @@ impl<'a> EntityCommands<'a> { /// # bevy_ecs::system::assert_is_system(remove_combat_stats_system); /// ``` #[track_caller] - pub fn remove(&mut self) -> &mut Self { + pub fn remove(&mut self) -> &mut Self { self.queue_handled(entity_command::remove::(), warn) } @@ -1658,7 +1658,7 @@ impl<'a> EntityCommands<'a> { /// # bevy_ecs::system::assert_is_system(remove_combat_stats_system); /// ``` #[track_caller] - pub fn remove_if(&mut self, condition: impl FnOnce() -> bool) -> &mut Self { + pub fn remove_if(&mut self, condition: impl FnOnce() -> bool) -> &mut Self { if condition() { self.remove::() } else { @@ -1675,7 +1675,10 @@ impl<'a> EntityCommands<'a> { /// If the entity does not exist when this command is executed, /// the resulting error will be ignored. #[track_caller] - pub fn try_remove_if(&mut self, condition: impl FnOnce() -> bool) -> &mut Self { + pub fn try_remove_if( + &mut self, + condition: impl FnOnce() -> bool, + ) -> &mut Self { if condition() { self.try_remove::() } else { @@ -1723,7 +1726,7 @@ impl<'a> EntityCommands<'a> { /// } /// # bevy_ecs::system::assert_is_system(remove_combat_stats_system); /// ``` - pub fn try_remove(&mut self) -> &mut Self { + pub fn try_remove(&mut self) -> &mut Self { self.queue_handled(entity_command::remove::(), ignore) } @@ -1755,7 +1758,7 @@ impl<'a> EntityCommands<'a> { /// # bevy_ecs::system::assert_is_system(remove_with_requires_system); /// ``` #[track_caller] - pub fn remove_with_requires(&mut self) -> &mut Self { + pub fn remove_with_requires(&mut self) -> &mut Self { self.queue(entity_command::remove_with_requires::()) } diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 47e7855021f03..bb40ed83d7d0e 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -2007,7 +2007,7 @@ impl<'w> EntityWorldMut<'w> { /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[must_use] #[track_caller] - pub fn take(&mut self) -> Option { + pub fn take(&mut self) -> Option { let location = self.location(); let entity = self.entity; @@ -2063,12 +2063,15 @@ impl<'w> EntityWorldMut<'w> { /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[track_caller] - pub fn remove(&mut self) -> &mut Self { + pub fn remove(&mut self) -> &mut Self { self.remove_with_caller::(MaybeLocation::caller()) } #[inline] - pub(crate) fn remove_with_caller(&mut self, caller: MaybeLocation) -> &mut Self { + pub(crate) fn remove_with_caller( + &mut self, + caller: MaybeLocation, + ) -> &mut Self { let location = self.location(); let Some(mut remover) = @@ -2100,11 +2103,11 @@ impl<'w> EntityWorldMut<'w> { /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[track_caller] - pub fn remove_with_requires(&mut self) -> &mut Self { + pub fn remove_with_requires(&mut self) -> &mut Self { self.remove_with_requires_with_caller::(MaybeLocation::caller()) } - pub(crate) fn remove_with_requires_with_caller( + pub(crate) fn remove_with_requires_with_caller( &mut self, caller: MaybeLocation, ) -> &mut Self { From c33033a5e7de4cea87dce5a25cb2f277b3f7e4ea Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Mon, 2 Jun 2025 18:44:49 +0200 Subject: [PATCH 13/20] Switch EntityWorldMut::retain to StaticBundle --- crates/bevy_ecs/src/system/commands/entity_command.rs | 4 ++-- crates/bevy_ecs/src/system/commands/mod.rs | 2 +- crates/bevy_ecs/src/world/entity_ref.rs | 9 ++++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ecs/src/system/commands/entity_command.rs b/crates/bevy_ecs/src/system/commands/entity_command.rs index b2cc0cdb721a6..f9eb516f0c32a 100644 --- a/crates/bevy_ecs/src/system/commands/entity_command.rs +++ b/crates/bevy_ecs/src/system/commands/entity_command.rs @@ -190,9 +190,9 @@ pub fn clear() -> impl EntityCommand { } /// An [`EntityCommand`] that removes all components from an entity, -/// except for those in the given [`Bundle`]. +/// except for those in the given [`StaticBundle`]. #[track_caller] -pub fn retain() -> impl EntityCommand { +pub fn retain() -> impl EntityCommand { let caller = MaybeLocation::caller(); move |mut entity: EntityWorldMut| { entity.retain_with_caller::(caller); diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index b0c757dc07552..ebfde2b602d54 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1943,7 +1943,7 @@ impl<'a> EntityCommands<'a> { /// # bevy_ecs::system::assert_is_system(remove_combat_stats_system); /// ``` #[track_caller] - pub fn retain(&mut self) -> &mut Self { + pub fn retain(&mut self) -> &mut Self { self.queue(entity_command::retain::()) } diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index bb40ed83d7d0e..71e2bf89dc5ad 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -2151,12 +2151,15 @@ impl<'w> EntityWorldMut<'w> { /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[track_caller] - pub fn retain(&mut self) -> &mut Self { + pub fn retain(&mut self) -> &mut Self { self.retain_with_caller::(MaybeLocation::caller()) } #[inline] - pub(crate) fn retain_with_caller(&mut self, caller: MaybeLocation) -> &mut Self { + pub(crate) fn retain_with_caller( + &mut self, + caller: MaybeLocation, + ) -> &mut Self { let old_location = self.location(); let archetypes = &mut self.world.archetypes; let storages = &mut self.world.storages; @@ -2168,7 +2171,7 @@ impl<'w> EntityWorldMut<'w> { let retained_bundle = self .world .bundles - .register_info::(&mut registrator, storages); + .register_static_info::(&mut registrator, storages); // SAFETY: `retained_bundle` exists as we just initialized it. let retained_bundle_info = unsafe { self.world.bundles.get_unchecked(retained_bundle) }; let old_archetype = &mut archetypes[old_location.archetype_id]; From a44cf372b4b75f84d2df26c4f990f5b6745e2e3e Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Mon, 2 Jun 2025 21:08:47 +0200 Subject: [PATCH 14/20] Require StaticBundle in ExtractComponent --- crates/bevy_render/src/extract_component.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_render/src/extract_component.rs b/crates/bevy_render/src/extract_component.rs index b7bb05e425d55..7e05d0ed16eff 100644 --- a/crates/bevy_render/src/extract_component.rs +++ b/crates/bevy_render/src/extract_component.rs @@ -8,7 +8,7 @@ use crate::{ }; use bevy_app::{App, Plugin}; use bevy_ecs::{ - bundle::NoBundleEffect, + bundle::{NoBundleEffect, StaticBundle}, component::Component, prelude::*, query::{QueryFilter, QueryItem, ReadOnlyQueryData}, @@ -54,7 +54,7 @@ pub trait ExtractComponent: Component { /// /// `Out` has a [`Bundle`] trait bound instead of a [`Component`] trait bound in order to allow use cases /// such as tuples of components as output. - type Out: Bundle; + type Out: Bundle + StaticBundle; // TODO: https://github.com/rust-lang/rust/issues/29661 // type Out: Component = Self; From e683a10d74b6bb69c0bb6cdac3774539d95213c5 Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Tue, 3 Jun 2025 21:26:05 +0200 Subject: [PATCH 15/20] Add support for #[bundle(dynamic)] attribute --- crates/bevy_ecs/macros/src/lib.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index f6ba72f10c110..49f62351d6aa9 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -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, } } } @@ -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 { @@ -139,7 +146,7 @@ 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 = quote! { + 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; @@ -168,7 +175,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { #(<#active_field_types as #ecs_path::bundle::StaticBundle>::register_required_components(components, &mut *required_components);)* } } - }; + }); let bundle_impl = quote! { // SAFETY: @@ -215,7 +222,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)] From 4a0f69eb89e7265186d354ea2f181f5ef24bd2c0 Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Fri, 20 Jun 2025 08:05:57 +0200 Subject: [PATCH 16/20] Add migration guide --- .../migration-guides/static-bundle-split.md | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 release-content/migration-guides/static-bundle-split.md diff --git a/release-content/migration-guides/static-bundle-split.md b/release-content/migration-guides/static-bundle-split.md new file mode 100644 index 0000000000000..3e9fcf112609b --- /dev/null +++ b/release-content/migration-guides/static-bundle-split.md @@ -0,0 +1,55 @@ +--- +title: StaticBundle split off from Bundle +pull_requests: [19491] +--- + +The `StaticBundle` trait has been split off from the `Bundle` trait to avoid conflating the concept of a type whose values can be inserted into an entity (`Bundle`) with the concept of a statically known set of components (`StaticBundle`). This required the update of existing APIs that were using `Bundle` as a statically known set of components to use `StaticBundle` instead. + +Changes for most users will be zero or pretty minimal, since `#[derive(Bundle)]` will automatically derive `StaticBundle` and most types that implemented `Bundle` will now also implement `StaticBundle`. The main exception will be generic APIs or types, which now will need to update or add a bound on `StaticBundle`. For example: + +```rs +// 0.16 +#[derive(Bundle)] +struct MyBundleWrapper { + inner: T +} + +fn my_register_bundle(world: &mut World) { + world.register_bundle::(); +} + + +// 0.17 +#[derive(Bundle)] +struct MyBundleWrapper { // Add a StaticBundle bound + inner: T +} + +fn my_register_bundle(world: &mut World) { // Replace Bundle with StaticBundle + world.register_bundle::(); +} +``` + +The following APIs now require the `StaticBundle` trait instead of the `Bundle` trait: + +- `World::register_bundle`, which has been renamed to `World::register_static_bundle` +- the `B` type parameter of `EntityRefExcept` and `EntityMutExcept` +- `EntityClonerBuilder::allow` and `EntityClonerBuilder::deny` +- `EntityCommands::clone_components` and `EntityCommands::move_components` +- `EntityWorldMut::clone_components` and `EntityWorldMut::move_components` +- the `B` type parameter of `IntoObserverSystem`, `Trigger`, `App::add_observer`, `World::add_observer`, `Observer::new`, `Commands::add_observer`, `EntityCommands::observe` and `EntityWorldMut::observe` +- `EntityWorldMut::remove_recursive` and `Commands::remove_recursive` +- `EntityCommands::remove`, `EntityCommands::remove_if`, `EntityCommands::try_remove_if`, `EntityCommands::try_remove`, `EntityCommands::remove_with_requires`, `EntityWorldMut::remove` and `EntityWorldMut::remove_with_requires` +- `EntityWorldMut::take` +- `EntityWorldMut::retain` and `EntityCommands::retain` + +The following APIs now require the `StaticBundle` trait in addition to the `Bundle` trait: + +- `Commands::spawn_batch`, `Commands::insert_batch`, `Commands::insert_batch_if_new`, `Commands::try_insert_batch`, `Commands::try_insert_batch_if_new`, `bevy::ecs::command::spawn_batch`, `bevy::ecs::command::insert_batch`, `World::spawn_batch`, `World::insert_batch`, `World::insert_batch_if_new`, `World::try_insert_batch` and `World::try_insert_batch_if_new` +- `ReflectBundle::new`, `impl FromType` for `ReflectBundle` and `#[reflect(Bundle)]` +- `ExtractComponent::Out` + +Moreover, some APIs have been renamed: + +- `World::register_bundle` has been renamed to `World::register_static_bundle` +- the `DynamicBundle` trait has been renamed to `ComponentsFromBundle` From ef88ad85cd8eec543a153c55f53cc6254252b5f9 Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Sat, 21 Jun 2025 10:52:14 +0200 Subject: [PATCH 17/20] Add &self parameter to Bundle methods --- crates/bevy_ecs/macros/src/lib.rs | 9 ++-- crates/bevy_ecs/src/bundle.rs | 57 +++++++++++++++++----- crates/bevy_ecs/src/lib.rs | 81 +++++++++++++++++++++++++++---- crates/bevy_ecs/src/spawn.rs | 6 +++ 4 files changed, 129 insertions(+), 24 deletions(-) diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 49f62351d6aa9..05e35036e9ff6 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -185,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);)* } } }; diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index bd29888e6ff61..ce59ec43d1fcf 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -187,6 +187,8 @@ use variadics_please::all_tuples; // bundle, in the _exact_ order that [`DynamicBundle::get_components`] is called. // - [`Bundle::from_components`] must call `func` exactly once for each [`ComponentId`] returned by // [`Bundle::component_ids`]. +// - [`Bundle::component_ids`], [`Bundle::get_component_ids`] and [`Bundle::register_required_components`] +// cannot depend on `self` for now. #[diagnostic::on_unimplemented( message = "`{Self}` is not a `Bundle`", label = "invalid `Bundle`", @@ -195,13 +197,18 @@ use variadics_please::all_tuples; pub unsafe trait Bundle: DynamicBundle + Send + Sync + 'static { /// Gets this [`Bundle`]'s component ids, in the order of this bundle's [`Component`]s #[doc(hidden)] - fn component_ids(components: &mut ComponentsRegistrator, ids: &mut impl FnMut(ComponentId)); + fn component_ids( + &self, + components: &mut ComponentsRegistrator, + ids: &mut impl FnMut(ComponentId), + ); /// Gets this [`Bundle`]'s component ids. This will be [`None`] if the component has not been registered. - fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option)); + fn get_component_ids(&self, components: &Components, ids: &mut impl FnMut(Option)); /// Registers components that are required by the components in this [`Bundle`]. fn register_required_components( + &self, _components: &mut ComponentsRegistrator, _required_components: &mut RequiredComponents, ); @@ -337,15 +344,24 @@ unsafe impl StaticBundle for C { // - `component_ids` calls `ids` for C's component id (and nothing else) // - `get_components` is called exactly once for C and passes the component's storage type based on its associated constant. unsafe impl Bundle for C { - fn component_ids(components: &mut ComponentsRegistrator, ids: &mut impl FnMut(ComponentId)) { + fn component_ids( + &self, + components: &mut ComponentsRegistrator, + ids: &mut impl FnMut(ComponentId), + ) { ::component_ids(components, ids); } - fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option)) { + fn get_component_ids( + &self, + components: &Components, + ids: &mut impl FnMut(Option), + ) { ::get_component_ids(components, ids); } fn register_required_components( + &self, components: &mut ComponentsRegistrator, required_components: &mut RequiredComponents, ) { @@ -426,19 +442,38 @@ macro_rules! tuple_impl { // - `Bundle::get_components` is called exactly once for each member. Relies on the above implementation to pass the correct // `StorageType` into the callback. unsafe impl<$($name: Bundle),*> Bundle for ($($name,)*) { - fn component_ids(components: &mut ComponentsRegistrator, ids: &mut impl FnMut(ComponentId)){ - $(<$name as Bundle>::component_ids(components, ids);)* + fn component_ids(&self, components: &mut ComponentsRegistrator, ids: &mut impl FnMut(ComponentId)){ + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($name,)*) = self; + + $(<$name as Bundle>::component_ids($name, components, ids);)* } - fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option)){ - $(<$name as Bundle>::get_component_ids(components, ids);)* + fn get_component_ids(&self, components: &Components, ids: &mut impl FnMut(Option)){ + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($name,)*) = self; + + $(<$name as Bundle>::get_component_ids($name, components, ids);)* } fn register_required_components( + &self, components: &mut ComponentsRegistrator, required_components: &mut RequiredComponents, ) { - $(<$name as Bundle>::register_required_components(components, required_components);)* + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($name,)*) = self; + + $(<$name as Bundle>::register_required_components($name, components, required_components);)* } } @@ -2059,14 +2094,14 @@ impl Bundles { /// Also registers all the components in the bundle. pub(crate) fn register_info( &mut self, - #[expect(unused, reason = "will be used for dynamic bundles support")] bundle: &T, + bundle: &T, components: &mut ComponentsRegistrator, storages: &mut Storages, ) -> BundleId { let bundle_infos = &mut self.bundle_infos; *self.bundle_ids.entry(TypeId::of::()).or_insert_with(|| { let mut component_ids= Vec::new(); - T::component_ids(components, &mut |id| component_ids.push(id)); + bundle.component_ids(components, &mut |id| component_ids.push(id)); let id = BundleId(bundle_infos.len()); let bundle_info = // SAFETY: T::component_id ensures: diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index e5f0e908e56c5..c0fe3df0f6b03 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -145,7 +145,7 @@ pub struct HotPatched; #[cfg(test)] mod tests { use crate::{ - bundle::Bundle, + bundle::{Bundle, StaticBundle}, change_detection::Ref, component::{Component, ComponentId, RequiredComponents, RequiredComponentsError}, entity::{Entity, EntityMapper}, @@ -241,11 +241,18 @@ mod tests { x: TableStored, y: SparseStored, } - let mut ids = Vec::new(); - ::component_ids(&mut world.components_registrator(), &mut |id| { - ids.push(id); - }); + let mut ids = Vec::new(); + ::component_ids( + &FooBundle { + x: TableStored("abc"), + y: SparseStored(123), + }, + &mut world.components_registrator(), + &mut |id| { + ids.push(id); + }, + ); assert_eq!( ids, &[ @@ -254,6 +261,21 @@ mod tests { ] ); + let mut static_ids = Vec::new(); + ::component_ids( + &mut world.components_registrator(), + &mut |id| { + static_ids.push(id); + }, + ); + assert_eq!( + static_ids, + &[ + world.register_component::(), + world.register_component::(), + ] + ); + let e1 = world .spawn(FooBundle { x: TableStored("abc"), @@ -292,10 +314,20 @@ mod tests { } let mut ids = Vec::new(); - ::component_ids(&mut world.components_registrator(), &mut |id| { - ids.push(id); - }); - + ::component_ids( + &NestedBundle { + a: A(1), + foo: FooBundle { + x: TableStored("ghi"), + y: SparseStored(789), + }, + b: B(2), + }, + &mut world.components_registrator(), + &mut |id| { + ids.push(id); + }, + ); assert_eq!( ids, &[ @@ -306,6 +338,23 @@ mod tests { ] ); + let mut static_ids = Vec::new(); + ::component_ids( + &mut world.components_registrator(), + &mut |id| { + static_ids.push(id); + }, + ); + assert_eq!( + static_ids, + &[ + world.register_component::(), + world.register_component::(), + world.register_component::(), + world.register_component::(), + ] + ); + let e3 = world .spawn(NestedBundle { a: A(1), @@ -345,13 +394,25 @@ mod tests { let mut ids = Vec::new(); ::component_ids( + &BundleWithIgnored { + c: C, + ignored: Ignored, + }, &mut world.components_registrator(), &mut |id| { ids.push(id); }, ); + assert_eq!(ids, &[world.register_component::()]); - assert_eq!(ids, &[world.register_component::(),]); + let mut static_ids = Vec::new(); + ::component_ids( + &mut world.components_registrator(), + &mut |id| { + static_ids.push(id); + }, + ); + assert_eq!(static_ids, &[world.register_component::()]); let e4 = world .spawn(BundleWithIgnored { diff --git a/crates/bevy_ecs/src/spawn.rs b/crates/bevy_ecs/src/spawn.rs index f75888f8e7886..3e01727b4a594 100644 --- a/crates/bevy_ecs/src/spawn.rs +++ b/crates/bevy_ecs/src/spawn.rs @@ -218,6 +218,7 @@ unsafe impl + Send + Sync + 'static> Bundle for SpawnRelatedBundle { fn component_ids( + &self, components: &mut crate::component::ComponentsRegistrator, ids: &mut impl FnMut(crate::component::ComponentId), ) { @@ -225,6 +226,7 @@ unsafe impl + Send + Sync + 'static> Bundle } fn get_component_ids( + &self, components: &crate::component::Components, ids: &mut impl FnMut(Option), ) { @@ -232,6 +234,7 @@ unsafe impl + Send + Sync + 'static> Bundle } fn register_required_components( + &self, components: &mut crate::component::ComponentsRegistrator, required_components: &mut crate::component::RequiredComponents, ) { @@ -308,6 +311,7 @@ unsafe impl StaticBundle for SpawnOneRelated { // SAFETY: This internally relies on the RelationshipTarget's Bundle implementation, which is sound. unsafe impl Bundle for SpawnOneRelated { fn component_ids( + &self, components: &mut crate::component::ComponentsRegistrator, ids: &mut impl FnMut(crate::component::ComponentId), ) { @@ -315,6 +319,7 @@ unsafe impl Bundle for SpawnOneRelated { } fn get_component_ids( + &self, components: &crate::component::Components, ids: &mut impl FnMut(Option), ) { @@ -322,6 +327,7 @@ unsafe impl Bundle for SpawnOneRelated { } fn register_required_components( + &self, components: &mut crate::component::ComponentsRegistrator, required_components: &mut crate::component::RequiredComponents, ) { From 5942b7af2260ad9c5f1f94cfbbd33e8d3835304f Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Sat, 21 Jun 2025 11:01:37 +0200 Subject: [PATCH 18/20] Fix warning --- crates/bevy_ecs/src/bundle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index ce59ec43d1fcf..8e3cd2c1e7605 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -1189,7 +1189,7 @@ impl<'w> BundleInserter<'w> { let bundle_id = world .bundles - .register_info::(&bundle, &mut registrator, &mut world.storages); + .register_info::(bundle, &mut registrator, &mut world.storages); // SAFETY: We just ensured this bundle exists unsafe { Self::new_with_id(world, archetype_id, bundle_id, change_tick) } } From 9711258657990a3e4ece1640cf2c93836a4a9ba2 Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Mon, 23 Jun 2025 19:05:56 +0200 Subject: [PATCH 19/20] Apply suggestions --- crates/bevy_ecs/src/bundle.rs | 10 ++++++---- .../migration-guides/static-bundle-split.md | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 8e3cd2c1e7605..61610cdd89d8a 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -81,12 +81,13 @@ use bevy_utils::TypeIdMap; use core::{any::TypeId, ptr::NonNull}; use variadics_please::all_tuples; +/// A collection of components, whose identity may or may not be fixed at compile time. +/// /// The `Bundle` trait enables insertion of [`Component`]s to an entity. /// For the removal of [`Component`]s from an entity see the [`StaticBundle`]`trait`. /// /// Implementers of the `Bundle` trait are called 'bundles'. /// -/// Each bundle represents a possibly dynamic set of [`Component`] types. /// Currently, bundles can only contain one of each [`Component`], and will /// panic once initialized if this is not met. /// @@ -214,10 +215,10 @@ pub unsafe trait Bundle: DynamicBundle + Send + Sync + 'static { ); } -/// Each bundle represents a static and fixed set of [`Component`] types. +/// A static and fixed set of [`Component`] types. /// See the [`Bundle`] trait for a possibly dynamic set of [`Component`] types. /// -/// Implementers of the `Bundle` trait are called 'static bundles'. +/// Implementers of the [`StaticBundle`] trait are called 'static bundles'. /// /// ## Removal /// @@ -233,7 +234,8 @@ pub unsafe trait Bundle: DynamicBundle + Send + Sync + 'static { /// Manual implementations of this trait are unsupported. /// That is, there is no safe way to implement this trait, and you must not do so. /// If you want a type to implement [`StaticBundle`], you must use [`derive@Bundle`](derive@Bundle). -// Some safety points: +// +// (bevy internal doc) Some safety points: // - [`StaticBundle::component_ids`] and [`StaticBundle::get_component_ids`] must match the behavior of [`Bundle::component_ids`] #[diagnostic::on_unimplemented( message = "`{Self}` is not a `StaticBundle`", diff --git a/release-content/migration-guides/static-bundle-split.md b/release-content/migration-guides/static-bundle-split.md index 3e9fcf112609b..35ff7f2f52d4c 100644 --- a/release-content/migration-guides/static-bundle-split.md +++ b/release-content/migration-guides/static-bundle-split.md @@ -1,5 +1,5 @@ --- -title: StaticBundle split off from Bundle +title: \`StaticBundle` has been split off from `Bundle` pull_requests: [19491] --- From bbac5b1892f8f3ad67d4fc61730be128b345f7a2 Mon Sep 17 00:00:00 2001 From: Giacomo Stevanato Date: Tue, 15 Jul 2025 07:42:04 +0200 Subject: [PATCH 20/20] Fix docs --- crates/bevy_ecs/src/bundle/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/bundle/mod.rs b/crates/bevy_ecs/src/bundle/mod.rs index de22d8f033c5e..26395e25c8540 100644 --- a/crates/bevy_ecs/src/bundle/mod.rs +++ b/crates/bevy_ecs/src/bundle/mod.rs @@ -191,7 +191,7 @@ use bevy_ptr::OwningPtr; note = "consider annotating `{Self}` with `#[derive(Component)]` or `#[derive(Bundle)]`" )] pub unsafe trait Bundle: DynamicBundle + Send + Sync + 'static { - /// Gets this [`Bundle`]'s component ids, in the order of this bundle's [`Component`]s + /// Gets this [`Bundle`]'s component ids, in the order of this bundle's [`Component`](crate::component::Component)s fn component_ids( &self, components: &mut ComponentsRegistrator, @@ -221,6 +221,8 @@ pub unsafe trait Bundle: DynamicBundle + Send + Sync + 'static { /// Manual implementations of this trait are unsupported. /// That is, there is no safe way to implement this trait, and you must not do so. /// If you want a type to implement [`StaticBundle`], you must use [`derive@Bundle`](derive@Bundle). +/// +/// [`Component`]: crate::component::Component // // (bevy internal doc) Some safety points: // - [`StaticBundle::component_ids`] and [`StaticBundle::get_component_ids`] must match the behavior of [`Bundle::component_ids`]