diff --git a/benches/benches/bevy_ecs/world/world_get.rs b/benches/benches/bevy_ecs/world/world_get.rs index fcb9b0116b..283b984186 100644 --- a/benches/benches/bevy_ecs/world/world_get.rs +++ b/benches/benches/bevy_ecs/world/world_get.rs @@ -1,7 +1,7 @@ use core::hint::black_box; use bevy_ecs::{ - bundle::Bundle, + bundle::{Bundle, NoBundleEffect}, component::Component, entity::Entity, system::{Query, SystemState}, @@ -36,7 +36,7 @@ fn setup(entity_count: u32) -> World { black_box(world) } -fn setup_wide(entity_count: u32) -> World { +fn setup_wide + 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/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 2c06ad1fe6..e97149ec04 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -100,7 +100,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { self.#field.get_components(&mut *func); }); field_from_components.push(quote! { - #field: <#field_type as #ecs_path::bundle::Bundle>::from_components(ctx, &mut *func), + #field: <#field_type as #ecs_path::bundle::BundleFromComponents>::from_components(ctx, &mut *func), }); } None => { @@ -109,7 +109,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { self.#index.get_components(&mut *func); }); field_from_components.push(quote! { - #index: <#field_type as #ecs_path::bundle::Bundle>::from_components(ctx, &mut *func), + #index: <#field_type as #ecs_path::bundle::BundleFromComponents>::from_components(ctx, &mut *func), }); } } @@ -128,7 +128,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { TokenStream::from(quote! { // SAFETY: - // - ComponentId is returned in field-definition-order. [from_components] and [get_components] use field-definition-order + // - ComponentId is returned in field-definition-order. [get_components] uses field-definition-order // - `Bundle::get_components` is exactly once for each member. Rely's on the Component -> Bundle implementation to properly pass // the correct `StorageType` into the callback. unsafe impl #impl_generics #ecs_path::bundle::Bundle for #struct_name #ty_generics #where_clause { @@ -146,6 +146,17 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { #(#field_get_component_ids)* } + fn register_required_components( + components: &mut #ecs_path::component::Components, + required_components: &mut #ecs_path::component::RequiredComponents + ){ + #(#field_required_components)* + } + } + + // SAFETY: + // - ComponentId is returned in field-definition-order. [from_components] uses field-definition-order + unsafe impl #impl_generics #ecs_path::bundle::BundleFromComponents for #struct_name #ty_generics #where_clause { #[allow(unused_variables, non_snake_case)] unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self where @@ -155,16 +166,10 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { #(#field_from_components)* } } - - fn register_required_components( - components: &mut #ecs_path::component::Components, - required_components: &mut #ecs_path::component::RequiredComponents - ){ - #(#field_required_components)* - } } impl #impl_generics #ecs_path::bundle::DynamicBundle for #struct_name #ty_generics #where_clause { + type Effect = (); #[allow(unused_variables)] #[inline] fn get_components( diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 1a33999cd5..285e42d4e6 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -18,7 +18,7 @@ use crate::{ prelude::World, query::DebugCheckedUnwrap, storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow}, - world::{unsafe_world_cell::UnsafeWorldCell, ON_ADD, ON_INSERT, ON_REPLACE}, + world::{unsafe_world_cell::UnsafeWorldCell, EntityWorldMut, ON_ADD, ON_INSERT, ON_REPLACE}, }; use alloc::{boxed::Box, vec, vec::Vec}; use bevy_platform_support::collections::{HashMap, HashSet}; @@ -156,6 +156,28 @@ pub unsafe trait Bundle: DynamicBundle + Send + Sync + 'static { /// 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)); + /// Registers components that are required by the components in this [`Bundle`]. + fn register_required_components( + _components: &mut Components, + _required_components: &mut RequiredComponents, + ); +} + +/// Creates a [`Bundle`] by taking it from internal storage. +/// +/// # 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 [`Bundle`], you must use [`derive@Bundle`](derive@Bundle). +/// +/// [`Query`]: crate::system::Query +// Some safety points: +// - [`Bundle::component_ids`] must return the [`ComponentId`] for each component type in the +// 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`]. +pub unsafe trait BundleFromComponents { /// Calls `func`, which should return data for each component in the bundle, in the order of /// this bundle's [`Component`]s /// @@ -168,16 +190,12 @@ pub unsafe trait Bundle: DynamicBundle + Send + Sync + 'static { // Ensure that the `OwningPtr` is used correctly F: for<'a> FnMut(&'a mut T) -> OwningPtr<'a>, Self: Sized; - - /// Registers components that are required by the components in this [`Bundle`]. - fn register_required_components( - _components: &mut Components, - _required_components: &mut RequiredComponents, - ); } /// The parts from [`Bundle`] that don't require statically knowing the components of the bundle. pub trait DynamicBundle { + /// An operation on the entity that happens _after_ inserting this bundle. + type Effect: BundleEffect; // SAFETY: // The `StorageType` argument passed into [`Bundle::get_components`] must be correct for the // component being fetched. @@ -185,29 +203,30 @@ pub trait DynamicBundle { /// Calls `func` on each value, in the order of this bundle's [`Component`]s. This passes /// ownership of the component values to `func`. #[doc(hidden)] - fn get_components(self, func: &mut impl FnMut(StorageType, OwningPtr<'_>)); + fn get_components(self, func: &mut impl FnMut(StorageType, OwningPtr<'_>)) -> Self::Effect; +} + +/// An operation on an [`Entity`] that occurs _after_ inserting the [`Bundle`] that defined this bundle effect. +/// The order of operations is: +/// +/// 1. The [`Bundle`] is inserted on the entity +/// 2. Relevant Hooks are run for the insert, then Observers +/// 3. The [`BundleEffect`] is run. +/// +/// See [`DynamicBundle::Effect`]. +pub trait BundleEffect { + /// Applies this effect to the given `entity`. + fn apply(self, entity: &mut EntityWorldMut); } // 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. -// - `Bundle::from_components` calls `func` exactly once for C, which is the exact value returned by `Bundle::component_ids`. unsafe impl Bundle for C { fn component_ids(components: &mut Components, ids: &mut impl FnMut(ComponentId)) { ids(components.register_component::()); } - unsafe fn from_components(ctx: &mut T, func: &mut F) -> Self - where - // Ensure that the `OwningPtr` is used correctly - F: for<'a> FnMut(&'a mut T) -> OwningPtr<'a>, - Self: Sized, - { - let ptr = func(ctx); - // Safety: The id given in `component_ids` is for `Self` - unsafe { ptr.read() } - } - fn register_required_components( components: &mut Components, required_components: &mut RequiredComponents, @@ -227,9 +246,25 @@ unsafe impl Bundle for C { } } +// SAFETY: +// - `Bundle::from_components` calls `func` exactly once for C, which is the exact value returned by `Bundle::component_ids`. +unsafe impl BundleFromComponents for C { + unsafe fn from_components(ctx: &mut T, func: &mut F) -> Self + where + // Ensure that the `OwningPtr` is used correctly + F: for<'a> FnMut(&'a mut T) -> OwningPtr<'a>, + Self: Sized, + { + let ptr = func(ctx); + // Safety: The id given in `component_ids` is for `Self` + unsafe { ptr.read() } + } +} + impl DynamicBundle for C { + type Effect = (); #[inline] - fn get_components(self, func: &mut impl FnMut(StorageType, OwningPtr<'_>)) { + fn get_components(self, func: &mut impl FnMut(StorageType, OwningPtr<'_>)) -> Self::Effect { OwningPtr::make(self, |ptr| func(C::STORAGE_TYPE, ptr)); } } @@ -261,23 +296,6 @@ macro_rules! tuple_impl { $(<$name as Bundle>::get_component_ids(components, ids);)* } - #[allow( - clippy::unused_unit, - reason = "Zero-length tuples will generate a function body equivalent to `()`; however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case." - )] - unsafe fn from_components(ctx: &mut T, func: &mut F) -> Self - where - F: FnMut(&mut T) -> OwningPtr<'_> - { - #[allow( - unused_unsafe, - reason = "Zero-length tuples will not run anything in the unsafe block. Additionally, rewriting this to move the () outside of the unsafe would require putting the safety comment inside the tuple, hurting readability of the code." - )] - // SAFETY: Rust guarantees that tuple calls are evaluated 'left to right'. - // https://doc.rust-lang.org/reference/expressions.html#evaluation-order-of-operands - unsafe { ($(<$name as Bundle>::from_components(ctx, func),)*) } - } - fn register_required_components( components: &mut Components, required_components: &mut RequiredComponents, @@ -286,6 +304,41 @@ macro_rules! tuple_impl { } } + #[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: + // - `Bundle::component_ids` calls `ids` for each component type in the + // bundle, in the exact order that `DynamicBundle::get_components` is called. + // - `Bundle::from_components` calls `func` exactly once for each `ComponentId` returned by `Bundle::component_ids`. + // - `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: BundleFromComponents),*> BundleFromComponents for ($($name,)*) { + #[allow( + clippy::unused_unit, + reason = "Zero-length tuples will generate a function body equivalent to `()`; however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case." + )] + unsafe fn from_components(ctx: &mut T, func: &mut F) -> Self + where + F: FnMut(&mut T) -> OwningPtr<'_> + { + #[allow( + unused_unsafe, + reason = "Zero-length tuples will not run anything in the unsafe block. Additionally, rewriting this to move the () outside of the unsafe would require putting the safety comment inside the tuple, hurting readability of the code." + )] + // SAFETY: Rust guarantees that tuple calls are evaluated 'left to right'. + // https://doc.rust-lang.org/reference/expressions.html#evaluation-order-of-operands + unsafe { ($(<$name as BundleFromComponents>::from_components(ctx, func),)*) } + } + } + #[expect( clippy::allow_attributes, reason = "This is a tuple-related macro; as such, the lints below may not always apply." @@ -297,16 +350,21 @@ macro_rules! tuple_impl { )] $(#[$meta])* impl<$($name: Bundle),*> DynamicBundle for ($($name,)*) { + type Effect = ($($name::Effect,)*); + #[allow( + clippy::unused_unit, + reason = "Zero-length tuples will generate a function body equivalent to `()`; however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case." + )] #[inline(always)] - fn get_components(self, func: &mut impl FnMut(StorageType, OwningPtr<'_>)) { + fn get_components(self, func: &mut impl FnMut(StorageType, OwningPtr<'_>)) -> Self::Effect { #[allow( non_snake_case, reason = "The names of these variables are provided by the caller, not by us." )] let ($(mut $name,)*) = self; - $( - $name.get_components(&mut *func); - )* + ($( + $name.get_components(&mut *func), + )*) } } } @@ -320,6 +378,37 @@ all_tuples!( B ); +/// A trait implemented for [`BundleEffect`] implementations that do nothing. This is used as a type constraint for +/// [`Bundle`] APIs that do not / cannot run [`DynamicBundle::Effect`], such as "batch spawn" APIs. +pub trait NoBundleEffect {} + +macro_rules! after_effect_impl { + ($($after_effect: ident),*) => { + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such, the lints below may not always apply." + )] + impl<$($after_effect: BundleEffect),*> BundleEffect for ($($after_effect,)*) { + #[allow( + clippy::unused_unit, + reason = "Zero-length tuples will generate a function body equivalent to `()`; however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case.") + ] + fn apply(self, _entity: &mut EntityWorldMut) { + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($after_effect,)*) = self; + $($after_effect.apply(_entity);)* + } + } + + impl<$($after_effect: NoBundleEffect),*> NoBundleEffect for ($($after_effect,)*) { } + } +} + +all_tuples!(after_effect_impl, 0, 15, P); + /// For a specific [`World`], this stores a unique value identifying a type of a registered [`Bundle`]. /// /// [`World`]: crate::world::World @@ -535,11 +624,11 @@ impl BundleInfo { bundle: T, insert_mode: InsertMode, #[cfg(feature = "track_location")] caller: &'static Location<'static>, - ) { + ) -> T::Effect { // NOTE: get_components calls this closure on each component in "bundle order". // bundle_info.component_ids are also in "bundle order" let mut bundle_component = 0; - bundle.get_components(&mut |storage_type, component_ptr| { + let after_effect = bundle.get_components(&mut |storage_type, component_ptr| { let component_id = *self.component_ids.get_unchecked(bundle_component); match storage_type { StorageType::Table => { @@ -598,6 +687,8 @@ impl BundleInfo { caller, ); } + + after_effect } /// Internal method to initialize a required component from an [`OwningPtr`]. This should ultimately be called @@ -1037,7 +1128,7 @@ impl<'w> BundleInserter<'w> { bundle: T, insert_mode: InsertMode, #[cfg(feature = "track_location")] caller: &'static Location<'static>, - ) -> EntityLocation { + ) -> (EntityLocation, T::Effect) { let bundle_info = self.bundle_info.as_ref(); let archetype_after_insert = self.archetype_after_insert.as_ref(); let archetype = self.archetype.as_ref(); @@ -1074,7 +1165,7 @@ impl<'w> BundleInserter<'w> { // so this reference can only be promoted from shared to &mut down here, after they have been ran let archetype = self.archetype.as_mut(); - let (new_archetype, new_location) = match &mut self.archetype_move_type { + let (new_archetype, new_location, after_effect) = match &mut self.archetype_move_type { ArchetypeMoveType::SameArchetype => { // SAFETY: Mutable references do not alias and will be dropped after this block let sparse_sets = { @@ -1082,7 +1173,7 @@ impl<'w> BundleInserter<'w> { &mut world.storages.sparse_sets }; - bundle_info.write_components( + let after_effect = bundle_info.write_components( table, sparse_sets, archetype_after_insert, @@ -1096,7 +1187,7 @@ impl<'w> BundleInserter<'w> { caller, ); - (archetype, location) + (archetype, location, after_effect) } ArchetypeMoveType::NewArchetypeSameTable { new_archetype } => { let new_archetype = new_archetype.as_mut(); @@ -1124,7 +1215,7 @@ impl<'w> BundleInserter<'w> { } let new_location = new_archetype.allocate(entity, result.table_row); entities.set(entity.index(), new_location); - bundle_info.write_components( + let after_effect = bundle_info.write_components( table, sparse_sets, archetype_after_insert, @@ -1138,7 +1229,7 @@ impl<'w> BundleInserter<'w> { caller, ); - (new_archetype, new_location) + (new_archetype, new_location, after_effect) } ArchetypeMoveType::NewArchetypeNewTable { new_archetype, @@ -1207,7 +1298,7 @@ impl<'w> BundleInserter<'w> { } } - bundle_info.write_components( + let after_effect = bundle_info.write_components( new_table, sparse_sets, archetype_after_insert, @@ -1221,7 +1312,7 @@ impl<'w> BundleInserter<'w> { caller, ); - (new_archetype, new_location) + (new_archetype, new_location, after_effect) } }; @@ -1291,7 +1382,7 @@ impl<'w> BundleInserter<'w> { } } - new_location + (new_location, after_effect) } #[inline] @@ -1366,10 +1457,10 @@ impl<'w> BundleSpawner<'w> { entity: Entity, bundle: T, #[cfg(feature = "track_location")] caller: &'static Location<'static>, - ) -> EntityLocation { + ) -> (EntityLocation, T::Effect) { // SAFETY: We do not make any structural changes to the archetype graph through self.world so these pointers always remain valid let bundle_info = self.bundle_info.as_ref(); - let location = { + let (location, after_effect) = { let table = self.table.as_mut(); let archetype = self.archetype.as_mut(); @@ -1380,7 +1471,7 @@ impl<'w> BundleSpawner<'w> { }; let table_row = table.allocate(entity); let location = archetype.allocate(entity, table_row); - bundle_info.write_components( + let after_effect = bundle_info.write_components( table, sparse_sets, &SpawnBundleStatus, @@ -1394,7 +1485,7 @@ impl<'w> BundleSpawner<'w> { caller, ); entities.set(entity.index(), location); - location + (location, after_effect) }; // SAFETY: We have no outstanding mutable references to world as they were dropped @@ -1438,7 +1529,7 @@ impl<'w> BundleSpawner<'w> { } }; - location + (location, after_effect) } /// # Safety @@ -1448,18 +1539,18 @@ impl<'w> BundleSpawner<'w> { &mut self, bundle: T, #[cfg(feature = "track_location")] caller: &'static Location<'static>, - ) -> Entity { + ) -> (Entity, T::Effect) { let entity = self.entities().alloc(); // SAFETY: entity is allocated (but non-existent), `T` matches this BundleInfo's type - unsafe { + let (_, after_effect) = unsafe { self.spawn_non_existent( entity, bundle, #[cfg(feature = "track_location")] caller, - ); - } - entity + ) + }; + (entity, after_effect) } #[inline] diff --git a/crates/bevy_ecs/src/hierarchy.rs b/crates/bevy_ecs/src/hierarchy.rs index 2f34a2e504..6c8ee9f31b 100644 --- a/crates/bevy_ecs/src/hierarchy.rs +++ b/crates/bevy_ecs/src/hierarchy.rs @@ -277,12 +277,49 @@ pub fn validate_parent_has_component( } } +/// Returns a [`SpawnRelatedBundle`] that will insert the [`Children`] component, spawn a [`SpawnableList`] of entities with given bundles that +/// relate to the [`Children`] entity via the [`ChildOf`] component, and reserve space in the [`Children`] for each spawned entity. +/// +/// Any additional arguments will be interpreted as bundles to be spawned. +/// +/// Also see [`related`](crate::related) for a version of this that works with any [`RelationshipTarget`] type. +/// +/// ``` +/// # use bevy_ecs::hierarchy::Children; +/// # use bevy_ecs::name::Name; +/// # use bevy_ecs::world::World; +/// # use bevy_ecs::children; +/// # use bevy_ecs::spawn::{Spawn, SpawnRelated}; +/// let mut world = World::new(); +/// world.spawn(( +/// Name::new("Root"), +/// children![ +/// Name::new("Child1"), +/// ( +/// Name::new("Child2"), +/// children![Name::new("Grandchild")] +/// ) +/// ] +/// )); +/// ``` +/// +/// [`RelationshipTarget`]: crate::relationship::RelationshipTarget +/// [`SpawnRelatedBundle`]: crate::spawn::SpawnRelatedBundle +/// [`SpawnableList`]: crate::spawn::SpawnableList +#[macro_export] +macro_rules! children { + [$($child:expr),*$(,)?] => { + $crate::hierarchy::Children::spawn(($($crate::spawn::Spawn($child)),*)) + }; +} + #[cfg(test)] mod tests { use crate::{ entity::Entity, hierarchy::{ChildOf, Children}, relationship::RelationshipTarget, + spawn::{Spawn, SpawnRelated}, world::World, }; use alloc::{vec, vec::Vec}; @@ -435,4 +472,11 @@ mod tests { "ChildOf should still be there" ); } + + #[test] + fn spawn_children() { + let mut world = World::new(); + let id = world.spawn(Children::spawn((Spawn(()), Spawn(())))).id(); + assert_eq!(world.entity(id).get::().unwrap().len(), 2,); + } } diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 299ffe7b76..26b888f230 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -58,6 +58,7 @@ pub mod removal_detection; pub mod resource; pub mod result; pub mod schedule; +pub mod spawn; pub mod storage; pub mod system; pub mod traversal; @@ -77,6 +78,7 @@ pub mod prelude { pub use crate::{ bundle::Bundle, change_detection::{DetectChanges, DetectChangesMut, Mut, Ref}, + children, component::{require, Component}, entity::{Entity, EntityBorrow, EntityMapper}, event::{Event, EventMutator, EventReader, EventWriter, Events}, @@ -84,6 +86,8 @@ pub mod prelude { name::{Name, NameOrEntity}, observer::{Observer, Trigger}, query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, + related, + relationship::RelationshipTarget, removal_detection::RemovedComponents, resource::Resource, result::{Error, Result}, @@ -91,6 +95,7 @@ pub mod prelude { apply_deferred, common_conditions::*, ApplyDeferred, Condition, IntoSystemConfigs, IntoSystemSet, IntoSystemSetConfigs, Schedule, Schedules, SystemSet, }, + spawn::{Spawn, SpawnRelated}, system::{ Command, Commands, Deferred, EntityCommand, EntityCommands, In, InMut, InRef, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Populated, Query, ReadOnlySystem, diff --git a/crates/bevy_ecs/src/reflect/bundle.rs b/crates/bevy_ecs/src/reflect/bundle.rs index baa6bc7d08..b7acf69d6a 100644 --- a/crates/bevy_ecs/src/reflect/bundle.rs +++ b/crates/bevy_ecs/src/reflect/bundle.rs @@ -8,6 +8,7 @@ use alloc::boxed::Box; use core::any::{Any, TypeId}; use crate::{ + bundle::BundleFromComponents, entity::EntityMapper, prelude::Bundle, world::{EntityMut, EntityWorldMut}, @@ -49,7 +50,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 } } @@ -139,7 +140,7 @@ impl ReflectBundle { } } -impl FromType for ReflectBundle { +impl FromType for ReflectBundle { fn from_type() -> Self { ReflectBundle(ReflectBundleFns { insert: |entity, reflected_bundle, registry| { diff --git a/crates/bevy_ecs/src/spawn.rs b/crates/bevy_ecs/src/spawn.rs new file mode 100644 index 0000000000..2fb04c4c7b --- /dev/null +++ b/crates/bevy_ecs/src/spawn.rs @@ -0,0 +1,351 @@ +//! Entity spawning abstractions, largely focused on spawning related hierarchies of entities. See [`related`](crate::related) and [`SpawnRelated`] +//! for the best entry points into these APIs and examples of how to use them. + +use crate::{ + bundle::{Bundle, BundleEffect, DynamicBundle}, + entity::Entity, + relationship::{RelatedSpawner, Relationship, RelationshipTarget}, + world::{EntityWorldMut, World}, +}; +use core::marker::PhantomData; +use variadics_please::all_tuples; + +/// A wrapper over a [`Bundle`] indicating that an entity should be spawned with that [`Bundle`]. +/// This is intended to be used for hierarchical spawning via traits like [`SpawnableList`] and [`SpawnRelated`]. +/// +/// Also see the [`children`](crate::children) and [`related`](crate::related) macros that abstract over the [`Spawn`] API. +/// +/// ``` +/// # use bevy_ecs::hierarchy::Children; +/// # use bevy_ecs::spawn::{Spawn, SpawnRelated}; +/// # use bevy_ecs::name::Name; +/// # use bevy_ecs::world::World; +/// let mut world = World::new(); +/// world.spawn(( +/// Name::new("Root"), +/// Children::spawn(( +/// Spawn(Name::new("Child1")), +/// Spawn(( +/// Name::new("Child2"), +/// Children::spawn(Spawn(Name::new("Grandchild"))), +/// )) +/// )), +/// )); +/// ``` +pub struct Spawn(pub B); + +/// A spawn-able list of changes to a given [`World`] and relative to a given [`Entity`]. This is generally used +/// for spawning "related" entities, such as children. +pub trait SpawnableList { + /// Spawn this list of changes in a given [`World`] and relative to a given [`Entity`]. This is generally used + /// for spawning "related" entities, such as children. + fn spawn(self, world: &mut World, entity: Entity); + /// Returns a size hint, which is used to reserve space for this list in a [`RelationshipTarget`]. This should be + /// less than or equal to the actual size of the list. When in doubt, just use 0. + fn size_hint(&self) -> usize; +} + +impl SpawnableList for Spawn { + fn spawn(self, world: &mut World, entity: Entity) { + world.spawn((R::from(entity), self.0)); + } + + fn size_hint(&self) -> usize { + 1 + } +} + +/// A [`SpawnableList`] that spawns entities using an iterator of a given [`Bundle`]: +/// +/// ``` +/// # use bevy_ecs::hierarchy::Children; +/// # use bevy_ecs::spawn::{Spawn, SpawnIter, SpawnRelated}; +/// # use bevy_ecs::name::Name; +/// # use bevy_ecs::world::World; +/// let mut world = World::new(); +/// world.spawn(( +/// Name::new("Root"), +/// Children::spawn(( +/// Spawn(Name::new("Child1")), +/// SpawnIter(["Child2", "Child3"].into_iter().map(Name::new)), +/// )), +/// )); +/// ``` +pub struct SpawnIter(pub I); + +impl + Send + Sync + 'static, B: Bundle> SpawnableList + for SpawnIter +{ + fn spawn(self, world: &mut World, entity: Entity) { + for bundle in self.0 { + world.spawn((R::from(entity), bundle)); + } + } + + fn size_hint(&self) -> usize { + self.0.size_hint().0 + } +} + +/// A [`SpawnableList`] that spawns entities using a [`FnOnce`] with a [`RelatedSpawner`] as an argument: +/// +/// ``` +/// # use bevy_ecs::hierarchy::{Children, ChildOf}; +/// # use bevy_ecs::spawn::{Spawn, SpawnWith, SpawnRelated}; +/// # use bevy_ecs::name::Name; +/// # use bevy_ecs::relationship::RelatedSpawner; +/// # use bevy_ecs::world::World; +/// let mut world = World::new(); +/// world.spawn(( +/// Name::new("Root"), +/// Children::spawn(( +/// Spawn(Name::new("Child1")), +/// SpawnWith(|parent: &mut RelatedSpawner| { +/// parent.spawn(Name::new("Child2")); +/// parent.spawn(Name::new("Child3")); +/// }), +/// )), +/// )); +/// ``` +pub struct SpawnWith(pub F); + +impl) + Send + Sync + 'static> SpawnableList + for SpawnWith +{ + fn spawn(self, world: &mut World, entity: Entity) { + world.entity_mut(entity).with_related(self.0); + } + + fn size_hint(&self) -> usize { + 1 + } +} + +macro_rules! spawnable_list_impl { + ($($list: ident),*) => { + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such, the lints below may not always apply." + )] + impl),*> SpawnableList for ($($list,)*) { + fn spawn(self, _world: &mut World, _entity: Entity) { + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($list,)*) = self; + $($list.spawn(_world, _entity);)* + } + + fn size_hint(&self) -> usize { + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] + let ($($list,)*) = self; + 0 $(+ $list.size_hint())* + } + } + } +} + +all_tuples!(spawnable_list_impl, 0, 12, P); + +/// A [`Bundle`] that: +/// 1. Contains a [`RelationshipTarget`] component (associated with the given [`Relationship`]). This reserves space for the [`SpawnableList`]. +/// 2. Spawns a [`SpawnableList`] of related entities with a given [`Relationship`]. +/// +/// This is intended to be created using [`SpawnRelated`]. +pub struct SpawnRelatedBundle> { + list: L, + marker: PhantomData, +} + +impl> BundleEffect for SpawnRelatedBundle { + fn apply(self, entity: &mut EntityWorldMut) { + let id = entity.id(); + entity.world_scope(|world: &mut World| { + self.list.spawn(world, id); + }); + } +} + +// 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::Components, + 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::Components, + required_components: &mut crate::component::RequiredComponents, + ) { + ::register_required_components( + components, + required_components, + ); + } +} +impl> DynamicBundle for SpawnRelatedBundle { + type Effect = Self; + + fn get_components( + self, + func: &mut impl FnMut(crate::component::StorageType, bevy_ptr::OwningPtr<'_>), + ) -> Self::Effect { + ::with_capacity(self.list.size_hint()) + .get_components(func); + self + } +} + +/// A [`Bundle`] that: +/// 1. Contains a [`RelationshipTarget`] component (associated with the given [`Relationship`]). This reserves space for a single entity. +/// 2. Spawns a single related entity containing the given `B` [`Bundle`] and the given [`Relationship`]. +/// +/// This is intended to be created using [`SpawnRelated`]. +pub struct SpawnOneRelated { + bundle: B, + marker: PhantomData, +} + +impl BundleEffect for SpawnOneRelated { + fn apply(self, entity: &mut EntityWorldMut) { + entity.with_related::(|s| { + s.spawn(self.bundle); + }); + } +} + +impl DynamicBundle for SpawnOneRelated { + type Effect = Self; + + fn get_components( + self, + func: &mut impl FnMut(crate::component::StorageType, bevy_ptr::OwningPtr<'_>), + ) -> Self::Effect { + ::with_capacity(1).get_components(func); + self + } +} + +// 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::Components, + 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::Components, + 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. +/// 2. Spawns an entity (or a list of entities) that relate to the entity the [`Bundle`] is added to via the [`RelationshipTarget::Relationship`]. +pub trait SpawnRelated: RelationshipTarget { + /// Returns a [`Bundle`] containing this [`RelationshipTarget`] component. It also spawns a [`SpawnableList`] of entities, each related to the bundle's entity + /// via [`RelationshipTarget::Relationship`]. The [`RelationshipTarget`] (when possible) will pre-allocate space for the related entities. + /// + /// See [`Spawn`], [`SpawnIter`], and [`SpawnWith`] for usage examples. + fn spawn>( + list: L, + ) -> SpawnRelatedBundle; + + /// Returns a [`Bundle`] containing this [`RelationshipTarget`] component. It also spawns a single entity containing [`Bundle`] that is related to the bundle's entity + /// via [`RelationshipTarget::Relationship`]. + /// + /// ``` + /// # use bevy_ecs::hierarchy::Children; + /// # use bevy_ecs::spawn::SpawnRelated; + /// # use bevy_ecs::name::Name; + /// # use bevy_ecs::world::World; + /// let mut world = World::new(); + /// world.spawn(( + /// Name::new("Root"), + /// Children::spawn_one(Name::new("Child")), + /// )); + /// ``` + fn spawn_one(bundle: B) -> SpawnOneRelated; +} + +impl SpawnRelated for T { + fn spawn>( + list: L, + ) -> SpawnRelatedBundle { + SpawnRelatedBundle { + list, + marker: PhantomData, + } + } + + fn spawn_one(bundle: B) -> SpawnOneRelated { + SpawnOneRelated { + bundle, + marker: PhantomData, + } + } +} + +/// Returns a [`SpawnRelatedBundle`] that will insert the given [`RelationshipTarget`], spawn a [`SpawnableList`] of entities with given bundles that +/// relate to the [`RelationshipTarget`] entity via the [`RelationshipTarget::Relationship`] component, and reserve space in the [`RelationshipTarget`] for each spawned entity. +/// +/// The first argument is the [`RelationshipTarget`] type. Any additional arguments will be interpreted as bundles to be spawned. +/// +/// Also see [`children`](crate::children) for a [`Children`](crate::hierarchy::Children)-specific equivalent. +/// +/// ``` +/// # use bevy_ecs::hierarchy::Children; +/// # use bevy_ecs::name::Name; +/// # use bevy_ecs::world::World; +/// # use bevy_ecs::related; +/// # use bevy_ecs::spawn::{Spawn, SpawnRelated}; +/// let mut world = World::new(); +/// world.spawn(( +/// Name::new("Root"), +/// related!(Children[ +/// Name::new("Child1"), +/// ( +/// Name::new("Child2"), +/// related!(Children[ +/// Name::new("Grandchild"), +/// ]) +/// ) +/// ]) +/// )); +/// ``` +#[macro_export] +macro_rules! related { + ($relationship_target:ty [$($child:expr),*$(,)?]) => { + <$relationship_target>::spawn(($($crate::spawn::Spawn($child)),*)) + }; +} diff --git a/crates/bevy_ecs/src/system/commands/command.rs b/crates/bevy_ecs/src/system/commands/command.rs index 13a45e6622..c9383a1ee0 100644 --- a/crates/bevy_ecs/src/system/commands/command.rs +++ b/crates/bevy_ecs/src/system/commands/command.rs @@ -8,7 +8,7 @@ use core::panic::Location; use crate::{ - bundle::{Bundle, InsertMode}, + bundle::{Bundle, InsertMode, NoBundleEffect}, entity::Entity, event::{Event, Events}, observer::TriggerTargets, @@ -111,7 +111,7 @@ impl HandleError for C { pub fn spawn_batch(bundles_iter: I) -> impl Command where I: IntoIterator + Send + Sync + 'static, - I::Item: Bundle, + I::Item: Bundle, { #[cfg(feature = "track_location")] let caller = Location::caller(); @@ -135,7 +135,7 @@ where pub fn insert_batch(batch: I, insert_mode: InsertMode) -> impl Command where I: IntoIterator + Send + Sync + 'static, - B: Bundle, + B: Bundle, { #[cfg(feature = "track_location")] let caller = Location::caller(); diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 180939f514..4292ccb0c5 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -17,7 +17,8 @@ use core::panic::Location; use log::error; use crate::{ - bundle::{Bundle, InsertMode}, + self as bevy_ecs, + bundle::{Bundle, InsertMode, NoBundleEffect}, change_detection::Mut, component::{Component, ComponentId, Mutable}, entity::{Entities, Entity, EntityClonerBuilder}, @@ -539,7 +540,7 @@ impl<'w, 's> Commands<'w, 's> { pub fn spawn_batch(&mut self, bundles_iter: I) where I: IntoIterator + Send + Sync + 'static, - I::Item: Bundle, + I::Item: Bundle, { self.queue(command::spawn_batch(bundles_iter)); } @@ -680,7 +681,7 @@ impl<'w, 's> Commands<'w, 's> { pub fn insert_or_spawn_batch(&mut self, bundles_iter: I) where I: IntoIterator + Send + Sync + 'static, - B: Bundle, + B: Bundle, { let caller = Location::caller(); self.queue(move |world: &mut World| { @@ -720,7 +721,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, { self.queue(command::insert_batch(batch, InsertMode::Replace)); } @@ -747,7 +748,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, { self.queue(command::insert_batch(batch, InsertMode::Keep)); } @@ -772,7 +773,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, { self.queue( command::insert_batch(batch, InsertMode::Replace) @@ -800,7 +801,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, { self.queue( command::insert_batch(batch, InsertMode::Keep).handle_error_with(error_handler::warn()), diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 5e9add0d81..3a9a49e7fa 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -1,6 +1,9 @@ use crate::{ archetype::{Archetype, ArchetypeId, Archetypes}, - bundle::{Bundle, BundleId, BundleInfo, BundleInserter, DynamicBundle, InsertMode}, + bundle::{ + Bundle, BundleEffect, BundleFromComponents, BundleId, BundleInfo, BundleInserter, + DynamicBundle, InsertMode, + }, change_detection::MutUntyped, component::{Component, ComponentId, ComponentTicks, Components, Mutable, StorageType}, entity::{ @@ -1566,13 +1569,21 @@ impl<'w> EntityWorldMut<'w> { let change_tick = self.world.change_tick(); let mut bundle_inserter = BundleInserter::new::(self.world, self.location.archetype_id, change_tick); - self.location = - // SAFETY: location matches current entity. `T` matches `bundle_info` - unsafe { - bundle_inserter.insert(self.entity, self.location, bundle, mode, #[cfg(feature = "track_location")] caller) - }; + // SAFETY: location matches current entity. `T` matches `bundle_info` + let (location, after_effect) = unsafe { + bundle_inserter.insert( + self.entity, + self.location, + bundle, + mode, + #[cfg(feature = "track_location")] + caller, + ) + }; + self.location = location; self.world.flush(); self.update_location(); + after_effect.apply(self); self } @@ -1707,7 +1718,7 @@ impl<'w> EntityWorldMut<'w> { // TODO: BundleRemover? #[must_use] #[track_caller] - pub fn take(&mut self) -> Option { + pub fn take(&mut self) -> Option { self.assert_not_despawned(); let world = &mut self.world; let storages = &mut world.storages; @@ -4088,6 +4099,7 @@ unsafe fn insert_dynamic_bundle< impl<'a, I: Iterator)>> DynamicBundle for DynamicInsertBundle<'a, I> { + type Effect = (); fn get_components(self, func: &mut impl FnMut(StorageType, OwningPtr<'_>)) { self.components.for_each(|(t, ptr)| func(t, ptr)); } @@ -4099,14 +4111,16 @@ unsafe fn insert_dynamic_bundle< // SAFETY: location matches current entity. unsafe { - bundle_inserter.insert( - entity, - location, - bundle, - InsertMode::Replace, - #[cfg(feature = "track_location")] - caller, - ) + bundle_inserter + .insert( + entity, + location, + bundle, + InsertMode::Replace, + #[cfg(feature = "track_location")] + caller, + ) + .0 } } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 28ef6a6e5a..71f581d6e0 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -32,7 +32,10 @@ pub use spawn_batch::*; use crate::{ archetype::{ArchetypeId, ArchetypeRow, Archetypes}, - bundle::{Bundle, BundleInfo, BundleInserter, BundleSpawner, Bundles, InsertMode}, + bundle::{ + Bundle, BundleEffect, BundleInfo, BundleInserter, BundleSpawner, Bundles, InsertMode, + NoBundleEffect, + }, change_detection::{MutUntyped, TicksMut}, component::{ Component, ComponentDescriptor, ComponentHooks, ComponentId, ComponentInfo, ComponentTicks, @@ -1098,7 +1101,7 @@ impl World { let entity = self.entities.alloc(); let mut bundle_spawner = BundleSpawner::new::(self, change_tick); // SAFETY: bundle's type matches `bundle_info`, entity is allocated but non-existent - let mut entity_location = unsafe { + let (mut entity_location, after_effect) = unsafe { bundle_spawner.spawn_non_existent( entity, bundle, @@ -1121,7 +1124,9 @@ impl World { .set_spawned_or_despawned_by(entity.index(), caller); // SAFETY: entity and location are valid, as they were just created above - unsafe { EntityWorldMut::new(self, entity, entity_location) } + let mut entity = unsafe { EntityWorldMut::new(self, entity, entity_location) }; + after_effect.apply(&mut entity); + entity } /// # Safety @@ -1172,7 +1177,7 @@ impl World { pub fn spawn_batch(&mut self, iter: I) -> SpawnBatchIter<'_, I::IntoIter> where I: IntoIterator, - I::Item: Bundle, + I::Item: Bundle, { SpawnBatchIter::new( self, @@ -2234,7 +2239,7 @@ impl World { where I: IntoIterator, I::IntoIter: Iterator, - B: Bundle, + B: Bundle, { self.insert_or_spawn_batch_with_caller( iter, @@ -2254,7 +2259,7 @@ impl World { where I: IntoIterator, I::IntoIter: Iterator, - B: Bundle, + B: Bundle, { self.flush(); @@ -2390,7 +2395,7 @@ impl World { where I: IntoIterator, I::IntoIter: Iterator, - B: Bundle, + B: Bundle, { self.insert_batch_with_caller( batch, @@ -2420,7 +2425,7 @@ impl World { where I: IntoIterator, I::IntoIter: Iterator, - B: Bundle, + B: Bundle, { self.insert_batch_with_caller( batch, @@ -2444,7 +2449,7 @@ impl World { ) where I: IntoIterator, I::IntoIter: Iterator, - B: Bundle, + B: Bundle, { struct InserterArchetypeCache<'w> { inserter: BundleInserter<'w>, @@ -2540,7 +2545,7 @@ impl World { where I: IntoIterator, I::IntoIter: Iterator, - B: Bundle, + B: Bundle, { self.try_insert_batch_with_caller( batch, @@ -2567,7 +2572,7 @@ impl World { where I: IntoIterator, I::IntoIter: Iterator, - B: Bundle, + B: Bundle, { self.try_insert_batch_with_caller( batch, @@ -2596,7 +2601,7 @@ impl World { where I: IntoIterator, I::IntoIter: Iterator, - B: Bundle, + B: Bundle, { 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 eaa8cf7b9c..cbeaf8f4ad 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}, + bundle::{Bundle, BundleSpawner, NoBundleEffect}, entity::{Entity, EntitySetIterator}, world::World, }; @@ -25,7 +25,7 @@ where impl<'w, I> SpawnBatchIter<'w, I> where I: Iterator, - I::Item: Bundle, + I::Item: Bundle, { #[inline] #[track_caller] @@ -81,11 +81,15 @@ where let bundle = self.inner.next()?; // SAFETY: bundle matches spawner type unsafe { - Some(self.spawner.spawn( - bundle, - #[cfg(feature = "track_location")] - self.caller, - )) + Some( + self.spawner + .spawn( + bundle, + #[cfg(feature = "track_location")] + self.caller, + ) + .0, + ) } } diff --git a/crates/bevy_render/src/extract_component.rs b/crates/bevy_render/src/extract_component.rs index 64e744775f..f771998428 100644 --- a/crates/bevy_render/src/extract_component.rs +++ b/crates/bevy_render/src/extract_component.rs @@ -8,6 +8,7 @@ use crate::{ }; use bevy_app::{App, Plugin}; use bevy_ecs::{ + bundle::NoBundleEffect, component::Component, prelude::*, query::{QueryFilter, QueryItem, ReadOnlyQueryData}, @@ -53,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; // TODO: https://github.com/rust-lang/rust/issues/29661 // type Out: Component = Self; diff --git a/crates/bevy_transform/src/systems.rs b/crates/bevy_transform/src/systems.rs index 41291ab557..697e8af3b7 100644 --- a/crates/bevy_transform/src/systems.rs +++ b/crates/bevy_transform/src/systems.rs @@ -334,7 +334,6 @@ mod test { .get::(parent) .unwrap() .iter() - .cloned() .collect::>(), children, ); @@ -353,7 +352,6 @@ mod test { .get::(parent) .unwrap() .iter() - .cloned() .collect::>(), vec![children[1]] ); @@ -363,7 +361,6 @@ mod test { .get::(children[1]) .unwrap() .iter() - .cloned() .collect::>(), vec![children[0]] ); @@ -377,7 +374,6 @@ mod test { .get::(parent) .unwrap() .iter() - .cloned() .collect::>(), vec![children[1]] ); diff --git a/crates/bevy_ui/src/experimental/ghost_hierarchy.rs b/crates/bevy_ui/src/experimental/ghost_hierarchy.rs index c5eb46ffe8..a343ec87de 100644 --- a/crates/bevy_ui/src/experimental/ghost_hierarchy.rs +++ b/crates/bevy_ui/src/experimental/ghost_hierarchy.rs @@ -186,7 +186,7 @@ impl<'w, 's> Iterator for UiChildrenIter<'w, 's> { return Some(entity); } if let Some(children) = children { - self.stack.extend(children.iter().rev().copied()); + self.stack.extend(children.iter().rev()); } } } diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 6f7875e62a..7d1860e3b4 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -1119,7 +1119,6 @@ mod tests { .get::() .unwrap() .iter() - .copied() .collect::>(); for r in [2, 3, 5, 7, 11, 13, 17, 19, 21, 23, 29, 31].map(|n| (n as f32).recip()) { diff --git a/examples/3d/lighting.rs b/examples/3d/lighting.rs index ea58396fa7..b8d7883763 100644 --- a/examples/3d/lighting.rs +++ b/examples/3d/lighting.rs @@ -118,41 +118,36 @@ fn setup( }); // red point light - commands - .spawn(( - PointLight { - intensity: 100_000.0, - color: RED.into(), - shadows_enabled: true, + commands.spawn(( + PointLight { + intensity: 100_000.0, + color: RED.into(), + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(1.0, 2.0, 0.0), + children![( + Mesh3d(meshes.add(Sphere::new(0.1).mesh().uv(32, 18))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: RED.into(), + emissive: LinearRgba::new(4.0, 0.0, 0.0, 0.0), ..default() - }, - Transform::from_xyz(1.0, 2.0, 0.0), - )) - .with_children(|builder| { - builder.spawn(( - Mesh3d(meshes.add(Sphere::new(0.1).mesh().uv(32, 18))), - MeshMaterial3d(materials.add(StandardMaterial { - base_color: RED.into(), - emissive: LinearRgba::new(4.0, 0.0, 0.0, 0.0), - ..default() - })), - )); - }); + })), + )], + )); // green spot light - commands - .spawn(( - SpotLight { - intensity: 100_000.0, - color: LIME.into(), - shadows_enabled: true, - inner_angle: 0.6, - outer_angle: 0.8, - ..default() - }, - Transform::from_xyz(-1.0, 2.0, 0.0).looking_at(Vec3::new(-1.0, 0.0, 0.0), Vec3::Z), - )) - .with_child(( + commands.spawn(( + SpotLight { + intensity: 100_000.0, + color: LIME.into(), + shadows_enabled: true, + inner_angle: 0.6, + outer_angle: 0.8, + ..default() + }, + Transform::from_xyz(-1.0, 2.0, 0.0).looking_at(Vec3::new(-1.0, 0.0, 0.0), Vec3::Z), + children![( Mesh3d(meshes.add(Capsule3d::new(0.1, 0.125))), MeshMaterial3d(materials.add(StandardMaterial { base_color: LIME.into(), @@ -160,29 +155,27 @@ fn setup( ..default() })), Transform::from_rotation(Quat::from_rotation_x(PI / 2.0)), - )); + )], + )); // blue point light - commands - .spawn(( - PointLight { - intensity: 100_000.0, - color: BLUE.into(), - shadows_enabled: true, + commands.spawn(( + PointLight { + intensity: 100_000.0, + color: BLUE.into(), + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(0.0, 4.0, 0.0), + children![( + Mesh3d(meshes.add(Sphere::new(0.1).mesh().uv(32, 18))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: BLUE.into(), + emissive: LinearRgba::new(0.0, 0.0, 713.0, 0.0), ..default() - }, - Transform::from_xyz(0.0, 4.0, 0.0), - )) - .with_children(|builder| { - builder.spawn(( - Mesh3d(meshes.add(Sphere::new(0.1).mesh().uv(32, 18))), - MeshMaterial3d(materials.add(StandardMaterial { - base_color: BLUE.into(), - emissive: LinearRgba::new(0.0, 0.0, 713.0, 0.0), - ..default() - })), - )); - }); + })), + )], + )); // directional 'sun' light commands.spawn(( @@ -209,38 +202,34 @@ fn setup( // example instructions - commands - .spawn(( - Text::default(), - Node { - position_type: PositionType::Absolute, - top: Val::Px(12.0), - left: Val::Px(12.0), - ..default() - }, - )) - .with_children(|p| { - p.spawn(TextSpan(format!( - "Aperture: f/{:.0}\n", - parameters.aperture_f_stops, - ))); - p.spawn(TextSpan(format!( + commands.spawn(( + Text::default(), + Node { + position_type: PositionType::Absolute, + top: Val::Px(12.0), + left: Val::Px(12.0), + ..default() + }, + children![ + TextSpan(format!("Aperture: f/{:.0}\n", parameters.aperture_f_stops,)), + TextSpan(format!( "Shutter speed: 1/{:.0}s\n", 1.0 / parameters.shutter_speed_s - ))); - p.spawn(TextSpan(format!( + )), + TextSpan(format!( "Sensitivity: ISO {:.0}\n", parameters.sensitivity_iso - ))); - p.spawn(TextSpan::new("\n\n")); - p.spawn(TextSpan::new("Controls\n")); - p.spawn(TextSpan::new("---------------\n")); - p.spawn(TextSpan::new("Arrow keys - Move objects\n")); - p.spawn(TextSpan::new("1/2 - Decrease/Increase aperture\n")); - p.spawn(TextSpan::new("3/4 - Decrease/Increase shutter speed\n")); - p.spawn(TextSpan::new("5/6 - Decrease/Increase sensitivity\n")); - p.spawn(TextSpan::new("R - Reset exposure")); - }); + )), + TextSpan::new("\n\n"), + TextSpan::new("Controls\n"), + TextSpan::new("---------------\n"), + TextSpan::new("Arrow keys - Move objects\n"), + TextSpan::new("1/2 - Decrease/Increase aperture\n"), + TextSpan::new("3/4 - Decrease/Increase shutter speed\n"), + TextSpan::new("5/6 - Decrease/Increase sensitivity\n"), + TextSpan::new("R - Reset exposure"), + ], + )); // camera commands.spawn(( diff --git a/examples/3d/motion_blur.rs b/examples/3d/motion_blur.rs index 66c90f51bb..68bb556c6b 100644 --- a/examples/3d/motion_blur.rs +++ b/examples/3d/motion_blur.rs @@ -318,7 +318,7 @@ fn move_cars( let delta = transform.translation - prev; transform.look_to(delta, Vec3::Y); for child in children.iter() { - let Ok(mut wheel) = spins.get_mut(*child) else { + let Ok(mut wheel) = spins.get_mut(child) else { continue; }; let radius = wheel.scale.x; diff --git a/examples/games/alien_cake_addict.rs b/examples/games/alien_cake_addict.rs index 8749fe988b..06a2b9891d 100644 --- a/examples/games/alien_cake_addict.rs +++ b/examples/games/alien_cake_addict.rs @@ -347,15 +347,15 @@ fn spawn_bonus( game.bonus.j as f32, ), SceneRoot(game.bonus.handle.clone()), - )) - .with_child(( - PointLight { - color: Color::srgb(1.0, 1.0, 0.0), - intensity: 500_000.0, - range: 10.0, - ..default() - }, - Transform::from_xyz(0.0, 2.0, 0.0), + children![( + PointLight { + color: Color::srgb(1.0, 1.0, 0.0), + intensity: 500_000.0, + range: 10.0, + ..default() + }, + Transform::from_xyz(0.0, 2.0, 0.0), + )], )) .id(), ); @@ -389,22 +389,21 @@ fn gameover_keyboard( // display the number of cake eaten before losing fn display_score(mut commands: Commands, game: Res) { - commands - .spawn(( - StateScoped(GameState::GameOver), - Node { - width: Val::Percent(100.), - align_items: AlignItems::Center, - justify_content: JustifyContent::Center, - ..default() - }, - )) - .with_child(( + commands.spawn(( + StateScoped(GameState::GameOver), + Node { + width: Val::Percent(100.), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }, + children![( Text::new(format!("Cake eaten: {}", game.cake_eaten)), TextFont { font_size: 67.0, ..default() }, TextColor(Color::srgb(0.5, 0.5, 1.0)), - )); + )], + )); } diff --git a/examples/games/breakout.rs b/examples/games/breakout.rs index cc53437519..6128f28f6c 100644 --- a/examples/games/breakout.rs +++ b/examples/games/breakout.rs @@ -213,30 +213,29 @@ fn setup( )); // Scoreboard - commands - .spawn(( - Text::new("Score: "), - TextFont { - font_size: SCOREBOARD_FONT_SIZE, - ..default() - }, - TextColor(TEXT_COLOR), - ScoreboardUi, - Node { - position_type: PositionType::Absolute, - top: SCOREBOARD_TEXT_PADDING, - left: SCOREBOARD_TEXT_PADDING, - ..default() - }, - )) - .with_child(( + commands.spawn(( + Text::new("Score: "), + TextFont { + font_size: SCOREBOARD_FONT_SIZE, + ..default() + }, + TextColor(TEXT_COLOR), + ScoreboardUi, + Node { + position_type: PositionType::Absolute, + top: SCOREBOARD_TEXT_PADDING, + left: SCOREBOARD_TEXT_PADDING, + ..default() + }, + children![( TextSpan::default(), TextFont { font_size: SCOREBOARD_FONT_SIZE, ..default() }, TextColor(SCORE_COLOR), - )); + )], + )); // Walls commands.spawn(Wall::new(WallLocation::Left)); diff --git a/examples/games/desk_toy.rs b/examples/games/desk_toy.rs index d88dbc7e46..90e3847994 100644 --- a/examples/games/desk_toy.rs +++ b/examples/games/desk_toy.rs @@ -136,6 +136,9 @@ fn setup( .with_children(|commands| { // For each bird eye for (x, y, radius) in BIRDS_EYES { + let pupil_radius = radius * 0.6; + let pupil_highlight_radius = radius * 0.3; + let pupil_highlight_offset = radius * 0.3; // eye outline commands.spawn(( Mesh2d(circle.clone()), @@ -145,33 +148,28 @@ fn setup( )); // sclera - commands - .spawn((Transform::from_xyz(x, y, 2.0), Visibility::default())) - .with_children(|commands| { + commands.spawn(( + Transform::from_xyz(x, y, 2.0), + Visibility::default(), + children![ // sclera - commands.spawn(( + ( Mesh2d(circle.clone()), MeshMaterial2d(sclera_material.clone()), Transform::from_scale(Vec3::new(radius, radius, 0.0)), - )); - - let pupil_radius = radius * 0.6; - let pupil_highlight_radius = radius * 0.3; - let pupil_highlight_offset = radius * 0.3; + ), // pupil - commands - .spawn(( - Transform::from_xyz(0.0, 0.0, 1.0), - Visibility::default(), - Pupil { - eye_radius: radius, - pupil_radius, - velocity: Vec2::ZERO, - }, - )) - .with_children(|commands| { + ( + Transform::from_xyz(0.0, 0.0, 1.0), + Visibility::default(), + Pupil { + eye_radius: radius, + pupil_radius, + velocity: Vec2::ZERO, + }, + children![ // pupil main - commands.spawn(( + ( Mesh2d(circle.clone()), MeshMaterial2d(pupil_material.clone()), Transform::from_xyz(0.0, 0.0, 0.0).with_scale(Vec3::new( @@ -179,10 +177,9 @@ fn setup( pupil_radius, 1.0, )), - )); - + ), // pupil highlight - commands.spawn(( + ( Mesh2d(circle.clone()), MeshMaterial2d(pupil_highlight_material.clone()), Transform::from_xyz( @@ -195,9 +192,11 @@ fn setup( pupil_highlight_radius, 1.0, )), - )); - }); - }); + ) + ], + ) + ], + )); } }); } diff --git a/examples/games/game_menu.rs b/examples/games/game_menu.rs index ad6bb46042..03056cbe30 100644 --- a/examples/games/game_menu.rs +++ b/examples/games/game_menu.rs @@ -73,27 +73,24 @@ mod splash { fn splash_setup(mut commands: Commands, asset_server: Res) { let icon = asset_server.load("branding/icon.png"); // Display the logo - commands - .spawn(( + commands.spawn(( + Node { + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + width: Val::Percent(100.0), + height: Val::Percent(100.0), + ..default() + }, + OnSplashScreen, + children![( + ImageNode::new(icon), Node { - align_items: AlignItems::Center, - justify_content: JustifyContent::Center, - width: Val::Percent(100.0), - height: Val::Percent(100.0), + // This will set the logo to be 200px wide, and auto adjust its height + width: Val::Px(200.0), ..default() }, - OnSplashScreen, - )) - .with_children(|parent| { - parent.spawn(( - ImageNode::new(icon), - Node { - // This will set the logo to be 200px wide, and auto adjust its height - width: Val::Px(200.0), - ..default() - }, - )); - }); + )], + )); // Insert the timer as a resource commands.insert_resource(SplashTimer(Timer::from_seconds(1.0, TimerMode::Once))); } @@ -138,81 +135,76 @@ mod game { display_quality: Res, volume: Res, ) { - commands - .spawn(( + commands.spawn(( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + // center children + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }, + OnGameScreen, + children![( Node { - width: Val::Percent(100.0), - height: Val::Percent(100.0), - // center children + // This will display its children in a column, from top to bottom + flex_direction: FlexDirection::Column, + // `align_items` will align children on the cross axis. Here the main axis is + // vertical (column), so the cross axis is horizontal. This will center the + // children align_items: AlignItems::Center, - justify_content: JustifyContent::Center, ..default() }, - OnGameScreen, - )) - .with_children(|parent| { - // First create a `Node` for centering what we want to display - parent - .spawn(( - Node { - // This will display its children in a column, from top to bottom - flex_direction: FlexDirection::Column, - // `align_items` will align children on the cross axis. Here the main axis is - // vertical (column), so the cross axis is horizontal. This will center the - // children - align_items: AlignItems::Center, + BackgroundColor(Color::BLACK), + children![ + ( + Text::new("Will be back to the menu shortly..."), + TextFont { + font_size: 67.0, ..default() }, - BackgroundColor(Color::BLACK), - )) - .with_children(|p| { - p.spawn(( - Text::new("Will be back to the menu shortly..."), - TextFont { - font_size: 67.0, - ..default() - }, - TextColor(TEXT_COLOR), - Node { - margin: UiRect::all(Val::Px(50.0)), - ..default() - }, - )); - p.spawn(( - Text::default(), - Node { - margin: UiRect::all(Val::Px(50.0)), - ..default() - }, - )) - .with_children(|p| { - p.spawn(( + TextColor(TEXT_COLOR), + Node { + margin: UiRect::all(Val::Px(50.0)), + ..default() + }, + ), + ( + Text::default(), + Node { + margin: UiRect::all(Val::Px(50.0)), + ..default() + }, + children![ + ( TextSpan(format!("quality: {:?}", *display_quality)), TextFont { font_size: 50.0, ..default() }, TextColor(BLUE.into()), - )); - p.spawn(( + ), + ( TextSpan::new(" - "), TextFont { font_size: 50.0, ..default() }, TextColor(TEXT_COLOR), - )); - p.spawn(( + ), + ( TextSpan(format!("volume: {:?}", *volume)), TextFont { font_size: 50.0, ..default() }, TextColor(LIME.into()), - )); - }); - }); - }); + ), + ] + ), + ] + )], + )); // Spawn a 5 seconds timer to trigger going back to the menu commands.insert_resource(GameTimer(Timer::from_seconds(5.0, TimerMode::Once))); } @@ -230,7 +222,12 @@ mod game { } mod menu { - use bevy::{app::AppExit, color::palettes::css::CRIMSON, prelude::*}; + use bevy::{ + app::AppExit, + color::palettes::css::CRIMSON, + ecs::spawn::{SpawnIter, SpawnWith}, + prelude::*, + }; use super::{despawn_screen, DisplayQuality, GameState, Volume, TEXT_COLOR}; @@ -395,96 +392,85 @@ mod menu { ..default() }; - commands - .spawn(( + let right_icon = asset_server.load("textures/Game Icons/right.png"); + let wrench_icon = asset_server.load("textures/Game Icons/wrench.png"); + let exit_icon = asset_server.load("textures/Game Icons/exitRight.png"); + + commands.spawn(( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }, + OnMainMenuScreen, + children![( Node { - width: Val::Percent(100.0), - height: Val::Percent(100.0), + flex_direction: FlexDirection::Column, align_items: AlignItems::Center, - justify_content: JustifyContent::Center, ..default() }, - OnMainMenuScreen, - )) - .with_children(|parent| { - parent - .spawn(( - Node { - flex_direction: FlexDirection::Column, - align_items: AlignItems::Center, + BackgroundColor(CRIMSON.into()), + children![ + // Display the game name + ( + Text::new("Bevy Game Menu UI"), + TextFont { + font_size: 67.0, ..default() }, - BackgroundColor(CRIMSON.into()), - )) - .with_children(|parent| { - // Display the game name - parent.spawn(( - Text::new("Bevy Game Menu UI"), - TextFont { - font_size: 67.0, - ..default() - }, - TextColor(TEXT_COLOR), - Node { - margin: UiRect::all(Val::Px(50.0)), - ..default() - }, - )); - - // Display three buttons for each action available from the main menu: - // - new game - // - settings - // - quit - parent - .spawn(( - Button, - button_node.clone(), - BackgroundColor(NORMAL_BUTTON), - MenuButtonAction::Play, - )) - .with_children(|parent| { - let icon = asset_server.load("textures/Game Icons/right.png"); - parent.spawn((ImageNode::new(icon), button_icon_node.clone())); - parent.spawn(( - Text::new("New Game"), - button_text_font.clone(), - TextColor(TEXT_COLOR), - )); - }); - parent - .spawn(( - Button, - button_node.clone(), - BackgroundColor(NORMAL_BUTTON), - MenuButtonAction::Settings, - )) - .with_children(|parent| { - let icon = asset_server.load("textures/Game Icons/wrench.png"); - parent.spawn((ImageNode::new(icon), button_icon_node.clone())); - parent.spawn(( - Text::new("Settings"), - button_text_font.clone(), - TextColor(TEXT_COLOR), - )); - }); - parent - .spawn(( - Button, - button_node, - BackgroundColor(NORMAL_BUTTON), - MenuButtonAction::Quit, - )) - .with_children(|parent| { - let icon = asset_server.load("textures/Game Icons/exitRight.png"); - parent.spawn((ImageNode::new(icon), button_icon_node)); - parent.spawn(( - Text::new("Quit"), - button_text_font, - TextColor(TEXT_COLOR), - )); - }); - }); - }); + TextColor(TEXT_COLOR), + Node { + margin: UiRect::all(Val::Px(50.0)), + ..default() + }, + ), + // Display three buttons for each action available from the main menu: + // - new game + // - settings + // - quit + ( + Button, + button_node.clone(), + BackgroundColor(NORMAL_BUTTON), + MenuButtonAction::Play, + children![ + (ImageNode::new(right_icon), button_icon_node.clone()), + ( + Text::new("New Game"), + button_text_font.clone(), + TextColor(TEXT_COLOR), + ), + ] + ), + ( + Button, + button_node.clone(), + BackgroundColor(NORMAL_BUTTON), + MenuButtonAction::Settings, + children![ + (ImageNode::new(wrench_icon), button_icon_node.clone()), + ( + Text::new("Settings"), + button_text_font.clone(), + TextColor(TEXT_COLOR), + ), + ] + ), + ( + Button, + button_node, + BackgroundColor(NORMAL_BUTTON), + MenuButtonAction::Quit, + children![ + (ImageNode::new(exit_icon), button_icon_node), + (Text::new("Quit"), button_text_font, TextColor(TEXT_COLOR),), + ] + ), + ] + )], + )); } fn settings_menu_setup(mut commands: Commands) { @@ -505,104 +491,94 @@ mod menu { TextColor(TEXT_COLOR), ); - commands - .spawn(( + commands.spawn(( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }, + OnSettingsMenuScreen, + children![( Node { - width: Val::Percent(100.0), - height: Val::Percent(100.0), + flex_direction: FlexDirection::Column, align_items: AlignItems::Center, - justify_content: JustifyContent::Center, ..default() }, - OnSettingsMenuScreen, - )) - .with_children(|parent| { - parent - .spawn(( - Node { - flex_direction: FlexDirection::Column, - align_items: AlignItems::Center, - ..default() - }, - BackgroundColor(CRIMSON.into()), - )) - .with_children(|parent| { - for (action, text) in [ - (MenuButtonAction::SettingsDisplay, "Display"), - (MenuButtonAction::SettingsSound, "Sound"), - (MenuButtonAction::BackToMainMenu, "Back"), - ] { - parent - .spawn(( - Button, - button_node.clone(), - BackgroundColor(NORMAL_BUTTON), - action, - )) - .with_children(|parent| { - parent.spawn((Text::new(text), button_text_style.clone())); - }); - } - }); - }); + BackgroundColor(CRIMSON.into()), + Children::spawn(SpawnIter( + [ + (MenuButtonAction::SettingsDisplay, "Display"), + (MenuButtonAction::SettingsSound, "Sound"), + (MenuButtonAction::BackToMainMenu, "Back"), + ] + .into_iter() + .map(move |(action, text)| { + ( + Button, + button_node.clone(), + BackgroundColor(NORMAL_BUTTON), + action, + children![(Text::new(text), button_text_style.clone())], + ) + }) + )) + )], + )); } fn display_settings_menu_setup(mut commands: Commands, display_quality: Res) { - let button_node = Node { - width: Val::Px(200.0), - height: Val::Px(65.0), - margin: UiRect::all(Val::Px(20.0)), - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - ..default() - }; - let button_text_style = ( - TextFont { - font_size: 33.0, + fn button_node() -> Node { + Node { + width: Val::Px(200.0), + height: Val::Px(65.0), + margin: UiRect::all(Val::Px(20.0)), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, ..default() - }, - TextColor(TEXT_COLOR), - ); - - commands - .spawn(( - Node { - width: Val::Percent(100.0), - height: Val::Percent(100.0), - align_items: AlignItems::Center, - justify_content: JustifyContent::Center, + } + } + fn button_text_style() -> impl Bundle { + ( + TextFont { + font_size: 33.0, ..default() }, - OnDisplaySettingsMenuScreen, - )) - .with_children(|parent| { - parent - .spawn(( + TextColor(TEXT_COLOR), + ) + } + + let display_quality = *display_quality; + commands.spawn(( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }, + OnDisplaySettingsMenuScreen, + children![( + Node { + flex_direction: FlexDirection::Column, + align_items: AlignItems::Center, + ..default() + }, + BackgroundColor(CRIMSON.into()), + children![ + // Create a new `Node`, this time not setting its `flex_direction`. It will + // use the default value, `FlexDirection::Row`, from left to right. + ( Node { - flex_direction: FlexDirection::Column, align_items: AlignItems::Center, ..default() }, BackgroundColor(CRIMSON.into()), - )) - .with_children(|parent| { - // Create a new `Node`, this time not setting its `flex_direction`. It will - // use the default value, `FlexDirection::Row`, from left to right. - parent - .spawn(( - Node { - align_items: AlignItems::Center, - ..default() - }, - BackgroundColor(CRIMSON.into()), - )) - .with_children(|parent| { - // Display a label for the current setting - parent.spawn(( - Text::new("Display Quality"), - button_text_style.clone(), - )); - // Display a button for each possible value + Children::spawn(( + // Display a label for the current setting + Spawn((Text::new("Display Quality"), button_text_style())), + SpawnWith(move |parent: &mut ChildSpawner| { for quality_setting in [ DisplayQuality::Low, DisplayQuality::Medium, @@ -613,35 +589,33 @@ mod menu { Node { width: Val::Px(150.0), height: Val::Px(65.0), - ..button_node.clone() + ..button_node() }, BackgroundColor(NORMAL_BUTTON), quality_setting, - )); - entity.with_children(|parent| { - parent.spawn(( + children![( Text::new(format!("{quality_setting:?}")), - button_text_style.clone(), - )); - }); - if *display_quality == quality_setting { + button_text_style(), + )], + )); + if display_quality == quality_setting { entity.insert(SelectedOption); } } - }); - // Display the back button to return to the settings screen - parent - .spawn(( - Button, - button_node, - BackgroundColor(NORMAL_BUTTON), - MenuButtonAction::BackToSettings, - )) - .with_children(|parent| { - parent.spawn((Text::new("Back"), button_text_style)); - }); - }); - }); + }) + )) + ), + // Display the back button to return to the settings screen + ( + Button, + button_node(), + BackgroundColor(NORMAL_BUTTON), + MenuButtonAction::BackToSettings, + children![(Text::new("Back"), button_text_style())] + ) + ] + )], + )); } fn sound_settings_menu_setup(mut commands: Commands, volume: Res) { @@ -661,64 +635,62 @@ mod menu { TextColor(TEXT_COLOR), ); - commands - .spawn(( + let volume = *volume; + let button_node_clone = button_node.clone(); + commands.spawn(( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }, + OnSoundSettingsMenuScreen, + children![( Node { - width: Val::Percent(100.0), - height: Val::Percent(100.0), + flex_direction: FlexDirection::Column, align_items: AlignItems::Center, - justify_content: JustifyContent::Center, ..default() }, - OnSoundSettingsMenuScreen, - )) - .with_children(|parent| { - parent - .spawn(( + BackgroundColor(CRIMSON.into()), + children![ + ( Node { - flex_direction: FlexDirection::Column, align_items: AlignItems::Center, ..default() }, BackgroundColor(CRIMSON.into()), - )) - .with_children(|parent| { - parent - .spawn(( - Node { - align_items: AlignItems::Center, - ..default() - }, - BackgroundColor(CRIMSON.into()), - )) - .with_children(|parent| { - parent.spawn((Text::new("Volume"), button_text_style.clone())); + Children::spawn(( + Spawn((Text::new("Volume"), button_text_style.clone())), + SpawnWith(move |parent: &mut ChildSpawner| { for volume_setting in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] { let mut entity = parent.spawn(( Button, Node { width: Val::Px(30.0), height: Val::Px(65.0), - ..button_node.clone() + ..button_node_clone.clone() }, BackgroundColor(NORMAL_BUTTON), Volume(volume_setting), )); - if *volume == Volume(volume_setting) { + if volume == Volume(volume_setting) { entity.insert(SelectedOption); } } - }); - parent - .spawn(( - Button, - button_node, - BackgroundColor(NORMAL_BUTTON), - MenuButtonAction::BackToSettings, - )) - .with_child((Text::new("Back"), button_text_style)); - }); - }); + }) + )) + ), + ( + Button, + button_node, + BackgroundColor(NORMAL_BUTTON), + MenuButtonAction::BackToSettings, + children![(Text::new("Back"), button_text_style)] + ) + ] + )], + )); } fn menu_action( diff --git a/examples/ui/button.rs b/examples/ui/button.rs index bf71ad0881..5f6c615736 100644 --- a/examples/ui/button.rs +++ b/examples/ui/button.rs @@ -51,44 +51,46 @@ fn button_system( } } -fn setup(mut commands: Commands, asset_server: Res) { +fn setup(mut commands: Commands, assets: Res) { // ui camera commands.spawn(Camera2d); - commands - .spawn(Node { + commands.spawn(button(&assets)); +} + +fn button(asset_server: &AssetServer) -> impl Bundle { + ( + Node { width: Val::Percent(100.0), height: Val::Percent(100.0), align_items: AlignItems::Center, justify_content: JustifyContent::Center, ..default() - }) - .with_children(|parent| { - parent - .spawn(( - Button, - Node { - width: Val::Px(150.0), - height: Val::Px(65.0), - border: UiRect::all(Val::Px(5.0)), - // horizontally center child text - justify_content: JustifyContent::Center, - // vertically center child text - align_items: AlignItems::Center, - ..default() - }, - BorderColor(Color::BLACK), - BorderRadius::MAX, - BackgroundColor(NORMAL_BUTTON), - )) - .with_child(( - Text::new("Button"), - TextFont { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), - font_size: 33.0, - ..default() - }, - TextColor(Color::srgb(0.9, 0.9, 0.9)), - TextShadow::default(), - )); - }); + }, + children![( + Button, + Node { + width: Val::Px(150.0), + height: Val::Px(65.0), + border: UiRect::all(Val::Px(5.0)), + // horizontally center child text + justify_content: JustifyContent::Center, + // vertically center child text + align_items: AlignItems::Center, + ..default() + }, + BorderColor(Color::BLACK), + BorderRadius::MAX, + BackgroundColor(NORMAL_BUTTON), + children![( + Text::new("Button"), + TextFont { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 33.0, + ..default() + }, + TextColor(Color::srgb(0.9, 0.9, 0.9)), + TextShadow::default(), + )] + )], + ) }