-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Event Rearchitecture #20731
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
Event Rearchitecture #20731
Conversation
|
Gotta resolve some conflicts with |
Fully agree here :) Changes to internals can wait, but I would really like to nail down the fundamental user facing API and terminology. Really glad you're taking a look here. I'm aligned with you on the core principles: less duplication, more staticness, more efficiency! I'll leave more detailed thoughts in review comments to allow them to be collapsed and threaded. |
alice-i-cecile
left a comment
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.
Very helpful: thank you for filling in the safety comments. I'm happy with the clarity of the explanation there, and feel like I can (carefully) make and review changes here in the future.
I think that there's likely things that we could do to clean up the gnarliness of the unsafe code here, but I am fine to leave it for now.
Nit: there are 5 instances of "contrained" in the new safety docs. They're all collapsed though, so it will be easier for you to just grep locally.
Hehe I fixed it before you comment was posted 😈 |
|
I'm going forward with merging this, in the interest of keeping the 0.17 ball rolling. |
This renames the concept of `BufferedEvent` to `Message`, and updates our APIs, comments, and documentation to refer to these types as "messages" instead of "events". It also removes/updates anything that considers messages to be "observable", "listenable", or "triggerable". This is a followup to #20731, which omitted the `BufferedEvent -> Message` rename for brevity. See that post for rationale.
… derives (#20996) # Objective The Event derive used to add these bounds on behalf of developers, but the new macros in #20731 don't do that. This removed the need for developers to specify the bounds themselves for Events that use generics. ## Solution Automatically add `Self: Send + Sync + 'static` bounds to the `Event` and `EntityEvent` derives
…ropagation (#21049) Before #20731, `On::target` was implemented for any `EntityEvent`. After that PR, it's only implemented for events with `#[entity_event(propagate)]`. ```rust entity.observe(|scene_ready: On<SceneInstanceReady>| { // Error: "the method `target` exists but its trait bounds were not satisfied". info!("Scene entity {} is ready", scene_ready.target()); }) ``` I suspect the change wasn't intentional. This PR restores the old behaviour and adds a regression test. Note that `On::target` is deprecated, but without the fix users will get an error instead of a deprecation warning. <details> <summary>Full error message:</summary> ``` error[E0599]: the method `target` exists for struct `observer::system_param::On<'_, '_, EntityEventA>`, but its trait bounds were not satisfied --> crates\bevy_ecs\src\observer\mod.rs:1129:34 | 285 | struct EntityEventA(Entity); | ------------------- doesn't satisfy `<_ as Event>::Trigger<'a> = PropagateEntityTrigger<_, EntityEventA, _>` ... 1129 | assert_eq!(event.target(), event.event_target()); | ^^^^^^ | ::: crates\bevy_ecs\src\observer\system_param.rs:38:1 | 38 | pub struct On<'w, 't, E: Event, B: Bundle = ()> { | ----------------------------------------------- method `target` not found for this struct | note: trait bound `<EntityEventA as event::Event>::Trigger<'a> = trigger::PropagateEntityTrigger<_, EntityEventA, _>` was not satisfied --> crates\bevy_ecs\src\observer\system_param.rs:153:40 | 153 | E: EntityEvent + for<'a> Event<Trigger<'a> = PropagateEntityTrigger<AUTO_PROPAGATE, E, T>>, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound introduced here ... 156 | > On<'w, 't, E, B> | ---------------- ``` </code> </details> ## Testing Hacked `animated_mesh` example to use `On::target`, confirming it worked and reported the deprecation warning.
… derives (#20996) # Objective The Event derive used to add these bounds on behalf of developers, but the new macros in #20731 don't do that. This removed the need for developers to specify the bounds themselves for Events that use generics. ## Solution Automatically add `Self: Send + Sync + 'static` bounds to the `Event` and `EntityEvent` derives
…ropagation (#21049) Before #20731, `On::target` was implemented for any `EntityEvent`. After that PR, it's only implemented for events with `#[entity_event(propagate)]`. ```rust entity.observe(|scene_ready: On<SceneInstanceReady>| { // Error: "the method `target` exists but its trait bounds were not satisfied". info!("Scene entity {} is ready", scene_ready.target()); }) ``` I suspect the change wasn't intentional. This PR restores the old behaviour and adds a regression test. Note that `On::target` is deprecated, but without the fix users will get an error instead of a deprecation warning. <details> <summary>Full error message:</summary> ``` error[E0599]: the method `target` exists for struct `observer::system_param::On<'_, '_, EntityEventA>`, but its trait bounds were not satisfied --> crates\bevy_ecs\src\observer\mod.rs:1129:34 | 285 | struct EntityEventA(Entity); | ------------------- doesn't satisfy `<_ as Event>::Trigger<'a> = PropagateEntityTrigger<_, EntityEventA, _>` ... 1129 | assert_eq!(event.target(), event.event_target()); | ^^^^^^ | ::: crates\bevy_ecs\src\observer\system_param.rs:38:1 | 38 | pub struct On<'w, 't, E: Event, B: Bundle = ()> { | ----------------------------------------------- method `target` not found for this struct | note: trait bound `<EntityEventA as event::Event>::Trigger<'a> = trigger::PropagateEntityTrigger<_, EntityEventA, _>` was not satisfied --> crates\bevy_ecs\src\observer\system_param.rs:153:40 | 153 | E: EntityEvent + for<'a> Event<Trigger<'a> = PropagateEntityTrigger<AUTO_PROPAGATE, E, T>>, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound introduced here ... 156 | > On<'w, 't, E, B> | ---------------- ``` </code> </details> ## Testing Hacked `animated_mesh` example to use `On::target`, confirming it worked and reported the deprecation warning.
|
I just read this a couple of weeks agoo then got into the throes of getting our app's Our history of prior apps done in C++ and java and a hybrid of both were highly event/message based between the "nodes" (ie: entities). I had there, made a pretty sharp divide between the "routing header" and the "payload". The "routing" being things like "initial_sender", and even a "hop history" of the thread through a graph traversal, max hops, timeout "reply_to" etc. You have "on.original_entity() and on.target". This was handed by a "router" which your "world" (commands being access to it) seems to take the place of. The main item for us to get up now is we use a different "bubbling" algorithm for hierarchy trees and interaction. We use a two way "bubble" system where one starts at a node and then go down to the leaf that was "hit" or had the initial event posted (triggered) to it then if "propagated" back up. On thew way down, any ancestor can eat, or forward to the child below or reverse the direction of the traversal, then on the way back up a node can eat, forward (propagate) or abort. For UI inputs only for low volume "hailing" events like enter/exit and press, and "wheel", and a node then can attach "listeners" to a pointer and receive moves etc directly from the source and request exclusive "capture" or focus. I saw some reference to "custom triggers" in discussions. In 0.16 this spring, | looked at the code and found a loop where the "propagate" flag was checked to see if the current entity has a component of the type specified in the traversal and then continue, or abort. It looks like much of the change is internal how to optimise and maintain caches for ECS triggered system functions ( those that can access the world via gueries ) And I really like using the ECS memory management plus thread managed triggered queries. Anyway thoughts on this, how best to get what I want ( at least simulated but effective ), and what are you envisioning for the architecture ? Initially for the bubbling difference. For the bigger aps ( muti-player, multi-core, server mirrored peer to peer etc ) events get dispatched locally in local context with entities as addressed nodes, and rust types and signatures as message types, for remotely we did it "over the wire" with a serialized version of the same structure through a local to remote node ID map. |
|
BTW congrats on getting 0.17 out by the end of the quarter. Much nicer in many ways :) |
|
Ok - I made some very small mods to my bevy 0.17.1 to add bits to "propagate" - I made the propagate bit the high bit ( so no shifting needed to access the low 31 bits as an integer) . My tests here work fine and do what I need for added flexibility with minimal mods to bevy_ecs/event/trigger.rs and bevy_ecs/observer/system_param.rs. Not an ideal where an entity could be returned from a triggered observer to propagate to and avoid the whole call chain and setup to get to PropagateEntityTrigger::trigger(), but the minimal change enables and if I trigger to the "next entity" inside the observer that wished to propagate it will lose it's "original_event_target". But I likely want to have an expanded "hop history" anyway. But I do have to invoke the whole call chain from "commands" again for the next trigger if done that way. Anyway thoughts ? Should I make a PR for this ? |
There is general consensus that our terminology for Events, "entity events", Observers, and BufferedEvents needs clarity. Additionally, many of us also agree that the current Observer system would benefit from additional static-ness: currently it is assumed that you can use events in pretty much any context, and they all go through the exact same code path.
Alice put forth a proposal to Overhaul Observers, and we have already partially implemented it for 0.17. I think it does a great job of outlining many of the issues at play, and it solves them reasonably well. But I also think the proposed solution isn't yet ideal. Given that it is already partially implemented for 0.17, it is a breaking change, and given that we have already broken the Observer API a number of times, I think we need to sort this out before the next release.
This is a big changeset, but it is largely just a reframing of what is already there. I haven't fundamentally changed the behaviors. I've just refined and constrained in a way that allows us to do what we are currently doing in a clearer, simpler, and more performant way.
First, I'll give some quick notes on Alice's proposal (which you all should read if you haven't yet!):
Notes on Alice's Proposal
app.add_broadcast_observers(),app.add_universal_observers(),Observer::entity_observer,Observer::broadcast, etc. TheOnevent should statically determine whether an observer is an "entity observer" or a "broadcast" Observer. This would already be encoded in the type system and is therefore something we can do on the developer's behalf. Likewise, any observer being registered at a top level is inherently not a specific entity observer. All of these variants serve to make users guess and poke around in a way that is unnecessary. I want simple one word concept names, single constructors, etc.Proposed Principals
Concepts
world.trigger_ref_withmakes it possible to pass in mutable reference to your own Trigger data, making it possible to customize the input trigger data and read out the final trigger data.Triggerdetermines which observers will run.Triggerthat expects a specific kind of event (ex:E: EntityEvent).EntityEventtrait, which defines anevent.entity()accessor. This is used by theTriggerimpls :EntityTrigger,PropagateEntityTrigger, andEntityComponentsTrigger.Messageis a solid metaphor for what this is ... it is data that is written and then at some later point read by someone / something else. I expect existing consumers of "buffered events" to lament this name change, as "event" feels nicer. But having a separate name is within everyone's best interest.The Changes
Eventtrait changesEvent::Trigger, which defines what trigger implementation this event will useTriggertraittrigger_targetshave been replaced bytrigger, which can now be used for anyEventEntityEventtrait changesEntityEventtrait. It now lives on theTriggertrait (specifically thePropagateEntityTriggertrait).EntityEventnow providesentity / entity_mutaccessors for the Event it is implemented forEntityEventdefaults to having no propagation (uses the simplerEntityTrigger)#[entity_event(propagate)]enables the "default" propagation logic (uses ChildOf). The existing#[entity_event(traversal = X)]has been renamed to#[entity_event(propagate = X)EntityEventrequires either a singleMyEvent(Entity), theentityfield name (MyEvent { entity: Entity}), orMyEvent { #[event_entity] custom: Entity }AnimationEventtrait, which sets theAnimationEventTrigger. This allows developers to pass in events that dont include the Entity field (as this is set by the system). The custom trigger also opens the doors to cheaply passing in additional animation system context, accessible throughOnEntityComponentsTriggerEntityComponentsTrigger, which passes in the components as additional state. This significantly cuts down on clones, as it does a borrow rather than cloning the list into each observer execution.entityfield.explode: On<Explode>notevent: On<Explode>event.entity(). This allows us to use more specific names where appropriate, provides better / more contextual docs, and coaches developers to think ofOn<MyEvent>as the event itself.Take a look at the changes to the examples and the built-in events to see what this looks like in practice.
Downsides
trigger_ref, and mutate the event on each call to change the target.trigger_targetbehavior.EntityTargetTrigger(not currently provided by bevy, but we could), which brings back the old behavior. This would be used withtrigger_withto replicate the old pattern:world.trigger_with(MyEvent, [e1, e2].into())(or we could make theinto()implicit)trigger_refwill result in the event'sEntityEvent::entity()being the final bubbled entity instead of the initial entity.#[event(trigger = AnimationEventTrigger)])Draft TODO
Next Steps
BufferedEvent -> Messagerename was not included to keep the size down.Fixes #19648