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

Suggestion: Entity Events #2070

Closed
forbjok opened this issue May 1, 2021 · 14 comments
Closed

Suggestion: Entity Events #2070

forbjok opened this issue May 1, 2021 · 14 comments
Labels
A-ECS Entities, components, systems, and events C-Enhancement A new feature C-Examples An addition or correction to our examples

Comments

@forbjok
Copy link
Contributor

forbjok commented May 1, 2021

What problem does this solve or what need does it fill?

Let's say you have a common system for handling a specific type of behavior - for example falling.
The logic for falling is the same for all types of objects capable of falling, so it makes sense for a single common system to handle this for all entities that need to be able to fall.
The common system will perform various collision checks and other logic to make the entity fall, and determine whether or not the fall resulted in it hitting something.

The obvious first solution would be to use events for this, and have the falling system emit an event whenever the entity hit something.

fn falling_system(
  query: Query<(Entity, &Falling)>,
  event_writer: EventWriter<HitSomething>,
) {
  for (entity, falling) in query.iter() {
    let has_hit_something = {
      // Execute fall logic and determine whether the entity hit something or not
    };

    if has_hit_something {
      event_writer.send(HitSomething { entity });
    }
  }
}

If there is only one type of entity falling, or all entities capable of falling should respond exactly the same way to hitting something, then this would be fine.
However, different types of entities need to respond differently to hitting something.
For example, when a rock hits the ground, nothing would happen - but if an egg hits the ground, it should break.

This could be handled by doing something like:

fn egg_break_on_hit_system(
  egg_query: Query<(&Egg,)>,
  event_reader: EventReader<HitSomething>,
) {
  for ev in event_reader.iter() {
    if let Some((egg,)) = egg_query.get(ev.entity) {
      // Execute egg break logic
    }
  }
}

However, this essentially means every type of entity that needs to respond to hitting something has to iterate every single of these events, regardless of whether they are relevant to their entity composition, and make a query for each one to determine whether it needs to do something.

This is at worst inefficient, and at best inelegant and clunky.

What solution would you like?

I've thought about this problem for a bit, and the potential solution I've come up with is something I've called EntityEvents.
Basically, a different type of events that are always associated with a specific entity, and can be queried based on components in the same way as you would in a regular query.

With this, the code example above would instead become something like this:

fn falling_system(
  query: Query<(Entity, &Falling)>,
  entity_event_writer: EntityEventWriter<HitSomething>,
) {
  for (entity, falling) in query.iter() {
    let has_hit_something = {
      // Execute fall logic and determine whether the entity hit something or not
    };

    if has_hit_something {
      entity_event_writer.send(entity, HitSomething);
    }
  }
}
fn egg_break_on_hit_system(
  entity_event_reader: EntityEventReader<HitSomething, With<Egg>>,
) {
  for (entity, ev) in entity_event_reader.iter() {
    // Execute egg break logic
  }
}

No need to iterate over and check a bunch of irrelevant events in every system that needs them.
No arbitrary and annoying limitations, such as a forced 1-frame delay or being unable to send multiple events of the same type per cycle.
No clunky and error-prone manual cleanup system required.

I don't know whether this is actually feasible to implement, as my understanding of how the ECS internals in Bevy work is extremely limited, but at least from a user point of view this seems like a pretty elegant and clean solution.

What alternative(s) have you considered?

There are some possible ways to work around this, the most obvious being the one I described above.
Another, and the one I ended up using personally in the game this example situation came up in, is to use the "components as events" pattern. Simply adding a component to the entity when it hits something, instead of sending an event, and then having the different entity types' systems check for that component.

This avoids doing unnecessary queries, but instead has a number of other problems.
The most immediately obvious being that inserting a component is not instant, so if you check for the component later in the same update cycle as it was added, it will not be found. Also, these components will need to be manually removed by a cleanup system each cycle.

The workaround I ended up using for these issues was to ensure that the cleanup system runs before any system producing the "event" component, and the systems handling these events run before the cleanup system.
This works, but ensures that there is always a 1-frame delay between the events being produced and being handled, which would make it unusable in any situation where immediate responsiveness is of importance. (such as player movement)
It also feels rather inelegant and clunky.

Another important limitation of this is that there would be no way to send multiple "events" of the same type each cycle.

@forbjok forbjok added C-Enhancement A new feature S-Needs-Triage This issue needs to be labelled labels May 1, 2021
@alice-i-cecile alice-i-cecile added A-Core Common functionality for all bevy apps and removed S-Needs-Triage This issue needs to be labelled labels May 1, 2021
@alice-i-cecile
Copy link
Member

Aha, this is the same use case that led @TheRawMeatball to invent the "events stored in components as channels" pattern.
The core idea is that rather than storing the Event<T> as a resource, we can store it as a component on a particular entity, and then read off those individual event queues.

By keeping the component around (sometimes empty) we can avoid the frame delay and reuse much of the existing Event machinery. I think this should be almost entirely feasible without any existing code changes, but the design pattern is subtle and tricky enough that I think it deserves a working example in this repo. Adding that example will also let us catch any other missing functionality that we need.

P.S. I've been interested in using this for my UI design work as well: particularly for input dispatching where you'd convert raw input event resource into actions stored on the components of each widget entity. I have some very early thoughts on this if you'd like to take a look at my pre-draft RFC.

@alice-i-cecile alice-i-cecile added the C-Examples An addition or correction to our examples label May 1, 2021
@forbjok
Copy link
Contributor Author

forbjok commented May 1, 2021

By keeping the component around (sometimes empty) we can avoid the frame delay and reuse much of the existing Event machinery.

Would this mean you could do it without requiring a manual cleanup system for each of these event types?

@alice-i-cecile
Copy link
Member

Events themselves have a manual cleanup system for each event type already ;) That's what add_event does, over simply add_resource. However, I think it would make sense to extend out that functionality to also clean up events stored as components, which should be a simple enough change and won't result in any additional boilerplate for the end user.

@alice-i-cecile
Copy link
Member

alice-i-cecile commented May 1, 2021

You can see what add_events does here. Following the breadcrumbs, we come across a nice explanation of how this ultimately work over in bevy_ecs/events.rs.

TBH, I suspect that simply querying for all components of Events<T> in that update system and calling .update() on each of them should do the trick.

@forbjok
Copy link
Contributor Author

forbjok commented May 1, 2021

If the internal functionality to achieve this already exists, then maybe some convenience traits similar to what I describe could be written as wrappers around it to make it more convenient to use?

@alice-i-cecile
Copy link
Member

Yep, I think we can make this quite ergonomic, although I don't know if we actually need convenience traits. I can start on a PR for this functionality in a week or so; I'll make a nice example and shave off any rough edges I encounter in the process.

@alice-i-cecile alice-i-cecile self-assigned this May 1, 2021
@TheRawMeatball
Copy link
Member

While events in entities has the potential to be a good solution, I don't think it is the correct tool for this kind of situation: each Events instance carries 2 Vec s, and 2 heap allocations per entity has the potential to be very bad for performance. Instead, some alternative patterns can be used to communicate this intent:

struct HitDetector;

fn falling_system(
  query: Query<(Entity, &Falling, &mut HitDetector)>,
) {
  for (entity, falling, hd) in query.iter() {
    let has_hit_something = {
      // Execute fall logic and determine whether the entity hit something or not
    };

    if has_hit_something {
      // this triggers change detection code, letting us use this stable channel of communication
      let _ = &mut *hd;
    }
  }
}

fn egg_break_on_hit_system(
  // the Changed bound directly filters for entities who recently changed
  query EntityEventReader<Entity, (With<Egg>, Changed<HitDetector>)>,
) {
  for entity in query.iter() {
    // Execute egg break logic
  }
}

This pattern could also be used to transmit data, by placing data inside of the HitDetector type:

struct HitDetector {
  hit_speed: f32,
}

fn falling_system(
  query: Query<(Entity, &Falling, &mut HitDetector)>,
) {
  for (entity, falling, hd) in query.iter() {
    let hit_velocity: Option<f32> = {
      // Execute fall logic and determine whether the entity hit something or not
    };

    if let Some(v) = hit_velocity {
      // this triggers change detection code, letting us use this stable channel of communication
      *hd = v;
    }
  }
}

fn egg_break_on_hit_system(
  // the Changed bound directly filters for entities who recently changed
  query EntityEventReader<(Entity, HitSomething), (With<Egg>, Changed<HitSomething>)>,
) {
  for (entity, hit_speed) in query.iter() {
    // Execute egg break logic
  }
}

@alice-i-cecile
Copy link
Member

#2071 should significantly limit the perf impact of this pattern; allowing the choice to come down to ergonomics and modelling of the problem domain.

In particular, this pattern is much more useful when you may have more than one event you need to process at once within a time frame.

@forbjok
Copy link
Contributor Author

forbjok commented May 1, 2021

While events in entities has the potential to be a good solution, I don't think it is the correct tool for this kind of situation: each Events instance carries 2 Vec s, and 2 heap allocations per entity has the potential to be very bad for performance. Instead, some alternative patterns can be used to communicate this intent:

This would also work, but in cases where you want to be able to detect multiple occurrences in one cycle, you'd end up putting a Vec or some other kind of collection in your component regardless, and just like some of the other solutions, it would require a manual cleanup system to reset the component between cycles.

@ItsDoot
Copy link
Contributor

ItsDoot commented Nov 27, 2023

Update: As of writing, bevy_eventlistener is a performant 3rd-party solution which is likely to be upstreamed in some form.

@ItsDoot ItsDoot added A-ECS Entities, components, systems, and events and removed A-Core Common functionality for all bevy apps labels Nov 27, 2023
@DrewRidley
Copy link

I mentioned this on the discord, but I think the final implementation should probably enforce some kind of fragmentation-reduction. In most cases, the messages will be very small, but the cost of fragmentation (especially with numerous message types) will be quite significant.

For most software problems, there is always a trade-off between performance and memory usage. I think this is especially relevant for entity events, and this trade-off should be explicitly available to the user depending on their requirements.

My suggestion is that, in particular for small messages, the buffer component is included on all entities (or some subset that might at some point have this event directed to them). This would decrease fragmentation and improve the iteration performance considerably.

@alice-i-cecile
Copy link
Member

That upstreaming process is going to be tracked in #12365 :)

@alice-i-cecile alice-i-cecile removed their assignment Jun 25, 2024
@forbjok
Copy link
Contributor Author

forbjok commented Jul 6, 2024

As far as I can tell, the new Trigger system in Bevy 0.14 can be used to do essentially this by using commands.trigger_targets() and having the entity .observe() the event type. The most obvious difference is that this way you can't process the events in a loop in a system, but instead get a function call per event trigger. Is there any significant downside to using triggers for this?

@alice-i-cecile
Copy link
Member

Yeah, IMO this is effectively resolved with the introduction of observers.

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-Enhancement A new feature C-Examples An addition or correction to our examples
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants