Skip to content

Suggestion: Entity Events #2070

Closed
Closed
@forbjok

Description

@forbjok

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-ECSEntities, components, systems, and eventsC-ExamplesAn addition or correction to our examplesC-FeatureA new feature, making something new possible

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions