Skip to content

Commit 7211ecf

Browse files
authored
Add observable OnCollisionStart and OnCollisionEnd events (#704)
# 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: ```rust 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`.
1 parent bac2021 commit 7211ecf

File tree

5 files changed

+302
-72
lines changed

5 files changed

+302
-72
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,6 @@ cargo run --example cubes --no-default-features --features "3d f64 parry-f64"
180180

181181
## Future Features
182182

183-
- Per-entity collision hooks or callbacks
184183
- Flags for what types of collisions are active, like collisions against specific rigid body types, sensors or parents
185184
- Performance optimization (better broad phase, parallel solver, proper SIMD...)
186185
- Joint motors

src/collision/collision_events.rs

Lines changed: 215 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,267 @@
1-
//! [`CollisionStarted`] and [`CollisionEnded`] events.
1+
//! Collision events for detecting when colliders start or stop touching.
22
//!
3-
//! Collision events are only sent if one of the entities has the [`CollisionEventsEnabled`] component.
3+
//! Depending on your use case, you may want to use either buffered events read
4+
//! using an [`EventReader`] or observable events triggered for specific entities.
5+
//! Avian provides both options using separate event types.
46
//!
5-
//! You can listen to these events with normal event readers:
7+
//! Note that collision events are only sent or triggered for entities that have
8+
//! the [`CollisionEventsEnabled`] component.
69
//!
7-
//! ```no_run
10+
//! # Buffered Events
11+
//!
12+
//! Avian provides two different buffered collision event types:
13+
//!
14+
//! - [`CollisionStarted`]
15+
//! - [`CollisionEnded`]
16+
//!
17+
//! These events are sent when two colliders start or stop touching, and can be read
18+
//! using an [`EventReader`]. This can be useful for efficiently processing large numbers
19+
//! of collision events between pairs of entities, such as for detecting bullet hits
20+
//! or playing impact sounds when two objects collide.
21+
//!
22+
//! The events are only sent if one of the entities has the [`CollisionEventsEnabled`] component.
23+
//!
24+
//! ```
825
#![cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
926
#![cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
1027
//! use bevy::prelude::*;
1128
//!
12-
//! fn main() {
13-
//! App::new()
14-
//! .add_plugins((DefaultPlugins, PhysicsPlugins::default()))
15-
//! // ...
16-
//! .add_systems(Update, print_collisions)
17-
//! .run();
18-
//! }
19-
//!
20-
//! fn print_collisions(mut collision_event_reader: EventReader<CollisionStarted>) {
29+
//! fn print_started_collisions(mut collision_event_reader: EventReader<CollisionStarted>) {
2130
//! for CollisionStarted(entity1, entity2) in collision_event_reader.read() {
22-
//! println!("Entities {entity1} and {entity2} are colliding");
31+
//! println!("{entity1} and {entity2} started colliding");
2332
//! }
2433
//! }
2534
//! ```
2635
//!
27-
//! Collision events that use observers are not yet supported.
36+
//! # Observable Events
37+
//!
38+
//! Avian provides two observable collision event types:
39+
//!
40+
//! - [`OnCollisionStart`]
41+
//! - [`OnCollisionEnd`]
42+
//!
43+
//! These events are triggered for [observers](Observer) when two colliders start or stop touching.
44+
//! This makes them good for entity-specific collision scenarios, such as for detecting when a player
45+
//! steps on a pressure plate or enters a trigger volume.
46+
//!
47+
//! The events are only triggered if the target entity has the [`CollisionEventsEnabled`] component.
48+
//!
49+
//! ```
50+
#![cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
51+
#![cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
52+
//! use bevy::prelude::*;
53+
//!
54+
//! #[derive(Component)]
55+
//! struct Player;
56+
//!
57+
//! #[derive(Component)]
58+
//! struct PressurePlate;
59+
//!
60+
//! fn setup_pressure_plates(mut commands: Commands) {
61+
//! commands.spawn((
62+
//! PressurePlate,
63+
#![cfg_attr(feature = "2d", doc = " Collider::rectangle(1.0, 1.0),")]
64+
#![cfg_attr(feature = "3d", doc = " Collider::cuboid(1.0, 0.1, 1.0),")]
65+
//! Sensor,
66+
//! // Enable collision events for this entity.
67+
//! CollisionEventsEnabled,
68+
//! ))
69+
//! .observe(|trigger: Trigger<OnCollisionStart>, player_query: Query<&Player>| {
70+
//! let pressure_plate = trigger.target();
71+
//! let other_entity = trigger.0;
72+
//! if player_query.contains(other_entity) {
73+
//! println!("Player {other_entity} stepped on pressure plate {pressure_plate}");
74+
//! }
75+
//! });
76+
//! }
77+
//! ```
2878
2979
use bevy::prelude::*;
3080

31-
/// A [collision event](super#collision-events) that is sent when two colliders start touching.
81+
/// A buffered [collision event](super#collision-events) that is sent when two colliders start touching.
3282
///
3383
/// The event is only sent if one of the entities has the [`CollisionEventsEnabled`] component.
3484
///
85+
/// Unlike [`OnCollisionStart`], this event is *not* triggered for observers.
86+
/// Instead, you must use an [`EventReader`] to read the event in a system.
87+
/// This makes it good for efficiently processing large numbers of collision events
88+
/// between pairs of entities.
89+
///
3590
/// # Example
3691
///
37-
/// ```no_run
92+
/// ```
3893
#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
3994
#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
4095
/// use bevy::prelude::*;
4196
///
42-
/// fn main() {
43-
/// App::new()
44-
/// .add_plugins((DefaultPlugins, PhysicsPlugins::default()))
45-
/// // ...
46-
/// .add_systems(Update, print_started_collisions)
47-
/// .run();
48-
/// }
49-
///
5097
/// fn print_started_collisions(mut collision_event_reader: EventReader<CollisionStarted>) {
5198
/// for CollisionStarted(entity1, entity2) in collision_event_reader.read() {
52-
/// println!(
53-
/// "Entities {} and {} started colliding",
54-
/// entity1,
55-
/// entity2,
56-
/// );
99+
/// println!("{entity1} and {entity2} started colliding");
57100
/// }
58101
/// }
59102
/// ```
60-
#[derive(Event, Clone, Debug, PartialEq)]
103+
///
104+
/// # Scheduling
105+
///
106+
/// The [`CollisionStarted`] event is sent in the [`NarrowPhaseSet::Update`] system set,
107+
/// but can be read at any time.
108+
///
109+
/// [`NarrowPhaseSet::Update`]: super::narrow_phase::NarrowPhaseSet::Update
110+
#[derive(Event, Clone, Copy, Debug, PartialEq)]
61111
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
62112
pub struct CollisionStarted(pub Entity, pub Entity);
63113

64-
/// A [collision event](super#collision-events) that is sent when two colliders stop touching.
114+
/// A buffered [collision event](super#collision-events) that is sent when two colliders stop touching.
65115
///
66116
/// The event is only sent if one of the entities has the [`CollisionEventsEnabled`] component.
67117
///
118+
/// Unlike [`OnCollisionEnd`], this event is *not* triggered for observers.
119+
/// Instead, you must use an [`EventReader`] to read the event in a system.
120+
/// This makes it good for efficiently processing large numbers of collision events
121+
/// between pairs of entities.
122+
///
68123
/// # Example
69124
///
70-
/// ```no_run
125+
/// ```
71126
#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
72127
#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
73128
/// use bevy::prelude::*;
74129
///
75-
/// fn main() {
76-
/// App::new()
77-
/// .add_plugins((DefaultPlugins, PhysicsPlugins::default()))
78-
/// // ...
79-
/// .add_systems(Update, print_ended_collisions)
80-
/// .run();
81-
/// }
82-
///
83130
/// fn print_ended_collisions(mut collision_event_reader: EventReader<CollisionEnded>) {
84131
/// for CollisionEnded(entity1, entity2) in collision_event_reader.read() {
85-
/// println!(
86-
/// "Entities {} and {} stopped colliding",
87-
/// entity1,
88-
/// entity2,
89-
/// );
132+
/// println!("{entity1} and {entity2} stopped colliding");
90133
/// }
91134
/// }
92135
/// ```
93-
#[derive(Event, Clone, Debug, PartialEq)]
136+
///
137+
/// # Scheduling
138+
///
139+
/// The [`CollisionEnded`] event is sent in the [`NarrowPhaseSet::Update`] system set,
140+
/// but can be read at any time.
141+
///
142+
/// Note that if one of the colliders was removed or the bounding boxes of the colliders stopped
143+
/// overlapping, the [`ContactPair`] between the entities was also removed, and the contact data
144+
/// will not be available through [`Collisions`].
145+
///
146+
/// [`NarrowPhaseSet::Update`]: super::narrow_phase::NarrowPhaseSet::Update
147+
/// [`ContactPair`]: super::ContactPair
148+
/// [`Collisions`]: super::Collisions
149+
#[derive(Event, Clone, Copy, Debug, PartialEq)]
94150
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
95151
pub struct CollisionEnded(pub Entity, pub Entity);
96152

97-
/// A marker component that enables [`CollisionStarted`] and [`CollisionEnded`] events for an entity.
153+
/// A [collision event](super#collision-events) that is triggered for [observers](Observer)
154+
/// when two colliders start touching.
155+
///
156+
/// The event is only triggered if the target entity has the [`CollisionEventsEnabled`] component.
157+
///
158+
/// Unlike [`CollisionStarted`], this event can *not* be read using an [`EventReader`].
159+
/// Instead, you must use an [observer](Observer). This makes it good for entity-specific
160+
/// collision listeners.
161+
///
162+
/// # Example
163+
///
164+
/// ```
165+
#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
166+
#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
167+
/// use bevy::prelude::*;
168+
///
169+
/// #[derive(Component)]
170+
/// struct Player;
171+
///
172+
/// #[derive(Component)]
173+
/// struct PressurePlate;
174+
///
175+
/// fn setup_pressure_plates(mut commands: Commands) {
176+
/// commands.spawn((
177+
/// PressurePlate,
178+
#[cfg_attr(feature = "2d", doc = " Collider::rectangle(1.0, 1.0),")]
179+
#[cfg_attr(feature = "3d", doc = " Collider::cuboid(1.0, 0.1, 1.0),")]
180+
/// Sensor,
181+
/// // Enable collision events for this entity.
182+
/// CollisionEventsEnabled,
183+
/// ))
184+
/// .observe(|trigger: Trigger<OnCollisionStart>, player_query: Query<&Player>| {
185+
/// let pressure_plate = trigger.target();
186+
/// let other_entity = trigger.0;
187+
/// if player_query.contains(other_entity) {
188+
/// println!("Player {other_entity} stepped on pressure plate {pressure_plate}");
189+
/// }
190+
/// });
191+
/// }
192+
/// ```
193+
///
194+
/// # Scheduling
195+
///
196+
/// The [`OnCollisionStart`] event is triggered after the physics step in the [`CollisionEventSystems`]
197+
/// system set. At this point, the solver has already run and contact impulses have been updated.
198+
///
199+
/// [`CollisionEventSystems`]: super::narrow_phase::CollisionEventSystems
200+
#[derive(Event, Clone, Copy, Debug, PartialEq)]
201+
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
202+
pub struct OnCollisionStart(pub Entity);
203+
204+
/// A [collision event](super#collision-events) that is triggered for [observers](Observer)
205+
/// when two colliders stop touching.
206+
///
207+
/// The event is only triggered if the target entity has the [`CollisionEventsEnabled`] component.
208+
///
209+
/// Unlike [`CollisionEnded`], this event can *not* be read using an [`EventReader`].
210+
/// Instead, you must use an [observer](Observer). This makes it good for entity-specific
211+
/// collision listeners.
212+
///
213+
/// # Example
214+
///
215+
/// ```
216+
#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
217+
#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
218+
/// use bevy::prelude::*;
219+
///
220+
/// #[derive(Component)]
221+
/// struct Player;
222+
///
223+
/// #[derive(Component)]
224+
/// struct PressurePlate;
225+
///
226+
/// fn setup_pressure_plates(mut commands: Commands) {
227+
/// commands.spawn((
228+
/// PressurePlate,
229+
#[cfg_attr(feature = "2d", doc = " Collider::rectangle(1.0, 1.0),")]
230+
#[cfg_attr(feature = "3d", doc = " Collider::cuboid(1.0, 0.1, 1.0),")]
231+
/// Sensor,
232+
/// // Enable collision events for this entity.
233+
/// CollisionEventsEnabled,
234+
/// ))
235+
/// .observe(|trigger: Trigger<OnCollisionEnd>, player_query: Query<&Player>| {
236+
/// let pressure_plate = trigger.target();
237+
/// let other_entity = trigger.0;
238+
/// if player_query.contains(other_entity) {
239+
/// println!("Player {other_entity} stepped off of pressure plate {pressure_plate}");
240+
/// }
241+
/// });
242+
/// }
243+
/// ```
244+
///
245+
/// # Scheduling
246+
///
247+
/// The [`OnCollisionEnd`] event is triggered after the physics step in the [`CollisionEventSystems`]
248+
/// system set. At this point, the solver has already run and contact impulses have been updated.
249+
///
250+
/// Note that if one of the colliders was removed or the bounding boxes of the colliders stopped
251+
/// overlapping, the [`ContactPair`] between the entities was also removed, and the contact data
252+
/// will not be available through [`Collisions`].
253+
///
254+
/// [`CollisionEventSystems`]: super::narrow_phase::CollisionEventSystems
255+
/// [`ContactPair`]: super::ContactPair
256+
/// [`Collisions`]: super::Collisions
257+
#[derive(Event, Clone, Copy, Debug, PartialEq)]
258+
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
259+
pub struct OnCollisionEnd(pub Entity);
260+
261+
/// A marker component that enables [collision events](self) for an entity.
262+
///
263+
/// This enables both the buffered [`CollisionStarted`] and [`CollisionEnded`] events,
264+
/// as well as the observable [`OnCollisionStart`] and [`OnCollisionEnd`] events.
98265
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
99266
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
100267
#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]

src/collision/mod.rs

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,36 +32,28 @@
3232
//!
3333
//! # Collision Events
3434
//!
35-
//! The following events are sent whenever two colliders start or stop touching:
35+
//! Collision events can be used for detecting when colliders start or stop touching.
36+
//!
37+
//! Avian provides two buffered collision event types that can be read using an [`EventReader`](bevy::ecs::event::EventReader):
3638
//!
3739
//! - [`CollisionStarted`]
3840
//! - [`CollisionEnded`]
3941
//!
40-
//! Collision events are only sent if one of the entities has the [`CollisionEventsEnabled`] component.
42+
//! These events are good for efficiently processing large numbers of collision events between pairs of entities,
43+
//! such as for detecting bullet hits or playing impact sounds when two objects collide.
4144
//!
42-
//! You can listen to these events with normal event readers:
45+
//! Avian also provides two collision event types that are triggered for observers:
4346
//!
44-
//! ```no_run
45-
#![cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
46-
#![cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
47-
//! use bevy::prelude::*;
47+
//! - [`OnCollisionStart`]
48+
//! - [`OnCollisionEnd`]
4849
//!
49-
//! fn main() {
50-
//! App::new()
51-
//! .add_plugins((DefaultPlugins, PhysicsPlugins::default()))
52-
//! // ...
53-
//! .add_systems(Update, print_collisions)
54-
//! .run();
55-
//! }
50+
//! These events are good for entity-specific collision scenarios, such as for detecting when a player
51+
//! steps on a pressure plate or enters a trigger volume.
5652
//!
57-
//! fn print_collisions(mut collision_event_reader: EventReader<CollisionStarted>) {
58-
//! for CollisionStarted(entity1, entity2) in collision_event_reader.read() {
59-
//! println!("Entities {entity1} and {entity2} are colliding");
60-
//! }
61-
//! }
62-
//! ```
53+
//! Collision events are only sent or triggered for entities that have the [`CollisionEventsEnabled`] component.
6354
//!
64-
//! Collision events that use observers are not yet supported.
55+
//! See the documentation of the event types and the [`collision_events`] module
56+
//! for more information and usage examples.
6557
//!
6658
//! # Contact Filtering and Modification
6759
//!
@@ -99,7 +91,9 @@ pub mod prelude {
9991
IntoCollider, LayerMask, PhysicsLayer, ScalableCollider, Sensor, SimpleCollider,
10092
TrimeshFlags, VhacdParameters,
10193
};
102-
pub use super::collision_events::{CollisionEnded, CollisionEventsEnabled, CollisionStarted};
94+
pub use super::collision_events::{
95+
CollisionEnded, CollisionEventsEnabled, CollisionStarted, OnCollisionEnd, OnCollisionStart,
96+
};
10397
pub use super::contact_types::{
10498
Collisions, ContactGraph, ContactManifold, ContactPair, ContactPairFlags, ContactPoint,
10599
};

0 commit comments

Comments
 (0)