-
-
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
Changes from all commits
a4c33a0
4f54804
0032455
47f7649
bd5cd36
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -271,6 +271,16 @@ pub trait BundleEffect { | |||||
fn apply(self, entity: &mut EntityWorldMut); | ||||||
} | ||||||
|
||||||
/// 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, | ||||||
{ | ||||||
fn apply(self, entity: &mut EntityWorldMut) { | ||||||
(self)(entity); | ||||||
} | ||||||
} | ||||||
|
||||||
// SAFETY: | ||||||
// - `Bundle::component_ids` calls `ids` for C's component id (and nothing else) | ||||||
// - `Bundle::get_components` is called exactly once for C and passes the component's storage type based on its associated constant. | ||||||
|
@@ -2118,6 +2128,65 @@ fn sorted_remove<T: Eq + Ord + Copy>(source: &mut Vec<T>, remove: &[T]) { | |||||
}); | ||||||
} | ||||||
|
||||||
/// [`Patch`] wraps a [`BundleEffect`] into a [`Bundle`] allowing | ||||||
/// you to leverage the power of a [`BundleEffect`] without having | ||||||
/// to implement your own unsafe implementation of the Bundle trait. | ||||||
/// [`BundleEffect`]s get access to [`EntityWorldMut`] on insert allowing for | ||||||
/// powerful modifications to the world. [`BundleEffect`]s also power other Bevy | ||||||
/// helper bundles like [`crate::spawn::SpawnIter`], [`crate::spawn::SpawnWith`], and [`crate::observer::Observer::as_bundle`]. | ||||||
/// You can implement [`BundleEffect`] directly or use the implementation of [`BundleEffect`] | ||||||
/// for `FnOnce(&mut EntityWorldMut)`. | ||||||
/// | ||||||
/// Note: [`crate::component::Component`] Hooks are almost as powerful as | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||
/// [`BundleEffect`]s and should be used instead where possible. [`BundleEffect`]s | ||||||
/// should only be used where structural ECS changes are required. For more details | ||||||
/// see [`crate::world::DeferredWorld`] | ||||||
/// | ||||||
/// Example, using [`Patch`] to add a [`crate::resource::Resource`] to the world | ||||||
/// this is not possible with `[crate::component::Component]` Hooks because it is | ||||||
/// a structural modification | ||||||
/// | ||||||
/// ```rust | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doc test is going to need imports to pass CI :) |
||||||
/// commands.spawn(( | ||||||
/// Node::default(), | ||||||
/// Patch(|entity: &mut EntityWorldMut| { | ||||||
/// // SAFETY: We don't modify the entity's position so this is safe | ||||||
/// let world = unsafe { entity.world_mut() }; | ||||||
/// | ||||||
/// world.insert_resource(SomeResource); | ||||||
/// }), | ||||||
/// )) | ||||||
/// ``` | ||||||
pub struct Patch<E>(pub E) | ||||||
where | ||||||
E: BundleEffect; | ||||||
|
||||||
impl<E> DynamicBundle for Patch<E> | ||||||
where | ||||||
E: BundleEffect, | ||||||
{ | ||||||
type Effect = E; | ||||||
|
||||||
fn get_components(self, _func: &mut impl FnMut(StorageType, OwningPtr<'_>)) -> Self::Effect { | ||||||
self.0 | ||||||
} | ||||||
} | ||||||
|
||||||
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 commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
Note that |
||||||
{ | ||||||
fn component_ids(_components: &mut ComponentsRegistrator, _ids: &mut impl FnMut(ComponentId)) {} | ||||||
|
||||||
fn get_component_ids(_components: &Components, _ids: &mut impl FnMut(Option<ComponentId>)) {} | ||||||
|
||||||
fn register_required_components( | ||||||
_components: &mut ComponentsRegistrator, | ||||||
_required_components: &mut RequiredComponents, | ||||||
) { | ||||||
} | ||||||
} | ||||||
|
||||||
#[cfg(test)] | ||||||
mod tests { | ||||||
use crate::{ | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ use alloc::{boxed::Box, vec}; | |
use core::any::Any; | ||
|
||
use crate::{ | ||
bundle::Patch, | ||
component::{ComponentId, Mutable, StorageType}, | ||
error::{ErrorContext, ErrorHandler}, | ||
lifecycle::{ComponentHook, HookContext}, | ||
|
@@ -296,6 +297,33 @@ impl Observer { | |
pub fn descriptor(&self) -> &ObserverDescriptor { | ||
&self.descriptor | ||
} | ||
|
||
/// Creates a [`Bundle`] that will spawn a new [`Observer`] (its own Entity) | ||
/// observing this entity it's spawned on. | ||
/// | ||
/// Example | ||
/// ```rust | ||
/// commands.spawn( | ||
/// Node::default(), | ||
/// Text::from("Not clicked"), | ||
/// Observer::as_bundle( | ||
/// |trigger: Trigger<Pointer<Click>>, | ||
/// mut text: Query<&mut Text>| { | ||
/// **text.get_mut(trigger.target).unwrap() = "Clicked!".to_string(); | ||
/// }) | ||
/// ) | ||
/// ``` | ||
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 commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would this have any overlap with an There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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:
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.
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 commentThe 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. |
||
where | ||
E: Event, | ||
B: Bundle, | ||
M: Send + Sync + 'static, | ||
O: IntoObserverSystem<E, B, M> + Send + Sync + 'static, | ||
{ | ||
Patch(move |entity: &mut EntityWorldMut| { | ||
entity.observe(observer); | ||
}) | ||
} | ||
} | ||
|
||
impl Component for Observer { | ||
|
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 theBundleEffect
trait itself.