Improved Spawn APIs and Bundle Effects (#17521)

## Objective

A major critique of Bevy at the moment is how boilerplatey it is to
compose (and read) entity hierarchies:

```rust
commands
    .spawn(Foo)
    .with_children(|p| {
        p.spawn(Bar).with_children(|p| {
            p.spawn(Baz);
        });
        p.spawn(Bar).with_children(|p| {
            p.spawn(Baz);
        });
    });
```

There is also currently no good way to statically define and return an
entity hierarchy from a function. Instead, people often do this
"internally" with a Commands function that returns nothing, making it
impossible to spawn the hierarchy in other cases (direct World spawns,
ChildSpawner, etc).

Additionally, because this style of API results in creating the
hierarchy bits _after_ the initial spawn of a bundle, it causes ECS
archetype changes (and often expensive table moves).

Because children are initialized after the fact, we also can't count
them to pre-allocate space. This means each time a child inserts itself,
it has a high chance of overflowing the currently allocated capacity in
the `RelationshipTarget` collection, causing literal worst-case
reallocations.

We can do better!

## Solution

The Bundle trait has been extended to support an optional
`BundleEffect`. This is applied directly to World immediately _after_
the Bundle has fully inserted. Note that this is
[intentionally](https://github.com/bevyengine/bevy/discussions/16920)
_not done via a deferred Command_, which would require repeatedly
copying each remaining subtree of the hierarchy to a new command as we
walk down the tree (_not_ good performance).

This allows us to implement the new `SpawnRelated` trait for all
`RelationshipTarget` impls, which looks like this in practice:

```rust
world.spawn((
    Foo,
    Children::spawn((
        Spawn((
            Bar,
            Children::spawn(Spawn(Baz)),
        )),
        Spawn((
            Bar,
            Children::spawn(Spawn(Baz)),
        )),
    ))
))
```

`Children::spawn` returns `SpawnRelatedBundle<Children, L:
SpawnableList>`, which is a `Bundle` that inserts `Children`
(preallocated to the size of the `SpawnableList::size_hint()`).
`Spawn<B: Bundle>(pub B)` implements `SpawnableList` with a size of 1.
`SpawnableList` is also implemented for tuples of `SpawnableList` (same
general pattern as the Bundle impl).

There are currently three built-in `SpawnableList` implementations:

```rust
world.spawn((
    Foo,
    Children::spawn((
        Spawn(Name::new("Child1")),   
        SpawnIter(["Child2", "Child3"].into_iter().map(Name::new),
        SpawnWith(|parent: &mut ChildSpawner| {
            parent.spawn(Name::new("Child4"));
            parent.spawn(Name::new("Child5"));
        })
    )),
))
```

We get the benefits of "structured init", but we have nice flexibility
where it is required!

Some readers' first instinct might be to try to remove the need for the
`Spawn` wrapper. This is impossible in the Rust type system, as a tuple
of "child Bundles to be spawned" and a "tuple of Components to be added
via a single Bundle" is ambiguous in the Rust type system. There are two
ways to resolve that ambiguity:

1. By adding support for variadics to the Rust type system (removing the
need for nested bundles). This is out of scope for this PR :)
2. Using wrapper types to resolve the ambiguity (this is what I did in
this PR).

For the single-entity spawn cases, `Children::spawn_one` does also
exist, which removes the need for the wrapper:

```rust
world.spawn((
    Foo,
    Children::spawn_one(Bar),
))
```

## This works for all Relationships

This API isn't just for `Children` / `ChildOf` relationships. It works
for any relationship type, and they can be mixed and matched!

```rust
world.spawn((
    Foo,
    Observers::spawn((
        Spawn(Observer::new(|trigger: Trigger<FuseLit>| {})),
        Spawn(Observer::new(|trigger: Trigger<Exploded>| {})),
    )),
    OwnerOf::spawn(Spawn(Bar))
    Children::spawn(Spawn(Baz))
))
```

## Macros

While `Spawn` is necessary to satisfy the type system, we _can_ remove
the need to express it via macros. The example above can be expressed
more succinctly using the new `children![X]` macro, which internally
produces `Children::spawn(Spawn(X))`:

```rust
world.spawn((
    Foo,
    children![
        (
            Bar,
            children![Baz],
        ),
        (
            Bar,
            children![Baz],
        ),
    ]
))
```

There is also a `related!` macro, which is a generic version of the
`children!` macro that supports any relationship type:

```rust
world.spawn((
    Foo,
    related!(Children[
        (
            Bar,
            related!(Children[Baz]),
        ),
        (
            Bar,
            related!(Children[Baz]),
        ),
    ])
))
```

## Returning Hierarchies from Functions

Thanks to these changes, the following pattern is now possible:

```rust
fn button(text: &str, color: Color) -> impl Bundle {
    (
        Node {
            width: Val::Px(300.),
            height: Val::Px(100.),
            ..default()
        },
        BackgroundColor(color),
        children![
            Text::new(text),
        ]
    )
}

fn ui() -> impl Bundle {
    (
        Node {
            width: Val::Percent(100.0),
            height: Val::Percent(100.0),
            ..default(),
        },
        children![
            button("hello", BLUE),
            button("world", RED),
        ]
    )
}

// spawn from a system
fn system(mut commands: Commands) {
    commands.spawn(ui());
}

// spawn directly on World
world.spawn(ui());
```

## Additional Changes and Notes

* `Bundle::from_components` has been split out into
`BundleFromComponents::from_components`, enabling us to implement
`Bundle` for types that cannot be "taken" from the ECS (such as the new
`SpawnRelatedBundle`).
* The `NoBundleEffect` trait (which implements `BundleEffect`) is
implemented for empty tuples (and tuples of empty tuples), which allows
us to constrain APIs to only accept bundles that do not have effects.
This is critical because the current batch spawn APIs cannot efficiently
apply BundleEffects in their current form (as doing so in-place could
invalidate the cached raw pointers). We could consider allocating a
buffer of the effects to be applied later, but that does have
performance implications that could offset the balance and value of the
batched APIs (and would likely require some refactors to the underlying
code). I've decided to be conservative here. We can consider relaxing
that requirement on those APIs later, but that should be done in a
followup imo.
* I've ported a few examples to illustrate real-world usage. I think in
a followup we should port all examples to the `children!` form whenever
possible (and for cases that require things like SpawnIter, use the raw
APIs).
* Some may ask "why not use the `Relationship` to spawn (ex:
`ChildOf::spawn(Foo)`) instead of the `RelationshipTarget` (ex:
`Children::spawn(Spawn(Foo))`)?". That _would_ allow us to remove the
`Spawn` wrapper. I've explicitly chosen to disallow this pattern.
`Bundle::Effect` has the ability to create _significant_ weirdness.
Things in `Bundle` position look like components. For example
`world.spawn((Foo, ChildOf::spawn(Bar)))` _looks and reads_ like Foo is
a child of Bar. `ChildOf` is in Foo's "component position" but it is not
a component on Foo. This is a huge problem. Now that `Bundle::Effect`
exists, we should be _very_ principled about keeping the "weird and
unintuitive behavior" to a minimum. Things that read like components
_should be the components they appear to be".

## Remaining Work

* The macros are currently trivially implemented using macro_rules and
are currently limited to the max tuple length. They will require a
proc_macro implementation to work around the tuple length limit.

## Next Steps

* Port the remaining examples to use `children!` where possible and raw
`Spawn` / `SpawnIter` / `SpawnWith` where the flexibility of the raw API
is required.

## Migration Guide

Existing spawn patterns will continue to work as expected.

Manual Bundle implementations now require a `BundleEffect` associated
type. Exisiting bundles would have no bundle effect, so use `()`.
Additionally `Bundle::from_components` has been moved to the new
`BundleFromComponents` trait.

```rust
// Before
unsafe impl Bundle for X {
    unsafe fn from_components<T, F>(ctx: &mut T, func: &mut F) -> Self {
    }
    /* remaining bundle impl here */
}

// After
unsafe impl Bundle for X {
    type Effect = ();
    /* remaining bundle impl here */
}

unsafe impl BundleFromComponents for X {
    unsafe fn from_components<T, F>(ctx: &mut T, func: &mut F) -> Self {
    }
}
```

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.com>
Co-authored-by: Emerson Coskey <emerson@coskey.dev>
This commit is contained in:
Carter Anderson 2025-02-09 15:32:56 -08:00 committed by GitHub
parent c679b861d8
commit ea578415e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1080 additions and 603 deletions

View File

@ -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<T: Component + Default>(entity_count: u32) -> World {
black_box(world)
}
fn setup_wide<T: Bundle + Default>(entity_count: u32) -> World {
fn setup_wide<T: Bundle<Effect: NoBundleEffect> + Default>(entity_count: u32) -> World {
let mut world = World::default();
world.spawn_batch((0..entity_count).map(|_| T::default()));
black_box(world)

View File

@ -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(

View File

@ -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<ComponentId>));
/// 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<C: Component> Bundle for C {
fn component_ids(components: &mut Components, ids: &mut impl FnMut(ComponentId)) {
ids(components.register_component::<C>());
}
unsafe fn from_components<T, F>(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<C: Component> 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<C: Component> BundleFromComponents for C {
unsafe fn from_components<T, F>(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<C: Component> 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<T, F>(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<T, F>(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]

View File

@ -277,12 +277,49 @@ pub fn validate_parent_has_component<C: 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::<Children>().unwrap().len(), 2,);
}
}

View File

@ -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,

View File

@ -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<T: Bundle + FromReflect + TypePath>() -> Self {
pub fn new<T: Bundle + FromReflect + TypePath + BundleFromComponents>() -> Self {
<ReflectBundle as FromType<T>>::from_type().0
}
}
@ -139,7 +140,7 @@ impl ReflectBundle {
}
}
impl<B: Bundle + Reflect + TypePath> FromType<B> for ReflectBundle {
impl<B: Bundle + Reflect + TypePath + BundleFromComponents> FromType<B> for ReflectBundle {
fn from_type() -> Self {
ReflectBundle(ReflectBundleFns {
insert: |entity, reflected_bundle, registry| {

View File

@ -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<B: Bundle>(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<R> {
/// 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<R: Relationship, B: Bundle> SpawnableList<R> for Spawn<B> {
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<I>(pub I);
impl<R: Relationship, I: Iterator<Item = B> + Send + Sync + 'static, B: Bundle> SpawnableList<R>
for SpawnIter<I>
{
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<ChildOf>| {
/// parent.spawn(Name::new("Child2"));
/// parent.spawn(Name::new("Child3"));
/// }),
/// )),
/// ));
/// ```
pub struct SpawnWith<F>(pub F);
impl<R: Relationship, F: FnOnce(&mut RelatedSpawner<R>) + Send + Sync + 'static> SpawnableList<R>
for SpawnWith<F>
{
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<R: Relationship, $($list: SpawnableList<R>),*> SpawnableList<R> 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<R: Relationship, L: SpawnableList<R>> {
list: L,
marker: PhantomData<R>,
}
impl<R: Relationship, L: SpawnableList<R>> BundleEffect for SpawnRelatedBundle<R, L> {
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<R: Relationship, L: SpawnableList<R> + Send + Sync + 'static> Bundle
for SpawnRelatedBundle<R, L>
{
fn component_ids(
components: &mut crate::component::Components,
ids: &mut impl FnMut(crate::component::ComponentId),
) {
<R::RelationshipTarget as Bundle>::component_ids(components, ids);
}
fn get_component_ids(
components: &crate::component::Components,
ids: &mut impl FnMut(Option<crate::component::ComponentId>),
) {
<R::RelationshipTarget as Bundle>::get_component_ids(components, ids);
}
fn register_required_components(
components: &mut crate::component::Components,
required_components: &mut crate::component::RequiredComponents,
) {
<R::RelationshipTarget as Bundle>::register_required_components(
components,
required_components,
);
}
}
impl<R: Relationship, L: SpawnableList<R>> DynamicBundle for SpawnRelatedBundle<R, L> {
type Effect = Self;
fn get_components(
self,
func: &mut impl FnMut(crate::component::StorageType, bevy_ptr::OwningPtr<'_>),
) -> Self::Effect {
<R::RelationshipTarget as RelationshipTarget>::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<R: Relationship, B: Bundle> {
bundle: B,
marker: PhantomData<R>,
}
impl<R: Relationship, B: Bundle> BundleEffect for SpawnOneRelated<R, B> {
fn apply(self, entity: &mut EntityWorldMut) {
entity.with_related::<R>(|s| {
s.spawn(self.bundle);
});
}
}
impl<R: Relationship, B: Bundle> DynamicBundle for SpawnOneRelated<R, B> {
type Effect = Self;
fn get_components(
self,
func: &mut impl FnMut(crate::component::StorageType, bevy_ptr::OwningPtr<'_>),
) -> Self::Effect {
<R::RelationshipTarget as RelationshipTarget>::with_capacity(1).get_components(func);
self
}
}
// SAFETY: This internally relies on the RelationshipTarget's Bundle implementation, which is sound.
unsafe impl<R: Relationship, B: Bundle> Bundle for SpawnOneRelated<R, B> {
fn component_ids(
components: &mut crate::component::Components,
ids: &mut impl FnMut(crate::component::ComponentId),
) {
<R::RelationshipTarget as Bundle>::component_ids(components, ids);
}
fn get_component_ids(
components: &crate::component::Components,
ids: &mut impl FnMut(Option<crate::component::ComponentId>),
) {
<R::RelationshipTarget as Bundle>::get_component_ids(components, ids);
}
fn register_required_components(
components: &mut crate::component::Components,
required_components: &mut crate::component::RequiredComponents,
) {
<R::RelationshipTarget as Bundle>::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<L: SpawnableList<Self::Relationship>>(
list: L,
) -> SpawnRelatedBundle<Self::Relationship, L>;
/// 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<B: Bundle>(bundle: B) -> SpawnOneRelated<Self::Relationship, B>;
}
impl<T: RelationshipTarget> SpawnRelated for T {
fn spawn<L: SpawnableList<Self::Relationship>>(
list: L,
) -> SpawnRelatedBundle<Self::Relationship, L> {
SpawnRelatedBundle {
list,
marker: PhantomData,
}
}
fn spawn_one<B: Bundle>(bundle: B) -> SpawnOneRelated<Self::Relationship, B> {
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)),*))
};
}

View File

@ -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<C: Command> HandleError for C {
pub fn spawn_batch<I>(bundles_iter: I) -> impl Command
where
I: IntoIterator + Send + Sync + 'static,
I::Item: Bundle,
I::Item: Bundle<Effect: NoBundleEffect>,
{
#[cfg(feature = "track_location")]
let caller = Location::caller();
@ -135,7 +135,7 @@ where
pub fn insert_batch<I, B>(batch: I, insert_mode: InsertMode) -> impl Command<Result>
where
I: IntoIterator<Item = (Entity, B)> + Send + Sync + 'static,
B: Bundle,
B: Bundle<Effect: NoBundleEffect>,
{
#[cfg(feature = "track_location")]
let caller = Location::caller();

View File

@ -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<I>(&mut self, bundles_iter: I)
where
I: IntoIterator + Send + Sync + 'static,
I::Item: Bundle,
I::Item: Bundle<Effect: NoBundleEffect>,
{
self.queue(command::spawn_batch(bundles_iter));
}
@ -680,7 +681,7 @@ impl<'w, 's> Commands<'w, 's> {
pub fn insert_or_spawn_batch<I, B>(&mut self, bundles_iter: I)
where
I: IntoIterator<Item = (Entity, B)> + Send + Sync + 'static,
B: Bundle,
B: Bundle<Effect: NoBundleEffect>,
{
let caller = Location::caller();
self.queue(move |world: &mut World| {
@ -720,7 +721,7 @@ impl<'w, 's> Commands<'w, 's> {
pub fn insert_batch<I, B>(&mut self, batch: I)
where
I: IntoIterator<Item = (Entity, B)> + Send + Sync + 'static,
B: Bundle,
B: Bundle<Effect: NoBundleEffect>,
{
self.queue(command::insert_batch(batch, InsertMode::Replace));
}
@ -747,7 +748,7 @@ impl<'w, 's> Commands<'w, 's> {
pub fn insert_batch_if_new<I, B>(&mut self, batch: I)
where
I: IntoIterator<Item = (Entity, B)> + Send + Sync + 'static,
B: Bundle,
B: Bundle<Effect: NoBundleEffect>,
{
self.queue(command::insert_batch(batch, InsertMode::Keep));
}
@ -772,7 +773,7 @@ impl<'w, 's> Commands<'w, 's> {
pub fn try_insert_batch<I, B>(&mut self, batch: I)
where
I: IntoIterator<Item = (Entity, B)> + Send + Sync + 'static,
B: Bundle,
B: Bundle<Effect: NoBundleEffect>,
{
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<I, B>(&mut self, batch: I)
where
I: IntoIterator<Item = (Entity, B)> + Send + Sync + 'static,
B: Bundle,
B: Bundle<Effect: NoBundleEffect>,
{
self.queue(
command::insert_batch(batch, InsertMode::Keep).handle_error_with(error_handler::warn()),

View File

@ -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::<T>(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)
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<T: Bundle>(&mut self) -> Option<T> {
pub fn take<T: Bundle + BundleFromComponents>(&mut self) -> Option<T> {
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<Item = (StorageType, OwningPtr<'a>)>> 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,7 +4111,8 @@ unsafe fn insert_dynamic_bundle<
// SAFETY: location matches current entity.
unsafe {
bundle_inserter.insert(
bundle_inserter
.insert(
entity,
location,
bundle,
@ -4107,6 +4120,7 @@ unsafe fn insert_dynamic_bundle<
#[cfg(feature = "track_location")]
caller,
)
.0
}
}

View File

@ -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::<B>(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<I>(&mut self, iter: I) -> SpawnBatchIter<'_, I::IntoIter>
where
I: IntoIterator,
I::Item: Bundle,
I::Item: Bundle<Effect: NoBundleEffect>,
{
SpawnBatchIter::new(
self,
@ -2234,7 +2239,7 @@ impl World {
where
I: IntoIterator,
I::IntoIter: Iterator<Item = (Entity, B)>,
B: Bundle,
B: Bundle<Effect: NoBundleEffect>,
{
self.insert_or_spawn_batch_with_caller(
iter,
@ -2254,7 +2259,7 @@ impl World {
where
I: IntoIterator,
I::IntoIter: Iterator<Item = (Entity, B)>,
B: Bundle,
B: Bundle<Effect: NoBundleEffect>,
{
self.flush();
@ -2390,7 +2395,7 @@ impl World {
where
I: IntoIterator,
I::IntoIter: Iterator<Item = (Entity, B)>,
B: Bundle,
B: Bundle<Effect: NoBundleEffect>,
{
self.insert_batch_with_caller(
batch,
@ -2420,7 +2425,7 @@ impl World {
where
I: IntoIterator,
I::IntoIter: Iterator<Item = (Entity, B)>,
B: Bundle,
B: Bundle<Effect: NoBundleEffect>,
{
self.insert_batch_with_caller(
batch,
@ -2444,7 +2449,7 @@ impl World {
) where
I: IntoIterator,
I::IntoIter: Iterator<Item = (Entity, B)>,
B: Bundle,
B: Bundle<Effect: NoBundleEffect>,
{
struct InserterArchetypeCache<'w> {
inserter: BundleInserter<'w>,
@ -2540,7 +2545,7 @@ impl World {
where
I: IntoIterator,
I::IntoIter: Iterator<Item = (Entity, B)>,
B: Bundle,
B: Bundle<Effect: NoBundleEffect>,
{
self.try_insert_batch_with_caller(
batch,
@ -2567,7 +2572,7 @@ impl World {
where
I: IntoIterator,
I::IntoIter: Iterator<Item = (Entity, B)>,
B: Bundle,
B: Bundle<Effect: NoBundleEffect>,
{
self.try_insert_batch_with_caller(
batch,
@ -2596,7 +2601,7 @@ impl World {
where
I: IntoIterator,
I::IntoIter: Iterator<Item = (Entity, B)>,
B: Bundle,
B: Bundle<Effect: NoBundleEffect>,
{
struct InserterArchetypeCache<'w> {
inserter: BundleInserter<'w>,

View File

@ -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<Effect: NoBundleEffect>,
{
#[inline]
#[track_caller]
@ -81,11 +81,15 @@ where
let bundle = self.inner.next()?;
// SAFETY: bundle matches spawner type
unsafe {
Some(self.spawner.spawn(
Some(
self.spawner
.spawn(
bundle,
#[cfg(feature = "track_location")]
self.caller,
))
)
.0,
)
}
}

View File

@ -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<Effect: NoBundleEffect>;
// TODO: https://github.com/rust-lang/rust/issues/29661
// type Out: Component = Self;

View File

@ -334,7 +334,6 @@ mod test {
.get::<Children>(parent)
.unwrap()
.iter()
.cloned()
.collect::<Vec<_>>(),
children,
);
@ -353,7 +352,6 @@ mod test {
.get::<Children>(parent)
.unwrap()
.iter()
.cloned()
.collect::<Vec<_>>(),
vec![children[1]]
);
@ -363,7 +361,6 @@ mod test {
.get::<Children>(children[1])
.unwrap()
.iter()
.cloned()
.collect::<Vec<_>>(),
vec![children[0]]
);
@ -377,7 +374,6 @@ mod test {
.get::<Children>(parent)
.unwrap()
.iter()
.cloned()
.collect::<Vec<_>>(),
vec![children[1]]
);

View File

@ -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());
}
}
}

View File

@ -1119,7 +1119,6 @@ mod tests {
.get::<Children>()
.unwrap()
.iter()
.copied()
.collect::<Vec<Entity>>();
for r in [2, 3, 5, 7, 11, 13, 17, 19, 21, 23, 29, 31].map(|n| (n as f32).recip()) {

View File

@ -118,8 +118,7 @@ fn setup(
});
// red point light
commands
.spawn((
commands.spawn((
PointLight {
intensity: 100_000.0,
color: RED.into(),
@ -127,21 +126,18 @@ fn setup(
..default()
},
Transform::from_xyz(1.0, 2.0, 0.0),
))
.with_children(|builder| {
builder.spawn((
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()
})),
)],
));
});
// green spot light
commands
.spawn((
commands.spawn((
SpotLight {
intensity: 100_000.0,
color: LIME.into(),
@ -151,8 +147,7 @@ fn setup(
..default()
},
Transform::from_xyz(-1.0, 2.0, 0.0).looking_at(Vec3::new(-1.0, 0.0, 0.0), Vec3::Z),
))
.with_child((
children![(
Mesh3d(meshes.add(Capsule3d::new(0.1, 0.125))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: LIME.into(),
@ -160,11 +155,11 @@ fn setup(
..default()
})),
Transform::from_rotation(Quat::from_rotation_x(PI / 2.0)),
)],
));
// blue point light
commands
.spawn((
commands.spawn((
PointLight {
intensity: 100_000.0,
color: BLUE.into(),
@ -172,17 +167,15 @@ fn setup(
..default()
},
Transform::from_xyz(0.0, 4.0, 0.0),
))
.with_children(|builder| {
builder.spawn((
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()
})),
)],
));
});
// directional 'sun' light
commands.spawn((
@ -209,8 +202,7 @@ fn setup(
// example instructions
commands
.spawn((
commands.spawn((
Text::default(),
Node {
position_type: PositionType::Absolute,
@ -218,29 +210,26 @@ fn setup(
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!(
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((

View File

@ -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;

View File

@ -347,8 +347,7 @@ fn spawn_bonus(
game.bonus.j as f32,
),
SceneRoot(game.bonus.handle.clone()),
))
.with_child((
children![(
PointLight {
color: Color::srgb(1.0, 1.0, 0.0),
intensity: 500_000.0,
@ -356,6 +355,7 @@ fn spawn_bonus(
..default()
},
Transform::from_xyz(0.0, 2.0, 0.0),
)],
))
.id(),
);
@ -389,8 +389,7 @@ fn gameover_keyboard(
// display the number of cake eaten before losing
fn display_score(mut commands: Commands, game: Res<Game>) {
commands
.spawn((
commands.spawn((
StateScoped(GameState::GameOver),
Node {
width: Val::Percent(100.),
@ -398,13 +397,13 @@ fn display_score(mut commands: Commands, game: Res<Game>) {
justify_content: JustifyContent::Center,
..default()
},
))
.with_child((
children![(
Text::new(format!("Cake eaten: {}", game.cake_eaten)),
TextFont {
font_size: 67.0,
..default()
},
TextColor(Color::srgb(0.5, 0.5, 1.0)),
)],
));
}

View File

@ -213,8 +213,7 @@ fn setup(
));
// Scoreboard
commands
.spawn((
commands.spawn((
Text::new("Score: "),
TextFont {
font_size: SCOREBOARD_FONT_SIZE,
@ -228,14 +227,14 @@ fn setup(
left: SCOREBOARD_TEXT_PADDING,
..default()
},
))
.with_child((
children![(
TextSpan::default(),
TextFont {
font_size: SCOREBOARD_FONT_SIZE,
..default()
},
TextColor(SCORE_COLOR),
)],
));
// Walls

View File

@ -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()),
@ -144,23 +147,19 @@ fn setup(
.with_scale(Vec2::splat(radius + 2.0).extend(1.0)),
));
// sclera
commands
.spawn((Transform::from_xyz(x, y, 2.0), Visibility::default()))
.with_children(|commands| {
// sclera
commands.spawn((
Transform::from_xyz(x, y, 2.0),
Visibility::default(),
children![
// sclera
(
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 {
@ -168,10 +167,9 @@ fn setup(
pupil_radius,
velocity: Vec2::ZERO,
},
))
.with_children(|commands| {
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,
)),
)
],
)
],
));
});
});
}
});
}

View File

@ -73,8 +73,7 @@ mod splash {
fn splash_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
let icon = asset_server.load("branding/icon.png");
// Display the logo
commands
.spawn((
commands.spawn((
Node {
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
@ -83,17 +82,15 @@ mod splash {
..default()
},
OnSplashScreen,
))
.with_children(|parent| {
parent.spawn((
children![(
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,8 +135,7 @@ mod game {
display_quality: Res<DisplayQuality>,
volume: Res<Volume>,
) {
commands
.spawn((
commands.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
@ -149,11 +145,7 @@ mod game {
..default()
},
OnGameScreen,
))
.with_children(|parent| {
// First create a `Node` for centering what we want to display
parent
.spawn((
children![(
Node {
// This will display its children in a column, from top to bottom
flex_direction: FlexDirection::Column,
@ -164,9 +156,8 @@ mod game {
..default()
},
BackgroundColor(Color::BLACK),
))
.with_children(|p| {
p.spawn((
children![
(
Text::new("Will be back to the menu shortly..."),
TextFont {
font_size: 67.0,
@ -177,42 +168,43 @@ mod game {
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((
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,8 +392,11 @@ 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),
@ -405,20 +405,16 @@ mod menu {
..default()
},
OnMainMenuScreen,
))
.with_children(|parent| {
parent
.spawn((
children![(
Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(CRIMSON.into()),
))
.with_children(|parent| {
children![
// Display the game name
parent.spawn((
(
Text::new("Bevy Game Menu UI"),
TextFont {
font_size: 67.0,
@ -429,62 +425,52 @@ mod menu {
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((
children![
(ImageNode::new(right_icon), button_icon_node.clone()),
(
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((
children![
(ImageNode::new(wrench_icon), button_icon_node.clone()),
(
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),
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,8 +491,7 @@ mod menu {
TextColor(TEXT_COLOR),
);
commands
.spawn((
commands.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
@ -515,57 +500,57 @@ mod menu {
..default()
},
OnSettingsMenuScreen,
))
.with_children(|parent| {
parent
.spawn((
children![(
Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(CRIMSON.into()),
))
.with_children(|parent| {
for (action, text) in [
Children::spawn(SpawnIter(
[
(MenuButtonAction::SettingsDisplay, "Display"),
(MenuButtonAction::SettingsSound, "Sound"),
(MenuButtonAction::BackToMainMenu, "Back"),
] {
parent
.spawn((
]
.into_iter()
.map(move |(action, text)| {
(
Button,
button_node.clone(),
BackgroundColor(NORMAL_BUTTON),
action,
children![(Text::new(text), button_text_style.clone())],
)
})
))
.with_children(|parent| {
parent.spawn((Text::new(text), button_text_style.clone()));
});
}
});
});
)],
));
}
fn display_settings_menu_setup(mut commands: Commands, display_quality: Res<DisplayQuality>) {
let button_node = Node {
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()
};
let button_text_style = (
}
}
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),
@ -574,35 +559,26 @@ mod menu {
..default()
},
OnDisplaySettingsMenuScreen,
))
.with_children(|parent| {
parent
.spawn((
children![(
Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(CRIMSON.into()),
))
.with_children(|parent| {
children![
// 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| {
Children::spawn((
// Display a label for the current setting
parent.spawn((
Text::new("Display Quality"),
button_text_style.clone(),
));
// Display a button for each possible value
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(),
button_text_style(),
)],
));
});
if *display_quality == quality_setting {
if display_quality == quality_setting {
entity.insert(SelectedOption);
}
}
});
})
))
),
// Display the back button to return to the settings screen
parent
.spawn((
(
Button,
button_node,
button_node(),
BackgroundColor(NORMAL_BUTTON),
MenuButtonAction::BackToSettings,
))
.with_children(|parent| {
parent.spawn((Text::new("Back"), button_text_style));
});
});
});
children![(Text::new("Back"), button_text_style())]
)
]
)],
));
}
fn sound_settings_menu_setup(mut commands: Commands, volume: Res<Volume>) {
@ -661,8 +635,9 @@ 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),
@ -671,54 +646,51 @@ mod menu {
..default()
},
OnSoundSettingsMenuScreen,
))
.with_children(|parent| {
parent
.spawn((
children![(
Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(CRIMSON.into()),
))
.with_children(|parent| {
parent
.spawn((
children![
(
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));
});
});
children![(Text::new("Back"), button_text_style)]
)
]
)],
));
}
fn menu_action(

View File

@ -51,20 +51,22 @@ fn button_system(
}
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
// 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((
},
children![(
Button,
Node {
width: Val::Px(150.0),
@ -79,8 +81,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
BorderColor(Color::BLACK),
BorderRadius::MAX,
BackgroundColor(NORMAL_BUTTON),
))
.with_child((
children![(
Text::new("Button"),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
@ -89,6 +90,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
TextShadow::default(),
));
});
)]
)],
)
}