-
-
Notifications
You must be signed in to change notification settings - Fork 4k
feat(bevy_ecs): Allow attaching/spawning observers as a Bundle via Bundle Effects #19613
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
feat(bevy_ecs): Allow attaching/spawning observers as a Bundle via Bundle Effects #19613
Conversation
Welcome, new contributor! Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨ |
It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note. Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes. |
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
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.
Wow, very cool.
Requests:
- Add this to the observer_overhaul.md release note.
- Add this to the docs for
Observer
and theobserver
module. - Mention
Observer::bundle
in theBundleEffectFn
docs. - Demo / explain in
Observer::bundle
why this is useful.
@SkiFire13 can I get your eyes on the unsafe bits here? |
The unsafe bits looks fine to me, as the safety requirements are trivially satisfied because the bundle includes no components and hence never calls the closures given to it. However I'd like to see a more flexible version of |
/// powerful modifications to the world. Bundle Effects also power other Bevy | ||
/// helper bundles like [`crate::spawn::SpawnIter`] and [`crate::spawn::SpawnWith`] | ||
pub struct BundleEffectFn<F>(pub F) | ||
/// Note: [`crate::component::Component`] Hooks are almost as powerful as |
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 think this advice needs workshopping. You can do structural changes inside of hooks. The real tradeoff to me IMO is "always needed" vs "optionally needed" behavior. Perf might also be impacted, but I would really really want to see benchmarks before comparing.
crates/bevy_ecs/src/bundle.rs
Outdated
/// // SAFETY: We don't modify the entity's position so this is safe | ||
/// let world = unsafe { entity.world_mut() }; | ||
/// | ||
/// // This is not idempotent |
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 don't think we should demo this in a doc test unless we can do it right.
/// | ||
/// Example, using a [`BundleEffect`] to add a system to the [`crate::schedule::PostUpdate`] | ||
/// | ||
/// ```rust |
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 doc test is going to need imports to pass CI :)
/// }) | ||
/// ) | ||
/// ``` | ||
pub fn as_bundle<E, B, M, O>(observer: O) -> impl Bundle |
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 don't like this name: it leaks too many implementation details and implies that the observer component is added to the same entity. Something like Observe::watch_this
(uh, please bikeshed) would much more clearly communicate the intent in this code snippet.
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.
Would this have any overlap with an On(|trigger: Trigger<...>| {})
BundleEffect like the on in the BSN prototype (https://github.com/bevyengine/bevy_editor_prototypes/blob/5dcfd5e19177f236379147f855383bfc75f761f1/crates/bevy_proto_bsn/src/bsn_helpers.rs#L41-L77)? I, personally, feel like this is a little weird to have implemented on the Observer
struct and maybe would make more sense as it's own thing.
/// BundleEffect can be implemented directly or using the implementation for `FnOnce(&mut EntityWorldMut)`. | ||
impl<E> BundleEffect for E | ||
where | ||
E: FnOnce(&mut EntityWorldMut) + Send + Sync + 'static, |
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.
Is Send + Sync + 'static
necessary here? They aren't required by the BundleEffect
trait itself.
E: FnOnce(&mut EntityWorldMut) + Send + Sync + 'static, | |
E: FnOnce(&mut EntityWorldMut), |
|
||
unsafe impl<F> Bundle for Patch<F> | ||
where | ||
F: FnOnce(&mut EntityWorldMut) + Send + Sync + 'static, |
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.
The DynamicBundle
impl uses the general BundleEffect
while this uses the more specific FnOnce
bound. They should probably be the same.
F: FnOnce(&mut EntityWorldMut) + Send + Sync + 'static, | |
F: BundleEffect + Send + Sync + 'static, |
Note that SpawnRelatedBundle
and SpawnOneRelated
have type Effect = Self
, which means this will make Patch(Children::spawn_one(things))
compile. I think that's harmless, though?
/// }) | ||
/// ) | ||
/// ``` | ||
pub fn as_bundle<E, B, M, O>(observer: O) -> impl Bundle |
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 see the appeal and value of this PR, but it sadly violates some "key principles" that I would like to retain in the Bundle API:
- Things that are not components on entities should not appear to be components on entities. Put another way: bundles should always be thought of as collections of components added to the current entity. The
Observer
being added in the example above is in the "component position". Someone without intimate knowledge of the inner working of this would very likely assume that they are adding an Observer component to the entity in the Bundle. This is not what is happening. By enabling this sugar, we are making the system significantly harder to reason about. Notably, theChildren::spawn()
API (which motivated the bundle effect API) returns a bundle that inserts the Children component on the entity, so it does not violate that principle. - BundleEffects should be written by people who are more likely to enforce (1): In Improved Spawn APIs and Bundle Effects #17521 I intentionally did not add sugar to make it easier for users to define BundleEffects for custom bundles because it would be easy (and desirable) for the community to violate this principle. By adding Patch, we're encouraging the community violate this principle (which they will want to do, as evidenced by this PR).
Again, I understand why people want this. But this is attempting to turn Bundle into an "arbitrary templating abstraction", which blurs the conceptual model (specifically, how hierarchies are formed) in a way that I think will be detrimental to peoples' understanding of the system. I'm even shooting for BSN, which is Bevy's planned "arbitrary templating abstraction", to do its best to keep the "perceived data model" intact.
For this specific case, we already have plans for how to make observers better:
- Make inline-component-driven observers viable by implying observer targets via relationships:
commands.spawn((
Node::default(),
Text::from("Not clicked"),
// This adds the Observers component to the current entity and spawns the given Observers
// as related entities
observers![|trigger: On<Pointer<Click>>, mut text: Query<&mut Text>| {
**text.get_mut(trigger.target).unwrap() = "Clicked!".to_string();
}]
))
This builds on Bevy's existing data model without introducing ambiguities or confusion.
- Use BSN to further improve this experience:
bsn! {
Node,
Text("Not clicked"),
Observers [|trigger: On<Pointer<Click>>, mut text: Query<&mut Text>| {
**text.get_mut(trigger.target).unwrap() = "Clicked!".to_string();
}]
}
And if for some reason we decide that we want inline "arbitrary effect templates" (such as "spawn an observer and relate it to the current entity), I think we should use a specific syntax in BSN to ensure the integrity of the conceptual model:
bsn! {
Node,
Text("Not clicked"),
@On(|trigger: Trigger<Pointer<Click>>, mut text: Query<&mut Text>| {
**text.get_mut(trigger.target).unwrap() = "Clicked!".to_string();
})
}
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.
Once BSN lands part of me wants to remove BundleEffect entirely in favor of BSN templates (in the interest of preserving the principles of the system / preventing the community from accidentally violating them). However being able to define hierarchies without BSN templates is nice, so I'm not currently planning of advocating for that.
Closing this based on my comment here: #19613 (comment) |
Objective
Solution
Introduced for
SpawnIter
andSpawnWith
, Bundle Effects are a powerful tool for manipulating the ECS world when a bundle is inserted. This is currently used in the aforementioned children spawning helper methods and has also be extensively used by the community for spawning observers inline. This helps simplify building complex UI by allowing the use of React style components offn(props) -> impl Bundle
. Without it, the relevant Bundle must be spawned so its entity ID can be obtained and then the observer added.Testing
Showcase
Example Usage for a tooltip bundle that can be mixed into other UI nodes