Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Improved Spawn APIs and Bundle Effects #17521

Merged
merged 12 commits into from
Feb 9, 2025
Merged

Improved Spawn APIs and Bundle Effects #17521

merged 12 commits into from
Feb 9, 2025

Conversation

cart
Copy link
Member

@cart cart commented Jan 24, 2025

Objective

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

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 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:

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:

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:

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!

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)):

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:

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:

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.

// 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 {
    }
}

@cart cart added A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use labels Jan 24, 2025
@cart cart added this to the 0.16 milestone Jan 24, 2025
@BenjaminBrienen BenjaminBrienen added D-Complex Quite challenging from either a design or technical perspective. Ask for help! S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jan 24, 2025
@alice-i-cecile alice-i-cecile added the M-Needs-Release-Note Work that should be called out in the blog due to impact label Jan 24, 2025
@alice-i-cecile
Copy link
Member

We probably want to roll this up into / right after the relations release note, but I want to make sure this doesn't get missed when writing them. It's also notable enough that folks skimming the milestone + "Needs-Release-Notes" will be interested.

@alice-i-cecile alice-i-cecile self-requested a review January 24, 2025 03:13
@benfrankel
Copy link
Contributor

benfrankel commented Jan 24, 2025

IMO this is a step in the right direction, making entity spawning constructs more reusable / composable (having only read the PR description). One thing I feel will still be missing after this PR is argument passing ergonomics, since fn ui and fn button aren't systems. But I imagine that can be iterated on independent of the new API.

@alice-i-cecile
Copy link
Member

One thing I feel will still be missing after this PR is argument passing ergonomics, since fn ui and fn button aren't systems. But I imagine that can be iterated on independent of the new API.

Agreed: the key thing there is "dependency injection-flavored access to asset collections". The Construct trait (which I think is Cart's next step after this PR!) should go a long way.

}

/// 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will partially break Box<dyn DynamicBundle>, since you'll need to specify an Effect associated type.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the existing and intended use cases for this?

Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this. The end result is great: like a generic, flexible version of the WithChild component I hacked together, but without the terrible performance and no bizarre type-system gotchas. I prefer the non-macro API, but I don't mind the macro form, and I'm happy to standardize that in the learning material.

Implementation is solid: lots of moving parts, but I don't see any immediate way to simplify them, and the docs are exceptionally clear. Updating the rest of the example should go in follow-up.

This makes impl Bundle the blessed API for spawning entity collections, which is super exciting because it unblocks widgets over in bevy_ui. I'm a little nervous about the lack of boxed trait objects hampering our the flexibility of designs there, but the recently added default query filters + entity cloning should make an entity zoo (prefabs?) approach extremely comfortable.

@vultix
Copy link

vultix commented Jan 24, 2025

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).

I think there's a third option here - breaking the concept of nested bundles into a set of new traits.

The ambiguity arises because we have the following impl:

impl<B: Bundle> Bundle for (B1, B2, ..., Bn) {}

I believe we could remove this impl, and introduce a new trait:

trait BundleList {}
impl<B: Bundle> BundleList for (B1, B2, ..., Bn) {}
impl<B: Bundle> BundleList for B {}

This allows us to use the bundles in BundleList differently depending on the context:

  • World::spawn(impl BundleList) would merge all of the bundles in the list and spawn a single entity
  • Children::spawn(impl BundleList) would spawn a separate child entity for each bundle in the list

We'll also need to adjust the definition of Bundle to be a "list of things that can be spawned on an entity"

/// An `Effect` or a `Component`, something that can be "spawned" on a given entity
///
/// Currently this will either be:
/// - a `Component`
/// - The `SpawnRelated<T>` struct, returned by `RelationshipTarget::spawn`. 
///   `SpawnRelated<T>` has an Effect that spawns the related entities
trait Spawn {}

/// A bundle is a list of `Spawn`, things that can be spawned on an entity
trait Bundle {}
impl<S: Spawn> Bundle for (S1, S2, ..., Sn) {}
impl<S: Spawn> Bundle for S {}

Finally, in cases where we want to support nested bundles, we make use of the BundleList trait:

/// World::spawn creates a new entity with all of the bundles in the BundleList
impl World {
    fn spawn(bundle: impl BundleList);
}

/// `Children::spawn` returns a type that implements `Spawn` that isn't a component, but an Effect
/// The returned `impl Spawn` has an Effect that spawns a new child entity for each Bundle in the BundleList
impl Children {
    fn spawn(entities: impl BundleList) -> impl Spawn {}
}

This set of traits should allow the following to work:

world.spawn((
    Foo,
    (Bar, Baz),
    Children::spawn((
        (Bar, Children::spawn(Baz)), 
        (Bar, Children::spawn(Baz))
    )),
    Observers::spawn((Observer1, Observer2)),
));

@viridia
Copy link
Contributor

viridia commented Jan 26, 2025

There's another aspect that I want to dive deeper into here: the choice of "spawning requires a world" as opposed to "spawning requires commands". Specifically, the BundleEffect APIs pass in a World, which means that setup relies on exclusive access.

My earlier frameworks (quill and bevy_reactor) used a similar approach, but my most recent framework, thorium, uses a commands-based spawning protocol. The reason for this is that I wanted as much as possible for the developer experience of spawning hierarchies to look and feel like the canonical bevy spawning pattern. Making everything commands-compatible was a considerable amount of work on my part, and I'm curious to know whether that is going to be a dead end or not.

Using commands for spawning does have some limitations. The most obvious is that any world dereferences must be lazy. That is, the template only has direct access to the parameters that are passed in to it. If it needs any additional data to populate the entity graph, that data can only be obtained asynchronously. However, because of the automatic flushing of commands, the delay in fetching the data is minimal in most cases.

For example, the InsertWhen primitive I mentioned in the previous post cannot know whether the condition is true or false at the instant when is inserted, so it cannot know at construction time whether to insert the component or not. However, due to the magic of hooks, it can evaluate that condition immediately afterwards.

Of course, the advantage of the commands-based approach is that it doesn't require exclusive access. However, this advantage is highly situational: how many other threads are we blocking? This is hard to evaluate as a general principle.

Right now, all of my UiTemplate implementations accept a ChildSpawnerCommands instance. To make them compatible with the new spawning API, I would need to change this to a ChildSpawner. In some cases, where a ChildSpawnerCommands is needed, it is possible to write an adapter: if you have access to a World, you can get the commands() instance, and from there it's easy to get an instance of ChildSpawnerCommands. But you can't do this if all you have is a struct that has a private World, so in those cases writing an adapter is not possible.

@viridia
Copy link
Contributor

viridia commented Jan 27, 2025

I've coded up another experiment to wrestle with the issues around multi-root and two-phase templates. The code looks like this:

commands.spawn((
    Node::default(),
    BorderColor(css::ALICE_BLUE.into()),
    Children::spawn((
        Hello,
        World,
        Spawn((
            Node {
                border: ui::UiRect::all(ui::Val::Px(3.)),
                ..default()
            },
            BorderColor(css::LIME.into()),
        )),
    )),
));

In this example Hello and World are templates. Hello looks like this:

struct Hello;

impl BundleTemplate for Hello {
    fn build(&self, builder: &mut ChildSpawner) {
        builder.spawn(Text::new("Hello, "));
    }
}

impl_bundle_template!(Hello);

Note that Hello and World are not wrapped in Spawn(). These templates implement SpawnableList, which allows them to output multiple entities. The templates don't have any properties, but they could have, and in most cases will.

I'll be the first to admit that this looks a bit odd. Here's the justification: Spawn() signals that we want to spawn one, and only one, entity. Templates aren't required to abide by this restriction; they can decide for themselves how many entities to spawn. As a result, they are required to implement the SpawnX protocol themselves.

This somewhat echoes the situation in JSX: "native" elements like <div> have a different syntax than "component" elements, which are functions. So in this model, Spawn() signals that we're creating a primitive element, a.k.a an entity.

Unfortunately, this means that templates can only be used with the explicit Relation::spawn() syntax, and won't work with children![], because the latter automatically wraps everything in Spawn().

Also, the reason for the impl_bundle_template macro is to get around the inability to write a blanket implementation of SpawnableList for BundleTemplate.

Note that SpawnableList<R> includes the relation type as part of the signature. This means that even though the template has a choice of how many entities to spawn, it does not have a choice of what relationship type to relate them with. Thus, a template written for Children won't work with another kind of relationship.

@JeanMertz
Copy link
Contributor

This looks great! I do like the implementation, but the user-facing API does look a little clunky and stuttery.

It would be great if we could make @vultix‘s suggestion work, as it does look like a cleaner API, even if it introduces some extra boilerplate for the (I believe) non-standard path.

@cart
Copy link
Member Author

cart commented Jan 27, 2025

Alrighty to rein this in a bit, I will assert that this is definitely not a generic do-anything templating API nor is it intended to be. This is built to be a way to express Bevy's core data model as efficiently and ergonomically as possible. This is the "what data do I want to insert on this entity" API.

Any API that involves enabling "arbitrary infinitely flexible World operations at arbitrary points in the tree" would exist a level above this one. A future API that adds that category of thing would still benefit from the ability to efficiently spawn raw hierarchies of entities and components.

To quote myself from Discord:

I'm currently of the position that we should avoid anything in the "component position" that implicitly creates hierarchy, or generally does anything "unexpected" from the perspective of "a static hierarchy of Components". I would like to explore what holding that line looks like before letting the "anything can do anything" genie out of the bottle.

This is not the place to implement letting the genie out of the bottle (or to win the argument that we should).

@viridia

There's another aspect that I want to dive deeper into here: the choice of "spawning requires a world" as opposed to "spawning requires commands". Specifically, the BundleEffect APIs pass in a World, which means that setup relies on exclusive access.

First, I will note that spawning does require a World functionally. Commands exist to solve the "what if I want to write to world when I am blocked from doing so" problem. If we are currently spawning, we are not blocked from writing to the world. If we want to spawn something when we are blocked from writing to the world, we can just queue the spawn action as a Command.

I'll also note that this PR is the direct exclusive access API. It is built with the expressed intention of being the efficient thing that anything can call (including commands). That doesn't stop us from building a deferred-only API on top (although I would hesitate to embrace any such deferred-only API as the user-facing data-definition API as it would force that paradigm globally and at every level of the hiearchy when that feels unnecessary and inefficient).

I'm curious to know whether that is going to be a dead end or not.

Given that it forces deferred changes where it is not necessary, and given that any direct-world spawn action is trivially convertable to a Command when it needs to be applied in a deferred way, I would consider a Commands-first API a dead end. We get to choose what "canonical spawning" in Bevy looks like. Currently that does use commands more often than not (as that is what is easiest for developers in the current paradigm), but that doesn't mean it should going forward when we introduce the next paradigm.

It means that you are forced to group together components based on their implementation, rather than on their meaning.

My general angle on this is that if the meaning is misaligned with the implementation, we should reconsider the implementation. If the Owned relationship is "too general" and results in grouping together things that should not be grouped, then we should reconsider that.

I would very much like to live in a World where Bevy's developer-facing data-definition API is the actual Bevy ECS API. If we need to build abstractions that hide the shape of the data on top of our data layer to make it "usable", I'm (currently) of the mind that we need to rethink our data layer. The data being what it "looks like" is a UX concern. Papering over our data layer may improve UX in this specific category, but it makes the system as a whole much harder to reason about, debug, and extend.

Users now have to properly nest components under their correct relationships. For example, if you accidentally place MutateDyn within children instead of owned, it will panic, because it relies on the ownership relation to find the target entity. (In the earlier example, the constructor for MutateDyn had access to the target entity id at construction time, but in the latter example this is no longer true).

I agree that this opens up a class of error, but this would (naively) be true for any API that directly expresses relationships (and given that this PR is about directly expressing relationships, I think that settles this within the scope of this PR). That being said, there are multiple paths we can take:

  1. Abstract out relationships entirely (this what you are generally arguing for). This has the benefit of removing this class of logic error at the cost of hiding the realities of the situation from the user.
  2. Allow this category of "misrelation". If you put an entity in the wrong bucket, that is a logic error.
  3. Add some form of runtime validation where necessary (ex: an ObserverOf without an Observer component is a runtime warning or error).
  4. Add some form of static compile-time validation (this would be gnarly if its even possible).

But yeah these are all good conversations to have, but this is not the venue for it imo.

@cart
Copy link
Member Author

cart commented Jan 27, 2025

Also I'll explore the "un-nested BundleList" approach. We might want to kick that to a future PR as it would touch a lot of things, but if it isn't toooo invasive we can consider it.

@viridia
Copy link
Contributor

viridia commented Jan 28, 2025

First, apologies for my long-windedness, my ideas have been evolving at a rapid pace and I've been writing things down as I go.

After some tinkering, I've realized that is is indeed possible (and in fact easy) to use the API in this PR to build a templating solution that meets my most important requirements. It looks like this:

commands.spawn((
    Node::default(),
    BorderColor(css::ALICE_BLUE.into()),
    Children::spawn((
        Invoke(Hello),
        Invoke(Button::new().labeled("Click me")),
        Spawn((
            Node {
                border: ui::UiRect::all(ui::Val::Px(3.)),
                ..default()
            },
            BorderColor(css::LIME.into()),
        )),
    )),
));

Like Spawn(), Invoke() implements SpawnableList, but adds a layer of indirection: the templates Hello and Button are free to spawn as few, or as many entities as they like, and can construct them atomically or piecemeal as needed. This solves the two-phase construction issue I mentioned earlier.

Now, I know that you have said that you want to defer thinking about the issue of ghost nodes and reactions, however I have been tinkering with that too, and the result is a solution that is somewhat invasive of what you've laid out here. However we can discuss that elsewhere.

@viridia
Copy link
Contributor

viridia commented Jan 28, 2025

@cart It would be useful to have some helper methods for working with lazily-constructed SpawnableLists.

Take this example:

Button::new().caption(|| Spawn(Text::new("Default"))) // One child
Button::new().caption(|| (Spawn(Text::new("Close"), Icon::new(CloseIcon)))) // Multi-child

The reason I don't simply pass in Spawn(...) directly is because templates aren't required to consume their arguments, and I don't want to construct a bundle that may or may not get used. In the case of a button, the bundle will always be used, but for something like a dialog or menu it might not be used until opened, and it might be used more than once. To keep things consistent, I'm treating all such parameters the same way.

(In a real templating system caption would be a reference to a template instance.)

Internally, within the button I need to hold a reference to the closure as something like Option<Box<dyn SpawnableListGen>> or Option<Arc<dyn SpawnableListGen>>, where SpawnableListGen impls Fn() -> SpawnableList<R>.

Within the button template, I need to be able to use this parameter, so I'd need to have something like:

builder.spawn((
    // (other button stuff)
    Children::spawn((
        // (other button decoration children)
        SpawnIndirect(self.children),
    ))
))

Unfortunately, I have not figured out how to get this to work; I keep getting "not a bundle" errors.

Update: I figured it out.

@alice-i-cecile alice-i-cecile added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jan 29, 2025
@alice-i-cecile
Copy link
Member

I hit this in my merge train today; letting you press the button when you're ready @cart.

@cart
Copy link
Member Author

cart commented Feb 9, 2025

@vultix a major issue with the proposed BundleList approach is that impl<B: Bundle> BundleList for B {} conflicts with all other BundleList impls (ex: impl<B1: Bundle, B2: Bundle> BundleList for (B1, B2) {}). If we were to use BundleList in our spawn API, it would necessitate using a (B1, ) tuple for single bundle spawns.

Rust Playground Example

One way around it is to impl<C: Component> BundleList for C instead of for Bundle to avoid conflicts. But then single-element spawns have confusing restrictions that multi-element spawns dont. That, when combined with the inherent "weirdness" of "BundleList can either behave like a bundle or a list of bundles" and I'm less inclined to go down this path. At the very least, I'm convinced this PR isn't the venue for this change.

@vultix
Copy link

vultix commented Feb 9, 2025

@cart that makes sense. One workaround is to have the Bundle derive automatically derive BundleList as well, but that leaves a foot gun for manual Bundle implementations

@vultix
Copy link

vultix commented Feb 9, 2025

Although we could make that a compiler error by making Bundle: BundleList!

I think that saves all of the ergonomics I care about. derive(Bundle) implements BundleList, impl<C: Component> BundleList for C captures all components, and then manual Bundle impls get a compiler error if they forget to implement BundleList as well.

Thoughts?

@cart
Copy link
Member Author

cart commented Feb 9, 2025

Hmm yeah that does work. Worth exploring, but it does make this zoo of impls even harder to reason about. It would also be interesting to see the effect this would have on our codegen / compile times.

Edit: here it is with a Children::spawn() scratch impl: Rust Playground.

@vultix
Copy link

vultix commented Feb 9, 2025

I think it’s definitely worth considering, but in another PR.

We’d need to carefully weigh the complexity tradeoff in return for not requiring macros to spawn relations. Compile times are definitely a concern as well

@cart cart enabled auto-merge February 9, 2025 23:31
@cart cart added this pull request to the merge queue Feb 9, 2025
Merged via the queue into bevyengine:main with commit ea57841 Feb 9, 2025
28 checks passed
mrchantey pushed a commit to mrchantey/bevy that referenced this pull request Feb 17, 2025
## 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](bevyengine#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 <[email protected]>
Co-authored-by: Gino Valente <[email protected]>
Co-authored-by: Emerson Coskey <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use D-Complex Quite challenging from either a design or technical perspective. Ask for help! M-Needs-Release-Note Work that should be called out in the blog due to impact S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers
Projects
None yet
Development

Successfully merging this pull request may close these issues.