-
-
Notifications
You must be signed in to change notification settings - Fork 3.8k
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
Conversation
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. |
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 |
Agreed: the key thing there is "dependency injection-flavored access to asset collections". The |
} | ||
|
||
/// 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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this 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.
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
We'll also need to adjust the definition of /// 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 /// 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)),
)); |
Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: Gino Valente <[email protected]> Co-authored-by: Emerson Coskey <[email protected]>
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 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 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 |
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 struct Hello;
impl BundleTemplate for Hello {
fn build(&self, builder: &mut ChildSpawner) {
builder.spawn(Text::new("Hello, "));
}
}
impl_bundle_template!(Hello); Note that I'll be the first to admit that this looks a bit odd. Here's the justification: This somewhat echoes the situation in JSX: "native" elements like Unfortunately, this means that templates can only be used with the explicit Also, the reason for the Note that |
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. |
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:
This is not the place to implement letting the genie out of the bottle (or to win the argument that we should).
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).
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.
My general angle on this is that if the meaning is misaligned with the implementation, we should reconsider the implementation. If the 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.
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:
But yeah these are all good conversations to have, but this is not the venue for it imo. |
Co-authored-by: Gino Valente <[email protected]>
Also I'll explore the "un-nested |
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 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. |
@cart It would be useful to have some helper methods for working with lazily-constructed 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 (In a real templating system Internally, within the button I need to hold a reference to the closure as something like 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. |
I hit this in my merge train today; letting you press the button when you're ready @cart. |
@vultix a major issue with the proposed BundleList approach is that One way around it is to |
@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 |
Although we could make that a compiler error by making I think that saves all of the ergonomics I care about. Thoughts? |
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 |
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 |
## 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]>
Objective
A major critique of Bevy at the moment is how boilerplatey it is to compose (and read) entity hierarchies:
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 allRelationshipTarget
impls, which looks like this in practice:Children::spawn
returnsSpawnRelatedBundle<Children, L: SpawnableList>
, which is aBundle
that insertsChildren
(preallocated to the size of theSpawnableList::size_hint()
).Spawn<B: Bundle>(pub B)
implementsSpawnableList
with a size of 1.SpawnableList
is also implemented for tuples ofSpawnableList
(same general pattern as the Bundle impl).There are currently three built-in
SpawnableList
implementations: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:For the single-entity spawn cases,
Children::spawn_one
does also exist, which removes the need for the wrapper: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!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 newchildren![X]
macro, which internally producesChildren::spawn(Spawn(X))
:There is also a
related!
macro, which is a generic version of thechildren!
macro that supports any relationship type:Returning Hierarchies from Functions
Thanks to these changes, the following pattern is now possible:
Additional Changes and Notes
Bundle::from_components
has been split out intoBundleFromComponents::from_components
, enabling us to implementBundle
for types that cannot be "taken" from the ECS (such as the newSpawnRelatedBundle
).NoBundleEffect
trait (which implementsBundleEffect
) 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.children!
form whenever possible (and for cases that require things like SpawnIter, use the raw APIs).Relationship
to spawn (ex:ChildOf::spawn(Foo)
) instead of theRelationshipTarget
(ex:Children::spawn(Spawn(Foo))
)?". That would allow us to remove theSpawn
wrapper. I've explicitly chosen to disallow this pattern.Bundle::Effect
has the ability to create significant weirdness. Things inBundle
position look like components. For exampleworld.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 thatBundle::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
Next Steps
children!
where possible and rawSpawn
/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()
. AdditionallyBundle::from_components
has been moved to the newBundleFromComponents
trait.