#Observers Overhaul
1324 messages Β· Page 2 of 2 (latest)
I admit it was a bit of a non-sequitur, but I do find signals useful because of the decoupling aspect.
The problem with static analysis is that it's strictly local
It's not a whole-program analysis, but only within a specific ui component or function
Also a bit funny:
In a way, signals are the inverse of type erasure: they erase everything but the type!
and then later:
In this case, a "signal" is just a type-erased wrapper
:p
Yep. In this case, the types that it is erasing are the types that make up the implementation, not the type of the output.
I could probably have chosen a better adjective
That sounds like a desirable property though, are there any practical downsides to this?
Well, we were discussing the usefulness of signals. Signals can be used both locally, within a UI component, or globally to wire components together. There are plenty of alternative ways to connect logic - the classic one is just bubbled events with a parent widget doing orchestration - but the number of choices available diminishes as you start to talk about widgets that are widely separated conceptually. That being said, I've seen other approaches too, like react-query or redux which, although advertised as a global data store and network layer respectively, can effectively function as a signaling mechanism.
Here's a practical use case: in my game editor, there's a type of entity called a waypoint which is used for actor goals. The waypoint asset can be edited via a reflective property editor - you can type in the coordinates. But while you are doing this, a gizmo shows up in the 3D view, showing you where the waypoint is in the world. These two display entities live in completely different parts of the UI hierarchy, but they are subscribed to the same underlying data.
(Actually, that's probably not the best example, as there are other ways to do gizmos).
What if in this case you'd use the ECS as the global store, and use regular observers to get notified when something changes?
Two UI components can depend on the same external ECS component
Yes, but you have to tell them which ECS entity to watch.
Is that a problem? Couldn't that be passed in as a prop?
A common code smell in React apps is called "prop drilling", which is the case where you have some changing property needed by a widget deep in the hierarchy, but the prop is supplied by an ancestor that's many levels higher; each time that property changes, you have to re-evaluate all the widgets in the ancestor chain, "drilling down" until you get to the widget that actually needs the value. With a signal approach, you can pass down the signal and the leaf node can update itself without the intermediate ancestors being involved.
Wouldn't that be the same here?
An entity is passed down to a child, but the entity value itself doesn't change
Only when the component of the entity (which is only accessed by the leaf) changes, will it get updated
Which doesn't trigger the ancestor chain
That's all that a signal really is - a pointer to some data, which can be dereferenced. The only difference is that doing the dereference also subscribes you to changes.
As the consumer of a signal, you don't know whether you are reading a plain variable or the computed output of some function.
Right, so this seems like something that can be done with a local/signal-less approach as well right? π€
Just that instead of a signal object you pass down the entity
Advantage of using an entity+component is that it works out of the box with everything else. You don't need a separate API
The waypoint gizmo is more general than that. It doesn't display just waypoints, but any 3d point in the world. It doesn't know whether the coordinates it is showing are a property of a struct, stored in a component, in a resource, or is some virtual value.
It just reads from the stream
So it's completely modular and reusable
yeah, any problem can be solved by introducing another level of abstraction :p
but is it worth it
Well, let me ask: are Futures worth it?
Everything you can do with futures could be done without them.
If your point is that an approach that uses a static/local analysis has worse UX or sth then I'm not sure if I agree π€
That's not my point
It's more that there are some cases that a static/local analysis doesn't cover
For the cases that they do cover, they work fine
Those cases being resources or virtual values?
Specifically, cases that require global interconnections
Cutting across the hierarchy
You can do that in the static approach as well:
parent_entity {
child_entity {
SomeComponent: {10, 20}
}
}
template CompA {
prop src = entity
const v = src[SomeComponent]
}
template CompB {
prop src = entity
const v = src[SomeComponent]
}
CompA(parent_entity.child_entity)
CompB(parent_entity.child_entity)
Think about unix/bash pipes: the reason that they are so versatile is because programs like more and cat are blind: they don't care where their input is coming from or where their output is going to.
If more required a specific filename as a parameter, it would be far less adaptable.
Right, I understand. But the price of that is that you now need to express every reactive value in this new abstraction that nobody yet uses
True
You can also flip it, and require that every reactive value must be expressed as a component
It's not like there are no rules at all for reactivity in JS and that everything just works
Yep, and in fact that that is how I've mutables and derived reactions in bevy_reactor, they are components. Signals are just an API, they aren't components by themselves.
The fact that it gives you a different API is an issue, especially in an established engine. It's easier in something like Solid which is signals all the way down.
You might also be interested in Redux, which is not signals, but a different form of reactivity: https://redux.js.org/tutorials/essentials/part-1-overview-concepts
Redux is based around the idea of an immutable data store: every change to the data conforms to the pattern old state + action => new state. Structural sharing is applied to make this efficient.
Individual UI components subscribe to a "slice" of the global data store, and dispatch "actions" which produce an updated global state. This gives you things like "time-travel debugging" in which you can rewind the most recent changes to get to an earlier state.
I'll take a look. I'm primarily interested in making reactivity work in a way that feels native to ECS though
So mostly curious about what issues I'm going to run into if I ignore signals
Your writeups are always very informative, definitely helps
Same goals here: very interested to see what you come up with
Yes, that would be great! I didn't really understand the counter-arguments well enough to feel like I could explain them, though. I'm hoping that was in part because I had failed to articulate what I was actually proposing, and that writing it down like this will make it easier for others to explain the problems with it. (And I feel a little guilty repeatedly pinging @feral tinsel to explain the same things over and over again. Although I'm about to do it again anyway...)
Huh, I meant to say the same thing here that I said in #ecs-dev message , and I interpreted your reaction emoji there as general agreement. What are some of the ways that multi-component observers are being used that don't fit this model?
I don't want to impact the discussion on the PR too much since I'm not actually a bevy user (and I've already made my points), so I'll let others comment
Perhaps one thing that's worth considering though is splitting up the query observer part from the lifecycle event refactor parts since (as I understand it) they're different things
@candid halo I can help if u want thread local stuff, I had to do it in order to do my async ui things
Wouldn't thread locals kinda fall apart if you capture signals (or signal-like handles)? There's no guarantee the closure will be executed on the same thread (unless you control for that of course, but that seems awkward).
I hadn't considered that problem, but I am not sure it is a problem. However, there's another problem: which World are we using? The reactive context contains a reference to a world, presumably the same world as was used to spawn the reaction in the first place, but if there are multiple worlds, then which one do we use? I guess that we need to initialize the thead-local variable to point to the world that is currently processing its reactions.
This is part of the issue. If we could accept passing around Commands, which happens frequently anyway, then we could probably extend its API to initialize signals with the correct world.
Assuming signals are just entities, basically.
In bevy_reactor, signals looked like this:
#[derive(Copy)]
pub enum Signal<T> {
/// A mutable variable that can be read and written to.
Mutable(Mutable<T>),
/// A readonly value that is computed from other signals.
Derived(Derived<T>),
/// A constant value, mainly useful for establishing defaults.
Constant(T),
}
Where Mutable looks like this:
/// Contains a reference to a reactive mutable variable.
#[derive(PartialEq, Debug)]
pub struct Mutable<T> {
/// The entity that holds the mutable value.
pub(crate) cell: Entity,
/// The component id for the mutable cell.
pub(crate) component: ComponentId,
/// Marker
pub(crate) marker: std::marker::PhantomData<T>,
}
So in the case of a signal that connects to a Mutable, it's just an entity id and a component id.
Oh, sorry, my thoughts here were only half-formed.
I guess that we need to initialize the thread-local variable to point to the world that is currently processing its reactions.
This seems like it could work just fine.
You could use this to implicitly subscribe to signals as well when they're accessed in a particular system.
I could maybe spend some time updating both bevy_reactor and thorium_ui to the latest Bevy.
I'd need to remove a bunch of stuff, like all the templating code
So to your point, signals aren't entities, they are handles to entities. Just ids, really. The main thing is that they are copy / clone so they can easily be captured.
I have a bunch of implicit conversions for signals, so that any function that accepts a signal, you can pass in a regular value and it turns into a Signal::Constant.
Unfortunately, because Signal in my implementation is an enum, you can't extend it to new kinds of reactions without changing the enum.
Making it truly extensible would require a dyn trait, which works against the copy / clone aspect of it.
I'm also thinking about what @feral tinsel said earlier; there is a case against signals. However, the question is what to replace them with. It's clear that a lot of the use cases for signals can be done in other ways, but this doesn't mean that all of the use cases for signals are so covered. Without actually writing a large complex app, it's hard to validate these ideas.
Solving the local connectivity problem has a lot more obvious solutions than solving the global connectivity problem.
I have a feeling most folks wouldn't be too excited about Clone signals (if the implicit cloning proposals are anything to go by).
Yeah I definitely havenβt built a complete UI/app yet so TBD if I can actually make it work without signals
Although Clone signals would solve my lifetime problems -- they could just be reference counted.
Frameworks like Leptos can largely sidestep this lifetime issue by relying on the structure of the application; that is, UIs in Leptos are necessarily built as a tree of components. Trees have clear ownership semantics, so you can associate signal lifetimes with component lifetimes. As Leptos builds the tree, it pushes and pops signal owners to the implicit stack.
I don't think this is a reasonable model for Bevy. While UIs in Bevy may usually form a tree, structures in the ECS are generally not so neat. Reactivity is useful for far more than just UI, so constraining Bevy's reactivity to a tree structure would be severely limiting.
All this to say that I'm wary of fully implicit, Copy, Leptos-like signals in Bevy.
It's important that we define the term "signal" here, because it has conflicting meanings. What Solid.js calls a "signal" (a reactive variable) is instead called a "mutable" in the futures-signals crate, and the term "signal" instead means an interface which references a reactive data source of any type.
So in bevy_reactor, mutables are entities; signals are just pointers to them.
Mutables use an OwnedBy relation to manage lifetimes - when the owner is despawned, so are any mutables it owns.
You can of course spawn a mutable with no owner, in which case it lives until manually despawned.
Hmm if you could refer to an Component on an Entity with an id would the Mutable need to be an Entity or does it need to be an Entity to add additional data/components.
/// Contains a mutable reactive value.
#[derive(Component)]
pub(crate) struct MutableCell<T>(pub(crate) T);
Please correct me if I'm off-base, but I get the sense that the majority of existing reactivity crates diverge from normal-looking Bevy code. Indeed, they often must diverge for the sake of their reactivity. This is primarily what I want to avoid. Rather than a Dioxus element or a trait implementation that takes some context, I just want a normal Bevy function. Something like:
fn counter(mut commands: Commands) -> impl Bundle {
let (count, set_count) = commands.signal(0);
let text = commands.derive(move || {
Text::new(format!("Count: {}", count.get()))
});
(
Button,
OnClick::new(move || set_count.update(|c| *c += 1)),
children![text]
)
}
Should this be feasible, which I think it is, my hope is that its applicability and generality are obvious. If we allow derived signals to be arbitrary systems, then the constraints begin to melt away (at the cost of potentially worse performance).
I know it's not always instructive to conjure examples that happen to be elegantly expressed by a particular paradigm, so take this with a grain of salt.
Really, my main point is that I'd love if we could find a solution that doesn't put reactivity in a little box, separate from the rest of your Bevy code. (And a solution that isn't coupled tightly to UI, since reactivity is broadly useful.)
@oak cipher @feral tinsel @jade hearth I just added a major addition to my post, which is an analysis of reactive solutions that don't use signals: https://github.com/bevyengine/bevy/discussions/20821#discussioncomment-14289962
I'll check it out after work
Okay, so just a wrapper, the magic is part of Signal then? π€
I think the reason that signals were invented was to solve a bigger problem, which is writing large, complex apps at scale.
This may be true, although I've actually found signals to be easiest to use and reason about for local state. The more complex an app gets, the less its data tends to "flow," and signals are not very nice when data doesn't flow.
I can't make much more of an argument for this though. It would take me a while to formalize my thoughts on it. My perception might also be too coupled to Leptos, which has some issues when it comes to complex state management.
Hmm now that I am looking at the code, couldn't this be merged into Query or a ReactQuery.
ReactQuery - Initialize (Entity, Tick) from Archetypes, then update via Query Events, adding or removing on Enter/Leave, and updating the tick OnMutate(via QueryParam's producing commands).
ReactQuery::should_react(t: Tick) -> bool returns true when there are entities in the buffer that are higher then the passed in system tick.
Then if not already, check should_react() either separately or as part of validation on Systems and Observers, and you have reactive Systems π€‘
throwing it out here, and i don't know maybe i'll have time to write my own addendum, but i think that signals as futures is actually really nice and ergonomic, you get a lot of useful things like selecting one of many, using futures select, etc
idk if still relevant, but that's basically the approach I took with bevy_rx. The signals are handled through a context, could just as easily be through Commands. The memos/derives were supposed to look vaguely like systems, though I never followed that all the way through https://github.com/aevyrie/bevy_rx/blob/17ab5d34b7c58e87c31f590bf72096afb705a001/examples/demo.rs#L32
all the state is held in a single World inside the context
@raven aurora lol
I had a feeling after I submitted that and saw you were active 3 minutes before.
@feral tinsel @jade hearth @mental meadow So this morning I had another thought about reactive queries: what if there was an .iter_changed() method, that would only returns rows that were different from the previous run? (Maybe flecs already does this). This would address the "over-reacting" issue - although it doesn't do anything for reactions with heterogenous inputs.
Furthermore, we could mark such queries as fallible if the number of changes was zero, so that the system execution would be skipped entirely.
I wrote a tiny Query wrapper with the intention of tracking gets, actually. I believe it's tenable, although it might be tricky to manage more than one component (and things like Entity which doesn't make sense to track).
This would be in the context of reactive systems -- that is, systems which we run explicitly in a "reactive context." So it wouldn't necessarily work in general.
Yeah I thought about that too. It might also make sense to have a performance optimization whereby if you call .iter() - basically getting all the rows - it skips the individual checks.
Sounds similiar to what I suggested here #1383928409784193024 message
, but I guess built-into existing Query with method access instead of a new-type.
BTW a question was asked earlier today in (#ui message) that is a great example of how our current approach falls short.
I'm going down the road now of "best-effort" incrementalization. With some systems, we can trivially determine whether their inputs have changed even now. For example, if it only takes resources or tracked queries. Others, like systems that take normal queries, have to be re-run every time. I've written a little trait to help distinguish the two, and it could be made even a bit better with deeper integration in bevy_ecs.
It could be a new type
One of the current pain points is that query filters don't handle component removals, making it hard to track changes for marker components.
I believe the backing state for both queries would be different, unless regular Queries were to be built around my suggestion, but I think that would leave some perf on the table for non-reactive uses.
I wouldn't even mind if there were separate query types for per-row and per-query change bits. Any system that wants to track changes is going to know up front what granularity they need, that doesn't change at runtime.
Yeah, with what I'm working on, I'd probably just write a reactive system that takes a Query<Has<T>> and run it every frame, memoizing the result. If we can accept some efficiency losses, it's a pretty easy workflow.
But I imagine there's a more clever solution.
Oh well I could just write some convenience signals that are only updated by lifecycle events
something like:
let pressed = commands.has::<Pressed>(entity);
let hovered = commands.signal_query::<Option<&Hovered>>(entity);
let variant = commands.signal_query::<&ButtonVariant>(entity);
let color = commands.derive(move || {
// bail if the variant doesn't exist
let variant = variant.get()?;
Some(calculate_color(hovered.get(), pressed.get(), variant))
});
Maybe that's too un-Bevy-like. But it would only re-run following changes from lifecycle events (or change detection in the signal_query case I suppose).
I could live with that
I assume that commands holds the hidden tracking context?
No, I've decided to try reference counting / garbage collection. In a sense it holds the context -- it spawns an entity that I check after every reactive pass, despawning it if no one holds it anymore.
It's potentially another efficiency hit, but I think it better matches the often non-trivial lifetime characteristics of entities. And it places no constraints on how signals are created, except that they need Commands.
@native mantle, would you like me to handle adding the deprecated aliases that @quaint dome recommends for https://github.com/bevyengine/bevy/pull/20731 ? If so, would you prefer a PR or a "push directly to your branch"?
Sure! You can push to my branch
Aye-aye π
Currently doing a detailed docs pass
I think there's a subtle difference between your proposal and mine, although I may be misunderstanding yours. We both like the idea of a query that reports changes, but there's a difference in what we mean by a "change". I think what you are proposing is that a change happens when an entity starts or stops matching the query constraint, what you describe as Enter/Leave. What I'm proposing is that a change happens when the data for a row (specifically, the destructured values returned by .get() or .iter()) are different than what was returned the previous time.
Although I don't mean different in the sense of having to do equality comparison with component values, just whether any of the returned components were added, removed, or mutated.
@native mantle, I've done a review pass on https://github.com/bevyengine/bevy/pull/20731, and I'm really happy with it. The terms are clear, the docs are good, and the core architecture is both less elaborate and more extensible. Before we merge:
- I'd like you to go over my suggested docs changes: nothing huge, but I think there's some helpful cleanup in there
- I'm going to improve the migration story by adding more deprecated methods and type, per @quaint dome's feedback
- I would like at least one more community approval
That all sounds good. I'm still trying to wrap my head around the safety doc thing
@quaint dome, the lifecycle events already have a deprected type alias π
Ah, that's great π
/// A deprecated way to retrieve the entity that this [`EntityEvent`] targeted at.
///
/// Access the event via [`On::event`], then read the entity that the event was targeting.
/// Prefer using the field name directly for clarity,
/// but if you are working in a generic context, you can use [`EntityEvent::event_target`].
#[deprecated(
since = "0.17.0",
note = "Call On::event() to access the event, then read the target entity from the event directly."
)]
pub fn target(&self) -> Entity {
self.event.event_target()
}
Just pushed this up
I have no comment about the implementation, but I can gladly give an approval for the user-facing API once the migration story is better π
My approval remains valid for the parts that I care about π
And another one:
/// A deprecated alias for [`trigger`](Self::trigger) to ease migration.
///
/// Instead of specifying the trigger target separately,
/// information about the target of the event is embedded in the data held by
/// the event type itself.
#[deprecated(since = "0.17.0", note = "Use `World::trigger` instead.")]
pub fn trigger_targets<'a>(&mut self, event: impl Event<Trigger<'a>: Default>) {
self.trigger(event)
}
Anything else you'd like to ease migration @quaint dome?
I've tackled all of the items listed in your comment
Except the macro
I don't have the macro-fu to improve that :p
Will think about it in an hour, SO just called me for a round of Mario Party π
The Monty Mole NPC won >:[
the macro-fu is already there
just need to improve the message
Maybe something like
Expected a struct in the form of
#[derive(EntityEvent)]
struct FooEvent {
entity: Entity,
// optional additional fields
}
Oh sick, I'll fix that up real quick then
@quaint dome
"EntityEvent derive does not work on unit structs. You must have a field to store the Entity target.",
How's that?
it must specifically be called entity
(or be a newtype for Entity)
hmm
I don't think someone new-ish to Rust and Bevy will have any idea what that means
Hence why my suggestion has a specific example
But the docs have detailed instructions?
Surely "oh, I should read the docs" is the natural response?
I would have guessed an error appears when the docs have already failed
But you probably have more experience with this than me π
So I'll trust your judgement
In this case, the error will appear when trying to migrate
I don't think new users will fail in this particular way very much
"EntityEvent derive does not work on unit structs. Your type must have a field to store the
Entitytarget, such asMessage(Entity)orMessage { entity: Entity }.",
Hold up, Messages are buffered events now, aren't they?
So this would be semantically wrong 
fair π
Right lol, bad name. Any alternative suggestions?
Kill is good but a bit morbid
Attack, InteractWith, TalkTo, AddScore
Attack?
Went with Attack and pushed changes
approving then 
Excellent. I think that just leaves the safety comment π
I was going to suggest Notify
Or Bulletin
Notify would be a good option, but I'm happy with Attack
The OnMutate part covers Mutable change detection in my way, and query events cover added/removed/replaced(Immutable change detection)
Both of these would update the Query with new data, and then the should_react will detect them if their change tick, which is also recorded, is above the Systems change tick.
@native mantle, https://github.com/bevyengine/bevy/pull/20731 has my approval now. Merge at your leisure, or wait until @deft thorn has reviewed the safety work π
Cool. I'll hold off for a bit while I work on the Message rename
I'm reasonably convinced the safety work is fine / im leaning to merge if we don't hear back soon
Yep, that's my feeling too
left a comment
Left a comment in the github thread, unfortunately I don't think the changes really improves the safety of this situation
Thanks π
Okay, I'm largely in over my head here, but I've done my best to understand, summarize and do some of the more mechanical changes involved with the safety requirements for #20731.
https://github.com/cart/bevy/pull/44
This is still draft: I'm not confident that I understand the problem well enough to actually write up the safety comments, and there's another more complex change that still needs to be made
@native mantle / @deft thorn / @ruby roost, feel free to push to this branch as needed (ask me for permissions if you'd like), or use the code suggestion tool to improve / correct me
The problem and machinery is too complex for me to get a full fix up on my own, but hopefully this helps save y'all some time and provides a nice thread for discussion. I also really wanted to include some clear meta-commentary on "why is this even needed" with backlinks, because the problem is extremely subtle and likely to be lost to the sands of time
Just pushed my safety comments / changes to https://github.com/bevyengine/bevy/pull/20731
(and @oak cipher's)
It looks like EntityCommands has lost its trigger method in 0.17. You can't do commands.entity(entity).trigger(...? I can't see that mentioned in the migration guides? They discuss the switch from commands.trigger_targets to just commands.trigger, but would be good to also mention EntityCommands.trigger also being sunset. Deprecation message would be nice too, they're helpful elsewhere in the Observers overhaul.
I've never seen commands.entity(entity).trigger()
Hmm solid point. It introduces weirdness / bugs in that it will not trigger for the current entity anymore if we wire it up to world.trigger. We'd probably need to make it warn or something
But I think thats worth it
I'll throw that in
is there an "intended" approach for implementing Events with occupied type parameters? I'm trying to faff about with bevy_http_client and now the dreaded trio of 'static + Sync + Send compiler suggestions has bubbled up when they weren't previously there.
i.e. MyObserverEvent<T> {data: T}
You can do:
#[derive(Event)]
struct MyObserverEvent<T: Send + Sync + 'static> {
data: T
}
Although this feels like a UX regression worth solving. We can specify those inside the derive / only impl Event for cases where Self: Send + Sync + 'static
Wonderful :)
Love the observers overhaul. Really nice to migrate to, and a much clearer API.
is there a generic struct Observed<T> for some Event or Message T which just tacks on the entity: Entity field to make it an EntityEvent?
Since we've moved to EntityEvent having target Entity as it's a value somewhere in event's struct, would it be useful for #[derive(EntityEvent)] to also resolve target to the only named field of type Entity?
Presently it resolves entity: Entity to target automatically. But this doesn't seem to lead as far from the previous event.target() that's only present on EntityEvent, just adds a bit of boiler-plate (need to type that entity: Entity on all entity events.
If you have semantic_name: Entity, that gives some semantic gain. But it won't be resolved as target automatically when the struct has only one Entity field, requiring it to be marked with #[event_entity] explicitly, which feels boiler-platy.
I found myself leaning to use entity: Event because of that. I know, just need to type that #[event_entity] but it feels egghhh
I suspect I hit a small regression after upgrading to 0.17 - On::target is only implemented for EntityEvents with propagation: https://github.com/bevyengine/bevy/pull/21049
I'm migrating to 0.17 and the overhaul seems great, thank you for your work.
A small nit is that since we don't use the On* prefix anymore and neither use the past tense we end up with very generic names for events in games:
- Before:
struct OnDamage/OnDeath/OnArrival/OnReward - After:
struct Damage/Death/Arrival/Reward
This makes it difficult to do things like OnReward(Reward)/Rewarded(Reward).
What if the events were a verb. Optionally you could have some indication of tense (depending on whether the event is triggered before or after the thing happening) On<Arrive> or On<WillArrive> or On<DidArrive>. Then the wrapped data object could be the noun Arrive(Arrival). Admittedly trickier for words where the verb and noun are the same like reward, and damage.
Will / Did is useful sometimes to indicate whether the event is the actual trigger for the thing happening, or a reaction after the thing has happened
Native events seem to use verbs more than I do (e.g. remove vs removal). I'll give it a try, thanks.
But I think I maintain my feedback that a unique name pattern for events had some advantages that I'll miss.
After working with the event rearchitecture for a while now, I must say: this is really really really good
great work Cart π
I must admit I was a skeptic of the entity field in entity events having to be defined as part of the struct, but I've come around to it after trying it out in practice
It's quite elegant, actually
Made me think if there's places in my own crates where I could redesign an API that way
Glad you like it!
it brings a bit of needed clarity vs the .target vs .target() vs etc. on the non-global observer side and makes everything else pretty clear.
I think I might have hit a bit of a limitation in the new system. In my project in 0.16 I used to have a set of events that I would add to animation clips, that were set to auto-propagate. I would then observe these events from the SceneRoot. The AnimationPlayer is not at the SceneRoot, it's a couple of nodes deep. I'm not sure how to port this to 0.17. My event has to conform to AnimationEvent to be added to a clip. The event struct can't contain an Entity, because the clip is a resource that will be applied to many entities. AnimationEvent gives you target().animation_player, but that's not at the root, and I don't think the event can be made to propagate further. Not sure how to proceed. I could:
- Try to spawn the scene at the deeper point in the hierarchy where the rig is (and hence the AnimationPlayer gets added). Even then though, I think the
AnimationPlayerwould still be one node deeper than theSceneRoot(asSceneRootspawns the scene as a child node). - Or, split each of these events into two, the
AnimationEventon the clip and then a secondaryEntityEvent, triggered by theAnimationEvent, that targets the root, or propagates up to the root
(2) feels like the cleanest workaround for now. But I also see no reason why we can't add propagation support to AnimationEvent. Not something we can land for 0.17, but that should be a reasonably straightforward addition
Can you open an issue?
To avoid repeating the event a bunch of times, you could make a generic event like struct Propagate<E: AnimationEvent> { entity: Entity, event: E }
Although if the goal is to build the ideal consumable interface, its probably better to duplicate the event (and give the "better" name to whatever is intended to be public)
There is something about the way propagation works now which really confused me and I don't think is explicitly documented. So now with EntityEvent we have to supply the event target ourselves, and we are encouraged to be specific about the naming of the entity. So for instance, with my Propagate<E: AnimationEvent> I originally labelled the root entity animation_player, because the player is what the event gets triggered on:
#[derive(EntityEvent)]
#[entity_event(propagate, auto_propagate)]
pub struct Propagate<E: AnimationEvent> {
#[event_target]
pub animation_player: Entity,
event: PhantomData<E>
}
impl<E: AnimationEvent> Propagate<E> {
pub fn new(animation_player: Entity) -> Self {
Self { animation_player, event: PhantomData }
}
}
pub fn on_propagatable<E: AnimationEvent>(
propagatable: On<E>,
mut commands: Commands,
) {
commands.trigger(Propagate::<E>::new(propagatable.trigger().animation_player));
}
My assumption was that when it propagates, this entity would remain unchanged. This is not in fact the case. As it propagates, this entity gets updated. In other words, it is no longer equal to the original_event_target, it becomes the current event_target at each layer of the propagation. This means that the specific naming, in my case animation_player is incorrect:
assert_eq!(trigger.event_target(), trigger.animation_player);
assert_ne!(trigger.original_event_target(), trigger.animation_player);
So if we're creating a propagating event, we shouldn't give the event_target a specific name. It probably should just be entity, because it will be referring to different entities at the callsite and observation site. I think this needs to be made clearer in the docs.
I agree that the name of the propagated entity should match the context of the event in every step of the propagation.
I wrote up my thoughts after banging my head against the observers-as-relations work yesterday: https://github.com/bevyengine/bevy/issues/17607#issuecomment-3373688702
I would really appreciate ideas and opinions on the best route here
left some thoughts
https://github.com/bevyengine/bevy/pull/21408 could use some feedback too!
Also left some thoughts
Thank you all; really appreciate it!
@candid halo
I also think that we (the Bevy community) often roadblock ourselves by requiring the perfect infrastructure to be in place
As a counterpoint & as someone who's been around since pretty much the start, the community has IME also had a tendency to delay complex/controversial work. You have to bite the bullet at some point, and it's better to do that when you have a good use case for it
I agree, but I will offer the counterpoint that we often have too many balls in the air at once in a way that slows down and worsens work π
I am generally a proponent of incrementalism and "imperfect-now over perfect-later". in this specific case, i think it's probably good to focus effort onto a workable version of many-to-many, since we already have somewhat imperfect implementations of both observers and relations.
the remaining work is refactoring, polish and design, and probably isn't blocking anything.
I am generally a proponent of incrementalism and "imperfect-now over perfect-later".
That's not at odds with doing complex/controversial work. You can incrementally implement something that's complex
My point is more about delaying a complex solution and go with* an entirely different simpler solution to fix a short term need
Which is totatlly fine, but you need to balance that
agreed, it's usually in alignment.
(heck sorry wrong thread)
A little summary on porting an observer based control flow library to bevy 0.17.
TLDR probs to early to say but the implementation got me wondering if there could be room for a more flexible superset of EntityEvent in some places, something like:
trait IntoEntityTargetEvent<M>{
type Event: Event;
type Trigger: Trigger;
fn into_entity_target_event(self, entity: Entity) -> (Self::Event, Self::Trigger);
}
Is there a way to order two observers on an object? As in, I have two observers on an object that both watch the click action, and I want one of them to resolve before the other
I have a hacky workaround available (using bevy_defer to make the other observer wait a couple frames before taking effect), but I was wondering if there's a way to do that without an arbitrary time delay
Part of the reason why propagate(false) does not affect observers on the same entity is precisely because the order of observers is not guaranteed.
The only thing you can know for sure is that all observers on an entity will complete before propagating to the next entity (if you are using propagation)
Makes sense
I may just want to find a different way to go about this; I gave my in-world menu buttons the ability to act as both a radio button of sorts and a regular button, so when I click one of them, the radio group observer will fire as well as the regular button click observer
What I intended to do with that is have the radio group fire first and set the selected option, then let the button observer handle responding to that changed selection
For the headless buttons, what I do is have the click observer fire a different event, Activate. This gives me two things:
- I can prevent the event from firing if the button is disabled
- I can activate the button either by click or by keyboard shortcut
Alright, that would work
I think
I'll note that down in case I end up rearchitecting my system; currently I just have a number of other things that need to happen first
In general, rather than assign multiple meanings to a single type of event, I prefer to cancel the event and trigger a new event that has a more specific meaning.
https://github.com/bevyengine/bevy/pull/21408 is ready to review now π
@oak cipher @native mantle I know this is going to be controversial, but the more I think about it, the more I would like to have a require_observe or require_on feature:
#[derive(Component, Clone, Debug, Default)]
#[require(AccessibilityNode(accesskit::Node::new(Role::Button)))]
#[require_observers(
button_on_key_event,
button_on_pointer_down,
button_on_pointer_up,
button_on_pointer_click,
button_on_pointer_drag_end,
button_on_pointer_cancel,
)]
pub struct Button;
This would call entity.observe() with the given observer functions when the component was inserted. This would have a number of advantages:
- I could get rid of about 10 separate plugins from ui_widgets and feathers.
- The user would no longer have to install those plugin groups, simplifying their code and removing a potential footgun.
- Users only pay the cost for the widgets they use.
- The individual observer functions could be simplified, since I no longer have to filter out global events that aren't meant for my entity.
- Performance wins: we wouldn't have to be calling 8 callbacks each time a picking event bubbles.
- It means that observer registration has feature parity and is more consistent with component hooks.
The downside is a small increase in memory consumption, because we're registering the observers individually on the entity instead of using universal observers. But I've been increasingly uncomfortable with the way universal observers are used for the widgets anyway, and my discomfort grows as the number of widgets increases.
In effect, what this does is to make components more like self-contained modules of functionality: the act of inserting a component automatically installs the logic that goes along with that component.
Something similar can be achieved by using a global observer for On<Add, Button> and whatever observer is needed, right? The user still would have to add the plugins, but the cost of having one global observer per component isn't high.
The current implementation of Button, in the bevy_ui_widgets uses global observers, but I don't see why this wouldn't work with entity observers also.
I like the idea of require_observers, because there is one additional benefit: It's easier to know what observers affect that Component, just look at the definition of the component.
I do something similar already for when i was doing my nodegraphs, i think this would be a good feature to have
That api is so clean, the locality of behaviour (?) is a huge win in this proposal imo
Ive been a proponent of global observers, but really only as a stop-gap until patterns emerge. I really like this, and think it would be a pretty clear improvement.
I wonder if we could de-duplicate observers created in this way, so that each handler function is only spawned as an observer entity once.
I can see the appeal here, but I want to push back on the exact mechanism
I think that spawning extra entities with a single component is too much implicitness. Why can't we return a template/fragment instead, and have users spawn those?
(other than BSN isn't merged yet)
I'm concerned about the reduced flexibility imposed by this model; I think it should be the default but not only approach
The downside is a small increase in memory consumption, because we're registering the observers individually on the entity instead of using universal observers. But I've been increasingly uncomfortable with the way universal observers are used for the widgets anyway, and my discomfort grows as the number of widgets increases.
Is this still true if we had proper query filtering in observers?
Isn't using universal observers much more efficient? π€ Trying to understand the concern here. If I have a list with a few hundred elements and an observer per element, I'd be much happier with a universal observer than each widget having its own
Also, how do you replace/provide these observers, so that the require doesn't get triggered, the signature would need to exactly match and even then idk
A better pattern might be a universal observer attempting to be added, whenever a component of this type is added
What are the tradeoffs then?
It's still implicit and pretty spaghetti
I really don't see what's so bad about a single app.add_plugins(FeathersPlugins) call once per app
Which is how every other bit of logic in Bevy works
This is simple with Components as Entities, just create them in the Component Entity setup callback that should exist on the Component trait.
Hmmm, in some ways though you shouldn't have access to those components unless the 'plugin', the behavior, is added
At a higher conceptual level
π€
That's where the disconnect comes from
I think one of the points is, what if you only wanna one component? You'll add a bunch of observers, even if you only use, say, Checkbox.
This too ig, there are other implications, but at a higher level there are components, where using them without the appropriate systems being added is just wrong and we wanna eliminate incorrect usages when we can
In general you want components to be avaliable without behaviors (systems or observers), but not always, and in this specific case we wanna tie them together
Yeah, it's bit weird, cause you dont add systems this way, you add them regardless of the components being used, although observers are slightly different since they can be targeted.
Not saying you have to do it this way, but I think there is something important here to consider
Too be fair, users may want to provide their own Systems/Observers so to say it is wrong without the provided systems/observers is itself kinda wrong π
Yeah, and plugins allow you to switch out behavior without switching out data
Which is very nice
In a way, observers are like components, but ones which impl IntoSystem with fixed first param.
If we think that way, it is almost like:
#[derive(Component, Clone, Debug, Default)]
#[require(AccessibilityNode(accesskit::Node::new(Role::Button)))]
#[require(
ButtonOnKeyEvent,
ButtonOnPointerDown,
ButtonOnPointerUp,
ButtonOnPointerClick,
ButtonOnPointerDragEnd,
ButtonOnPointerCancel,
)]
pub struct Button;
but at this point, observers are components which carries behavior, not data. π€
Couldnβt the solution here be global observers with query filters? ex: On<PointerUp, Button>
The user would no longer have to install those plugin groups, simplifying their code and removing a potential footgun.
I think "no plugins needed" is the biggest appeal here. But imo that isn't a problem specific to observers, and this approach operates under the assumption that we will forever limit Button (and widget functionality in general) to required components and observers (which may or may not be true). And the local vs universal observer question ideally should be made based on how it behaves and performs, not which one is more convenient (ideally both are equally convenient).
I think we might want to consider a more generalized solution to the "components are standalone / drive their own behavior" problem (ex: they register whatever functionality they need when they are inserted). @cerulean turtle might be interested in this, as theres a chance a solution would cut down on binary sizes / improve our granularity there (although Reflect might get in the way of that, as it would put the init code in the registry).
I also think this isn't currently the biggest problem that needs solving atm and I'd rather not get caught in the design weeds here prior to getting bsn out.
Wow, that's a lot of responses! π
With respect to universal observers, the concern I have (performance wise) is that they trigger for every click/up/down/drag/keypress event, even for entities which aren't widgets. Consider what happens if you click on an entity that is not a widget:
- It runs the on_click event for each registered widget: button, slider, checkbox, and so on. Each one says, "nope, this is not for me" and lets the event propagate.
- The event propagates upward, and we again run every registered observer for button, slider, checkbox, and so on.
- This continues until we get all the way to the UI root.
So the number of handlers fired is (number of widget types * depth of hierarchy). This means that the cost increases as we add more widget types. If the hierarchy depth is only like 1 or 2 levels this isn't so bad, but what if it's like 10? That's 50 or 60 observers being called for a click event that isn't even doing anything.
A different solution would be to use bsn on mechanism for standard widgets, that is, to make all the standard widgets bsn scenes. However, part of my design goal for the standard widgets is for them to be template-agnostic. The fact that bevy_immediate is able to work with these widgets is IMHO a strong validation of this decision.
With respect to universal observers, the concern I have (performance wise) is that they trigger for every click/up/down/drag/keypress event, even for entities which aren't widgets
With a first-class observer filtering mechanisms we could bake in caching for what observers are relevant for each entity
Which I believe is something that flecs does
With respect to the issue of plugins, part of the complexity for users is that feathers widgets have a web of dependencies:
- The plugins for the feathers widgets themselves (button, slider, checkbox, etc.)
- The underlying plugins for the headless widgets (also button, slider, checkbox, etc.)
- The feathers infrastructure plugin, which installs:
- cursor shape plugin
- text propagation
- embedded assets (fonts, shaders, icons)
- theme stuff
- the input focus / tab navigation plugin
- (soon to be added) the plugin for popover positioning
...all of which is relatively easy if you want to just install everything; but if you decide that you want just buttons and sliders and don't want to pay the costs for checkboxes and menus it starts to get complicated.
Can we just have a single high-level plugin with fields for config?
struct FeathersPlugin {
buttons: bool,
sliders: bool,
}
For my downstream work, I also use patterns like this. I sometimes need to encapsulate big observers in their own little subplugins (internal pub(super) fn plugin(...), not externally visible ones) in own files, but adding those observers across files by exporting the functions is sometimes a bit spaghetty-i, so I like to have a component (in this case Button) that has an On<Add> observer that then wires up the entity observers. In that case, that component kinda is the observer, API wise.
So can confirm that this would be something useful for me downstream π
My behavior library is built on this pattern, I'm currently getting by with a proc macro that hijacks the on_add hook but a derive-only solution would be nice.
#[action(do_something)]
#[derive(Component)]
struct DoSomething;
fn do_something(run: On<Run>) {
...
}
I call this state + behavior type an Action, it could be worth formalizing something like that if its gonna be super common
Before opening up an issue/pr can i get some initial feedback on upstreaming IntoEntityTargetEvent ?
The goal is to improve ergonomics for third party entity event types.
As discussed bevy should only officially support a single flavor of entity events with the target stored on the Event not the Trigger, however the event_fn: impl FnOnce(Entity) -> E pattern only allows for events with a default Trigger. This results in third parties introducing awkward extension methods for EntityCommands, EntityMut etc.
I'd like to open a PR to upstream IntoEntityTargetEvent which is a superset of FnOnce(Entity) -> E that allows for a non-default Trigger:
pub trait IntoEntityTargetEvent<M> {
type Event: for<'a> Event<Trigger<'a> = Self::Trigger>;
type Trigger: Trigger<Self::Event>;
fn into_entity_target_event(
self,
entity: Entity,
) -> (Self::Event, Self::Trigger);
}
This is a non-breaking change 100% compatible with the current api and allows third party implementers to use the entity.trigger() method, as well as opening up more possibilities for official patterns.
IIRC @native mantle chimed in on some very similar work around "kinded entity targets", perhaps by @steel zephyr? He was generally fairly skeptical: it increased the perceived user-facing complexity and risked compile time regressions due to the use of generics
I'm sympathetic to those concerns too, although I would really like the broad adoption of kinded entities, both in Bevy and in the ecosystem more broadly
Aha, there we go
I like that implementation better than your proposal at first blush at least π
haha we've been here before, these issues look similar on the surface but I think they are different.
This proposal does not touch EntityEvent, it replaces the FnOnce(Entity) -> E where E::Trigger: Default with a trait to allow for non-default triggers.
To be clear I'm not proposing the EntityTargetEvent struct in the link I provided, just the trait.
@steel zephyr are you able to confirm this?
Reading though #21384 I gather your issue is about EntityEvent support for the kinded entities, and is not related to custom Trigger types.
That said, you did mention the current limitations of EntityCommand::trigger.
My proposal is to replace the FnOnce with a trait similar to IntoScheduleConfigs<M> which would allow you to implement your own patterns in moonshine_kind, would that help?
I'm not 100% sure. It's been a while and I've been distracted from Bevy dev.
But my main issues with the Observer/Relationship API is that they're now too intrusive and limiting in usage because they require mutable access to the underlying entity. As far as I understand, for events this is required because of propagation (which imo is an edge case feature and not worth this limitation), and for relations it is to support spawning entities with pre-existing relations (which again, to me is not common usage).
Both of these issues imply that library authors cannot guarantee any invariants on the target of event/relationship types, and it's kinda killed my motivation to continue my main project. I still haven't migrated my main project to Bevy 0.17 because of all this... π
My "instance" events now look like this. but this is just flat out wrong
we'll get there, the apis only been out for a few weeks and its a major change. i think its just a bit of a process understanding all the colorful ways people are using bevy ecs and then finding the win-win path for the api
Yah, and that's totally fair. π I figured I can wait it out a bit.
But imo, an event, on the base level, should be immutable after dispatch. I understand mutating the event opens room for optimization because you can use the same event to invoke multiple handlers.
But to me, that should be an opt-in feature, not something that's required
If we can prevent mutation after dispatch, and if we have the API to create events with entity-like targets (which is what I believe you're proposing), then I'd be happy
hmm thats interesting i did find myself copy-pasting the propagation logic when writing my own triggers, maybe thats another sign theres further work to do in propagation
have you looked into writing custom triggers or doesn't that work in your case?
heres the one i wrote so i could store the target on the Trigger instead of the Event
it allows you to control propagation and you dont have to impl EntityEvent
Maybe we could have an ImmutableTrigger π€
Could work, but it feels like a workaround. Now all my events would have to use this special trigger
Especially since I'm trying to delete a feature Bevy really wants to give me
I agree that custom impls currently feel like 'special triggers' requiring their own extension methods, macros, propagation etc
I also feel like we're getting a better understanding of whats required to make them feel like first class citizens, just like bevy's other beautifully modular patterns.
pr is up, have at it π
https://github.com/bevyengine/bevy/pull/21799
I looked at the PR and the other PRs linked. I haven't been involved in the discussion, so take this with a grain of salt. I'm not sure I understand the motivation for this change: what exactly are you trying to do that the current design prevents you from doing? I've been coding a lot of event types as part of both the feathers and bevy_reactor work, and so far I haven't felt the need to go outside the bounds of the current design.
thanks for the feedback yes it seems an example is needed, here's the explanation for a pr i created in moonshine_kind to demonstrate the proposal, let me know if I can clarify further
Is this in 0.17.3? how do you emit this event after a On<Pointer<Click>> event? I haven't been able to find any examples