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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open

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.

crates/bevy_ecs/src/bundle.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/hierarchy.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/spawn.rs Outdated Show resolved Hide resolved
@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)),
));

@cart
Copy link
Member Author

cart commented Jan 24, 2025

@vultix

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

When we hit the n size limit, wouldn't we hit a dead end, because we can't nest another BundleList inside of a BundleList?

@vultix
Copy link

vultix commented Jan 24, 2025

@cart

When we hit the n size limit, wouldn't we hit a dead end, because we can't nest another BundleList inside of a BundleList?

For spawning with World::spawn I don't think that'd ever be an issue, because that means you've tried spawning 16 bundles, each with potentially 16 types implementing Spawn (e.g. 16 bundles with 16 Components = 256 components).

The only place I'd see this being an issue is Children::spawn(), as you'd be limited to spawning no more than 16 child entities.

For the rare cases where you need more than 16, we could introduce a bundles![] macro that allows making lists longer than 16?

#[macro_export]
macro_rules! children {
[$($child:expr),*$(,)?] => {
Children::spawn(($($crate::spawn::Spawn($child)),*))
Copy link
Member

Choose a reason for hiding this comment

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

This still needs a $crate path:

Suggested change
Children::spawn(($($crate::spawn::Spawn($child)),*))
$crate::hierarchy::Children::spawn(($($crate::spawn::Spawn($child)),*))

Copy link
Member

@mnmaita mnmaita left a comment

Choose a reason for hiding this comment

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

I'm very glad this will be a thing soon! Leaving a minor suggestion.

Would also be interesting to give the option presented by vultix some more thought, as it looks like it'd simplify a few things. No macros/wrappers to build the related entities but having to use macros/wrappers when we're exceeding some tuple limit feels much more approachable. In my experience this is more of a corner case so it seems appropriate to add the "burden" there.

/// Children::spawn_one(Name::new("Child")),
/// ));
/// ```
fn spawn_one<B: Bundle>(bundle: B) -> SpawnOneRelated<Self::Relationship, B>;
Copy link
Member

@mnmaita mnmaita Jan 25, 2025

Choose a reason for hiding this comment

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

I know it's a longer name but IMHO spawn_single sounds better since it "spawns a single entity containing Bundle" as per the docs. I think it's also easier to reason about it since we already have Single and single methods to get entities from queries.

@viridia
Copy link
Contributor

viridia commented Jan 25, 2025

Now that I have had a chance to work with this extensively, I have a bunch of feedback items. I've discussed some of this already on Discord, but I want to gather it all in one place, especially given that this is going to be a long post.

TL;DR: For constructing individual entities, I'm able do pretty much all of the things I want. However, I've come to the conclusion that I can't build a complete templating solution within the restrictions of the API.

Checkbox Example

Let's look at a concrete example. The Checkbox widget uses a number of dynamic components, specifically InsertWhen and MutateDyn:

let mut checkbox = builder.spawn((
    Node { ..default() },
    Hovering::default(),
    Name::new("Checkbox"),
    Styles((style_checkbox, self.style.clone())),
    TabIndex(self.tab_index),
    CoreCheckbox {
        checked: false,
        on_change: self.on_change,
    },
    MutateDyn::new(
        move |world: DeferredWorld| checked.get(&world),
        |checked, ent| {
            let mut checkbox = ent.get_mut::<CoreCheckbox>().unwrap();
            checkbox.checked = checked;
        },
    ),
    InsertWhen::new(
        move |world: DeferredWorld| disabled.get(&world),
        || InteractionDisabled,
    ),
));
let checkbox_id = checkbox.id();

Dynamic Mutation Components

Within this example, there are three components that use the new API:

  • Styles is simply a poor-man's Patch, and will eventually not be needed with BSN. It's primary function is to allow the caller of the checkbox to pass in additional styles to the checkbox component. The implementation of Styles is an after effect that passes an EntityCommands instance to a sequence of callbacks, allowing arbitrary changes to the entity once it has been created. I know that this is very much abusing the intent of after-effects, but I'm deliberately pushing boundaries here.
  • MutateDyn is a reactive primitive: the first argument is a one-shot system which returns a value. This system is run every frame and the value stored. Whenever this value changes, the second argument, a closure, is run. This second closure is passed the output of the first function along with an EntityWorldMut, which can use it to update the state of the entity, in this case setting the internal "checked" property.
  • InsertWhen is another reactive primitive: like MutateDyn, the first argument is a one-shot system that is run every frame. The output is a boolean, and this is used to determine whether the component (as determined by the second argument) should be inserted or removed.

Both MutateDyn and InsertWhen create a helper satellite entity, much like observers do. The reason for this is to get around the limitation that you can only have one component of a given type. Many widgets have more than one MutateDyn or InsertWhen, and this would not be possible if they were ordinary components.

The satellite entity is linked to the primary entity using an Owned relationship. Owned is similar to Children or Observers except that it has no semantics - it only controls lifetime.

In this example, the creation of the satellite entity and the relationship is done within the after effect, meaning that this is hidden from the user.

Now, let's look at a slightly different example:

let mut checkbox = builder.spawn((
    Node { ..default() },
    Hovering::default(),
    Name::new("Checkbox"),
    Styles((style_checkbox, self.style.clone())),
    TabIndex(self.tab_index),
    CoreCheckbox {
        checked: false,
        on_change: self.on_change,
    },
    owned![
        MutateDyn::new(
            move |world: DeferredWorld| checked.get(&world),
            |checked, ent| {
                let mut checkbox = ent.get_mut::<CoreCheckbox>().unwrap();
                checkbox.checked = checked;
            },
        ),
        InsertWhen::new(
            move |world: DeferredWorld| disabled.get(&world),
            || InteractionDisabled,
        ),
    ]
));
let checkbox_id = checkbox.id();

In this example, we have changed how MutateDyn and InsertWhen work: they no longer spawn a satellite entity or set up a relationship. Instead this is done explicitly with the owned! macro.

This approach is more in line with @cart 's vision: it explicitly shows the user what the actual hierarchy is going to look like, instead of hiding things under the hood. It's also more efficient: the Owner relationship is allocated up front instead of being added incrementally.

However, it also has some drawbacks:

  • 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).
  • It means that you are forced to group together components based on their implementation, rather than on their meaning.

In general the pros and cons of the two approaches really depend on what the user cares about. Here's a metaphor: the smooth flowing skin of a sports car hides all the nasty details of engines, crankshafts and grease. Most drivers would prefer not to think about the mechanics of what's going on under the hood, and don't want to see all that stuff. However, some car enthusiasts are very interested in engines and such, and do care about what's happening.

By the same token, many users don't care about how observers work, they just want to call observe() and have things work. However, if a user is manipulating observers via ECS modifications, they do care about the exact hierarchy that is created.

Checkbox Example Part 2

I only showed the first part of the checkbox construction. The next part looks like this (many details omitted):

checkbox.insert(
    StyleDyn::new(
        move |world: DeferredWorld| world.is_focus_visible(checkbox_id),
        |is_focused, ec| {
            if is_focused {
                ec.insert(Outline {
                    color: colors::FOCUS.into(),
                    width: ui::Val::Px(2.0),
                    offset: ui::Val::Px(2.0),
                });
            } else {
                ec.remove::<Outline>();
            };
        },
    )),

This code is responsible for placing a focus rectangle (using Outline) when the checkbox has keyboard focus. It uses StyleDyn, another reactive primitive that works similarly to MutateDyn.

The important thing to note here is the use of checkbox_id. In order to determine if the widget has focus, we need to compare the entity id with the current global focus id. However, the entity id is not known until the checkbox entity is spawned. Later on in the code there is a similar dynamic style for hovering, which also relies on the id.

The checkbox id is captured by the closure. There's no way to inject it as a SystemParam.

This means that the checkbox widget cannot be created all in one shot: it requires a two-phase initialization. This will be important later.

Template Friction

The PR allows returning bundles from functions. However, this is not sufficient for templating.

  • Because the function returns an impl Bundle, it has no access to the constructed entity. This makes the two-part initialization that I mentioned earlier impossible.
  • Also, the function can only construct one root entity. In real-world templates, a single root is by far the most common case in UI work, but you occasionally see templates that construct more than one, or even zero entities. In non-UI scenes, templates that produce multiple root entities is likely to be fairly common ("load squad of goblins").
    • Zero-output templates are sometimes seen in frameworks like React or Solid which have a strong lifecycle model: the idea is that you want to run some side-effect using the lifecycle hooks. Whether this would have any applicability in Bevy is uncertain.
  • Most widgets have a large number of parameters, too many for positional arguments. Typically most of these params will be defaults. The obvious solution is to use a parameter struct, e.g. ButtonParams. However, now you have redundancy: button(ButtonParams { ... }). My preference is to "cut out the middleman" and simply have a Button struct rather than a button() function.

In my previous frameworks, the Button struct could be initialized using a struct literal, but is most often initialized using fluent methods. This takes advantage of impl Into to cut down on needless boilerplate.

Button::new()
    .variant(ButtonVariant::Primary)
    .labeled("Primary"),

For example, the disabled parameter can either be a constant or a Signal. The disabled() method has a method signature that can convert a constant into a signal, so you can either say Button::new().disabled(true) or Button::new().disabled(is_disabled_signal). Without this magic, you'd have to explicitly wrap the constant: Button::new().disabled(Signal::Constant(true)).

It would be possible to adapt this struct-based approach by adding an explicit .into() method on the template instance which returns an impl Bundle, but unfortunately this doesn't get around the limitations I mentioned earlier.

An alternate approach would be for the template to use an after-effect. This at least would let us get around the two-phase construction problem, but it doesn't address the issue of multiple roots, since an after-effect can't control how many entities are spawned, it can only control what goes into the entities after they are spawned.

Also, we can't make a blanket impl for all templates (at least, I tried and wasn't able to). This means that each template will have to derive DynamicBundle and BundleEffect separately.

@villor
Copy link
Contributor

villor commented Jan 25, 2025

@viridia

The satellite entity is linked to the primary entity using an Owned relationship. Owned is similar to Children or Observers except that it has no semantics - it only controls lifetime.

What's the reason to not use Children for the satellites? Seems like that is essentially what the "main hierarchy" would be for. Ownership and "a reflection of what your template looks like". That would also help with the "using the wrong macro" issue. Is Owner really necessary for this?

I do realize one might want to keep the hierarchy "clean". If it is a UI hierarchy - its all Node's. However, now that we have relationships, maybe a ChildNodes-relationship is in order... Could be a derived relationship which by default uses Children to resolve ChildNodeOf as the nearest Node ancestor. But with the option to override ChildNodeOf for things like portal-scenarios (notification in corner, modals, unit frame child of monster but layouted in a UI container somewhere else etc).

Actually the same thing for Transform would be pretty neat. Imagine being able to attach a child of entity A as a TransformChild of entity B (which is not part of A’s descendants)

Also, the function can only construct one root entity.

ChildNodes could also solve this one I guess, essentially implicit ghost nodes. Wouldn't an instanced reactive template still need a single main (parent) entity to hold the context for its multiple "root" entities anyway? Being the owner of any state/memos etc that is used among the children

@viridia
Copy link
Contributor

viridia commented Jan 26, 2025

@villor As several have mentioned, in React the problem is solved via "Fragment" nodes. These are essentially the same as ghost nodes, except for lifecycle: in a fragment, the "flattening" is done as a conversion from the VDOM to the DOM, whereas with ghost nodes the flattening is performed continuously, each time the tree is traversed.

Note BTW that ghost nodes as they exist today won't solve our problem, or rather they only solve the UI case, but don't provide a general solution. Ghost nodes are currently only interpreted by the UI subsystems (layout and picking). 3D scenes ignore ghost nodes because 3D scenes don't care if there are intermediate nodes (SpatialBundle and friends).

However, for BSN there are other use cases: for example, suppose we want to represent actor goal nodes as an entity graph, such that an actor's behavior is a composition of multiple BSN resources, so that the various prioritized goals are merged together. If you used a ghost node inside a resource like this, it wouldn't work unless the goal tree walker used the ghost-aware traversal methods (which you wrote).

So a different approach would be to get rid of ghost nodes entirely and replace them with fragments. This would require having two different "children" relationships (which, fortunately, is easy to do now). That is, your template would construct a node with a "source children" which can be a mix of normal entities and fragments (a fragment being exactly the same as a ghost node - an entity with a marker, and some children). Then at some point the source children is transformed into a list of "flat children", that is, a flat list of children with all of the fragments flattened into a normal Vec.

All of the normal systems that traverse children relationships would now look at the "flat children", and would not need any special logic for ghost nodes / fragments. And it works across all types of hierarchies, not just UI or 3D scenes.

Fragment root nodes could also work: if a fragment has no parent, then its children are never flattened, meaning that they are not part of any flat children list. So even though they are part of a "source children" relationship, they behave as roots in all other ways.

Templates could return either regular nodes or fragments, since a fragment is just an entity. So this solves the problem of templates returning more than one root entity.

However, this idea does come with some downsides:

  • Template users would need to know to specify "source children" instead of flat children in their templates (We need a better name than "source children" but I can't think of one at the moment).
  • If an entity has "source children", then it becomes an error to try and manipulate the flat children directly, because any changes to flat children will be blown away next time they are sync'd to the current state of source children. (If an entity has no source children relationship, then presumably it's safe since no synchronization will ever occur).
  • We would need some helper system that does the synchronization / flattening. I'm guessing that the "source children" will have a change bit, like any component. However, we would also need to propagate any changes to fragments upwards. An open question is in which execution stage this should occur.
  • It's possible that there could be a delay between the time that the source children relationships are updated and the time in which the flat children is produced. We would want to arrange things to minimize this delay, but without spending too many cycles syncing the flat list.
  • Also, there may be a brief period in which there are dangling references: if an entity is removed from the source children list and despawned, the flat children list will still contain the entity id of the despawned entity until the two lists are synchronized.

Under this approach, all existing users of ghost nodes (Cond, ForEach and so on) would instead generate fragments. Note that this means, however, that if you try and use these control-flow nodes within regular (flat) children, they won't work.

@villor
Copy link
Contributor

villor commented Jan 26, 2025

@viridia It seems like we are on the same track here, but from different angles. Correct me if I'm wrong, but it seems like you want the output of the template to be the "flat children" (e.g. the DOM), while I think that the template should output "source children". In other words "source children" is Children. The template/reactivity implementation shouldn't have to concern itself with concepts like UI layout (DOM) or 3D transform propagation.

A clarification on derived hierarchies

What I am proposing here would be a built-in way to define derived relationships/hierarchies. I have to admit I haven't thought this through completely. But I imagine something like this, for the UI/Node-hierarchy:

#[derive(Component)]
#[relationship(relationship_source = NodeChildren)]
#[derive_hierarchy(derive_from = Children, marker = Node)]
struct NodeChildOf(Entity);

A derived relationship would be a filtered version of the derive_from hierarchy based on a marker component. Essentially caching the filtered hierarchy. These derived relationships would be updated directly when their derive_from is updated, eliminating the "helper systems"/"delay" problems.

This would completely replace the current GhostNode and its inefficient traversal APIs. Instead the UI systems could be reverted to pre-ghost nodes traversal, but instead of using Parent/Children it would use NodeChildOf/NodeChildren.

The same could possibly be done for Transform/Visibility propagation. The goal being to decouple those hierarchy-dependant systems from the "scene hierarchy" (aka Children).

This allows the developer/scene designer to group entities (implicit fragments) in any way they like in their scenes/templates, while also getting the expected UI layout/spatial propagation that they are used to. Reactive "auxilary" entities like Cond would just be a natural part of the scene hierarchy, not requiring any extra relationships.

Ideally, derived relationships would also allow:

  • Overrides - overriding a NodeChildOf could be useful for having Node's spawned/despawned with a template, but layouted somewhere else. For example a Boss-entity could have a (HealthBar, NodeChildOf(unit_frames_container)). Same goes for Transform. A monster could cast a spell which applies something visual to the player entity (SpellEffect, TransformChildOf(player)). The spell is visually a child of the player, but despawned with/reactively owned by the Boss.
  • Termination - terminating a derived hierarchy could be useful for things like UiLayer, allowing termination of one UI hierarchy, to allow the next Node descendant to be treated as a root node. Possibly this could be done using another marker like Terminate<NodeChildren>. EDIT: Or possibly combined with overrides, e.g. NodeChildOf(None).

And of course the future scene inspector would need to have a nice way to switch between "Scene hierarchy", "UI hierarchy (DOM)", "Spatial (3D/2D) hierarchy" etc.

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

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-Needs-Review Needs reviewer attention (from anyone!) to move forward
Projects
None yet
Development

Successfully merging this pull request may close these issues.