diff --git a/benches/benches/bevy_ecs/world/world_get.rs b/benches/benches/bevy_ecs/world/world_get.rs index fcb9b0116bb95..283b984186150 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 2c06ad1fe6bd5..e97149ec0457e 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 1a33999cd58ec..285e42d4e6070 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,6 +296,31 @@ macro_rules! tuple_impl { $(<$name as Bundle>::get_component_ids(components, ids);)* } + fn register_required_components( + components: &mut Components, + required_components: &mut RequiredComponents, + ) { + $(<$name as Bundle>::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." + )] + #[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." @@ -275,14 +335,7 @@ macro_rules! tuple_impl { )] // 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, - ) { - $(<$name as Bundle>::register_required_components(components, required_components);)* + unsafe { ($(<$name as BundleFromComponents>::from_components(ctx, func),)*) } } } @@ -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 2f34a2e504b1b..6c8ee9f31b9b0 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 299ffe7b76542..26b888f2302ce 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 baa6bc7d08bfd..b7acf69d6aad9 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 0000000000000..2fb04c4c7b293 --- /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 13a45e6622fd5..c9383a1ee07d4 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 180939f514988..4292ccb0c5497 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 5e9add0d81878..3a9a49e7fad2f 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 28ef6a6e5a6ca..71f581d6e0670 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 eaa8cf7b9c889..cbeaf8f4ade16 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 64e744775ffaf..f77199842828c 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 41291ab55726f..697e8af3b73cf 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 c5eb46ffe817b..a343ec87de135 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 6f7875e62a7e0..7d1860e3b4fc5 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 ea58396fa71f3..b8d7883763019 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 66c90f51bb1ff..68bb556c6b3e0 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 8749fe988bf7e..06a2b9891d5d8 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 cc53437519410..6128f28f6c0c6 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 d88dbc7e46225..90e38479944fb 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 ad6bb4604251e..03056cbe30151 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(( + BackgroundColor(Color::BLACK), + children![ + ( + Text::new("Will be back to the menu shortly..."), + TextFont { + font_size: 67.0, + ..default() + }, + TextColor(TEXT_COLOR), 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, + margin: UiRect::all(Val::Px(50.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(( + ), + ( + 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(( + BackgroundColor(CRIMSON.into()), + children![ + // Display the game name + ( + Text::new("Bevy Game Menu UI"), + TextFont { + font_size: 67.0, + ..default() + }, + TextColor(TEXT_COLOR), Node { - flex_direction: FlexDirection::Column, - align_items: AlignItems::Center, + margin: UiRect::all(Val::Px(50.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), - )); - }); - }); - }); + ), + // 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), - ); + } + } + fn button_text_style() -> impl Bundle { + ( + TextFont { + font_size: 33.0, + ..default() + }, + TextColor(TEXT_COLOR), + ) + } - commands - .spawn(( + 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 { - width: Val::Percent(100.0), - height: Val::Percent(100.0), + flex_direction: FlexDirection::Column, align_items: AlignItems::Center, - justify_content: JustifyContent::Center, ..default() }, - OnDisplaySettingsMenuScreen, - )) - .with_children(|parent| { - parent - .spawn(( + 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 bf71ad0881ecc..5f6c615736ac0 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(), + )] + )], + ) }