Skip to content

Add observable OnCollisionStart and OnCollisionEnd events#704

Merged
Jondolf merged 5 commits intomainfrom
observable-collisions
Apr 18, 2025
Merged

Add observable OnCollisionStart and OnCollisionEnd events#704
Jondolf merged 5 commits intomainfrom
observable-collisions

Conversation

@Jondolf
Copy link
Member

@Jondolf Jondolf commented Apr 17, 2025

Objective

Closes #361.
Closes #481.

A common user request is to allow listening to collision events for specific entities, with an API where you don't need to check e.g. which entity is the player and which entity is an enemy like you might need to do when using CollisionStarted and CollisionEnded. The typical Bevy API for per-entity events like this is observers.

On the surface, this is a straightforward addition. However, there have historically been some challenges and open questions:

  1. We don't want to trigger collision events for every contact. Doing so would mean that we get one buffered event and two observable events (one for each entity) per started/ended contact pair, which could add up to a decent amount of overhead.
  2. Observable events shouldn't store two entities like CollisionStarted and CollisionEnded do. The Trigger already stores the observed entity.
  3. Should we trigger the observers immediately in the narrow phase, which could be slightly more efficient, or should we defer events until after the solver so that contact impulses have been computed?

Now that #683 made collision events opt-in via the CollisionEventsEnabled component, I consider (1) to be solved. We can only trigger the event for entities with that component. The other two are more just a matter of how we want to design this, but I would assert that (2) we should just have separate types for buffered and observable events, and (3) we should trigger the events after the solver, because having access to contact impulses is valuable, even if it has a small extra cost.

Solution

Add OnCollisionStart and OnCollisionEnd events that are triggered when an entity starts or stops colliding with another entity. The naming was chosen to be distinct enough from CollisionStarted and CollisionEnded, while reflecting Bevy's observer event names such as OnAdd or Pointer<Move> (present tense verb).

The events are triggered after the solver by iterating through the buffered events and checking which entities have CollisionEventsEnabled. System-local buffers and exclusive world access are used to try and minimize overhead and unnecessary allocations.

Usage of the events might look like this:

use avian3d::prelude::*;
use bevy::prelude::*;

#[derive(Component)]
struct Player;

#[derive(Component)]
struct PressurePlate;

fn setup_pressure_plates(mut commands: Commands) {
    commands.spawn((
        PressurePlate,
        Collider::cuboid(1.0, 0.1, 1.0),
        Sensor,
        // Enable collision events for this entity.
        CollisionEventsEnabled,
    ))
    .observe(|trigger: Trigger<OnCollisionStart>, player_query: Query<&Player>| {
        let pressure_plate = trigger.entity();
        if player_query.contains(trigger.0) {
            println!("Player {trigger.0} stepped on pressure plate {pressure_plate}");
        }
    });
}

The system that triggers the events runs in a CollisionEventSystems system set in a new PhysicsStepSet::Finalize system set right before PhysicsStepSet::Last.

@Jondolf Jondolf added C-Feature A new feature, making something new possible A-Collision Relates to the broad phase, narrow phase, colliders, or other collision functionality labels Apr 17, 2025
@Jondolf Jondolf added this to the 0.3 milestone Apr 17, 2025
@Jondolf Jondolf enabled auto-merge (squash) April 18, 2025 10:40
@Jondolf Jondolf merged commit 7211ecf into main Apr 18, 2025
5 checks passed
@Jondolf Jondolf deleted the observable-collisions branch April 18, 2025 11:38
Jondolf added a commit that referenced this pull request May 4, 2025
…717)

# Objective

#704 added observable `OnCollisionStart` and `OnCollisionEnd` events. However, they only provide the collider entity, and that is also just behind a nameless tuple field. It could be nicer to use named fields and also provide the rigid body entity.

## Solution

Use named `collider` and `rigid_body` fields. `rigid_body` is `None` if the collider is not attached to a rigid body.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Collision Relates to the broad phase, narrow phase, colliders, or other collision functionality C-Feature A new feature, making something new possible

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Observable collision events Entity events support

1 participant