Skip to main content

bevy_rapier3d/pipeline/
events.rs

1use crate::math::{Real, Vect};
2use bevy::ecs::message::Message;
3use bevy::prelude::Entity;
4use rapier::dynamics::{RigidBodySet, SoftBodySet, SoftBodyTearEvent as RapierSoftBodyTearEvent};
5use rapier::geometry::{
6    ColliderHandle, ColliderSet, CollisionEvent as RapierCollisionEvent, CollisionEventFlags,
7    ContactForceEvent as RapierContactForceEvent, ContactPair,
8};
9use rapier::pipeline::EventHandler;
10use std::collections::HashMap;
11use std::sync::RwLock;
12
13#[cfg(doc)]
14use crate::prelude::{
15    ActiveEvents, ContactForceEventThreshold, RapierContext, RapierContextMut,
16    RapierContextSimulation, SoftBodyAttachments, SoftBodyExternalForce, SoftBodyExternalImpulse,
17    SoftBodyKinematicTargets, SoftBodyPinnedParticles,
18};
19
20/// Events occurring when two colliders start or stop colliding
21///
22/// This will only get triggered if the entity has the
23/// [`ActiveEvents::COLLISION_EVENTS`] flag enabled.
24///
25/// This event only identifies the colliders involved. The detailed contact geometry
26/// (manifolds, contact points, normals, impulses) of a non-sensor pair can be read from the
27/// narrow-phase with [`RapierContext::contact_pair`] (or
28/// [`RapierContextSimulation::contact_pair`]) while the pair still exists, i.e., for
29/// [`CollisionEvent::Started`], and for [`CollisionEvent::Stopped`] unless one of the colliders
30/// was removed. For sensors, use [`RapierContext::intersection_pair`] instead. To access the raw
31/// Rapier contact pair at the exact time the event is emitted, install a custom
32/// [`EventHandler`] with [`RapierContextSimulation::set_event_handler`].
33#[derive(Message, Copy, Clone, Debug, PartialEq, Eq)]
34pub enum CollisionEvent {
35    /// Event occurring when two colliders start colliding
36    Started(Entity, Entity, CollisionEventFlags),
37    /// Event occurring when two colliders stop colliding
38    Stopped(Entity, Entity, CollisionEventFlags),
39}
40
41/// Event emitted after a simulation step during which Rapier detected non-finite (NaN or
42/// infinite) state and neutralized it.
43///
44/// Each reported rigid-body was rolled back to its last valid pose (when known), had its
45/// velocities and forces zeroed, and was disabled inside of Rapier; each reported collider was
46/// disabled inside of Rapier; each reported soft body was disabled and had its velocities zeroed
47/// (its non-finite particle positions are left as is). The plugin mirrors this by inserting
48/// [`RigidBodyDisabled`], [`ColliderDisabled`] or [`SoftBodyDisabled`] on the quarantined
49/// entities. Once the offending state is fixed (e.g. by writing a finite [`Velocity`],
50/// [`Transform`] or [`Collider`], or by moving the particles with
51/// [`RapierSoftBody::set_particle_position`]), remove that component to re-enable the entity. A
52/// warning is also logged for each event.
53///
54/// [`RigidBodyDisabled`]: crate::dynamics::RigidBodyDisabled
55/// [`ColliderDisabled`]: crate::geometry::ColliderDisabled
56/// [`SoftBodyDisabled`]: crate::dynamics::SoftBodyDisabled
57/// [`Velocity`]: crate::dynamics::Velocity
58/// [`Collider`]: crate::geometry::Collider
59/// [`Transform`]: bevy::prelude::Transform
60/// [`RapierSoftBody::set_particle_position`]: crate::dynamics::RapierSoftBody::set_particle_position
61#[derive(Message, Clone, Debug, PartialEq, Eq)]
62pub struct PhysicsQuarantineEvent {
63    /// The entity of the Rapier context in which the non-finite state was detected.
64    pub context: Entity,
65    /// The rigid-body entities quarantined during the step.
66    pub bodies: Vec<Entity>,
67    /// The collider entities quarantined during the step (because their own geometry or
68    /// position became non-finite).
69    pub colliders: Vec<Entity>,
70    /// The soft-body entities quarantined during the step (because a particle's position or
71    /// velocity became non-finite).
72    pub soft_bodies: Vec<Entity>,
73}
74
75/// Event occurring when the sum of the magnitudes of the contact forces
76/// between two colliders exceed a threshold ([`ContactForceEventThreshold`]).
77///
78/// This will only get triggered if the entity has the
79/// [`ActiveEvents::CONTACT_FORCE_EVENTS`] flag enabled.
80#[derive(Message, Copy, Clone, Debug, PartialEq)]
81pub struct ContactForceEvent {
82    /// The first collider involved in the contact.
83    pub collider1: Entity,
84    /// The second collider involved in the contact.
85    pub collider2: Entity,
86    /// The sum of all the forces between the two colliders.
87    pub total_force: Vect,
88    /// The sum of the magnitudes of each force between the two colliders.
89    ///
90    /// Note that this is **not** the same as the magnitude of `self.total_force`.
91    /// Here we are summing the magnitude of all the forces, instead of taking
92    /// the magnitude of their sum.
93    pub total_force_magnitude: Real,
94    /// The world-space (unit) direction of the force with strongest magnitude.
95    pub max_force_direction: Vect,
96    /// The magnitude of the largest force at a contact point of this contact pair.
97    pub max_force_magnitude: Real,
98    /// Is this the first step the total force of this pair exceeded its threshold?
99    ///
100    /// This is `true` on the step the force crosses the [`ContactForceEventThreshold`]
101    /// from below (or from not touching), and `false` while it stays above it on subsequent
102    /// steps. It becomes `true` again after the force drops below the threshold or the
103    /// colliders separate.
104    pub started: bool,
105}
106
107/// Message sent after a soft body tore (an element loaded past its tear threshold, or a tear or
108/// cut requested through [`RapierContextMut::tear_soft_body`] or
109/// [`RapierContextMut::cut_soft_body`]).
110///
111/// When a tear splits the soft body into disconnected pieces, the largest piece keeps the torn
112/// soft body and its entity; each other piece becomes a new soft body for which the plugin spawns
113/// a new entity (or uses the one already returned by the manual tear, see
114/// [`SoftBodyTearResult`]). That entity is a clone of the torn soft body's entity (its material,
115/// mesh synchronization, render components, user components...), except for the components tied
116/// to the torn body's particle indices ([`SoftBodyPinnedParticles`], [`SoftBodyKinematicTargets`],
117/// [`SoftBodyAttachments`], [`SoftBodyExternalForce`], [`SoftBodyExternalImpulse`]), which are
118/// remapped to the particles of each piece, and its mesh (a new one is generated if
119/// [`SoftBodyMeshSync`] is present).
120///
121/// [`SoftBodyMeshSync`]: crate::dynamics::SoftBodyMeshSync
122#[derive(Message, Clone, Debug)]
123pub struct SoftBodyTearEvent {
124    /// The entity of the Rapier context the soft body belongs to.
125    pub context: Entity,
126    /// The entity of the soft body that tore.
127    pub soft_body: Entity,
128    /// The soft bodies the torn body came apart into (empty if nothing was split off).
129    ///
130    /// The first piece is the torn soft body itself (it keeps [`Self::soft_body`] as entity), the
131    /// others are the entities spawned for the new soft bodies. The particle indices follow those
132    /// of [`Self::raw`].
133    pub pieces: Vec<SoftBodyTearPiece>,
134    /// The pieces of every soft-body cluster the tear split (see [`SoftBodyClusterSplit`]).
135    pub cluster_splits: Vec<SoftBodyClusterSplit>,
136    /// The impulse joints moved from a cluster proxy to another by a cluster split.
137    pub moved_joints: Vec<SoftBodyJointMove>,
138    /// The raw event from Rapier, with every detail of the topology change in terms of Rapier
139    /// handles and particle indices.
140    pub raw: RapierSoftBodyTearEvent,
141}
142
143/// The result of a tear or cut requested through [`RapierContextMut::tear_soft_body`] or
144/// [`RapierContextMut::cut_soft_body`].
145///
146/// The entities of the pieces are known right away: the soft bodies of the pieces are mapped to
147/// them immediately (so the helpers of [`RapierRigidBodySet`] find them), and the entities are
148/// spawned by the given `Commands`. They receive their components (cloned from the torn entity,
149/// see [`SoftBodyTearEvent`]) during the next [`PhysicsSet::Writeback`], when the
150/// [`SoftBodyTearEvent`] message of this tear is sent. The entities of the clusters split by the
151/// tear are only spawned then (see [`SoftBodyTearEvent::cluster_splits`]).
152///
153/// [`RapierRigidBodySet`]: crate::plugin::context::RapierRigidBodySet
154/// [`PhysicsSet::Writeback`]: crate::plugin::PhysicsSet::Writeback
155#[derive(Clone, Debug)]
156pub struct SoftBodyTearResult {
157    /// The entities of the soft bodies the torn body came apart into, in the order of the pieces
158    /// of [`Self::raw`] (empty if nothing was split off). The first one is the torn soft body's
159    /// entity.
160    pub pieces: Vec<Entity>,
161    /// The raw event from Rapier.
162    pub raw: RapierSoftBodyTearEvent,
163}
164
165/// A soft body resulting from a tear (see [`SoftBodyTearEvent::pieces`]).
166#[derive(Clone, Debug, PartialEq, Eq)]
167pub struct SoftBodyTearPiece {
168    /// The entity of the soft body holding the piece.
169    pub soft_body: Entity,
170    /// The particles of the piece: `particles[i]` is the index, in the torn body after the tear,
171    /// of the piece's `i`-th particle.
172    pub particles: Vec<u32>,
173}
174
175/// A piece of a soft-body cluster split by a tear (see [`SoftBodyTearEvent::cluster_splits`]).
176#[derive(Copy, Clone, Debug, PartialEq, Eq)]
177pub struct SoftBodyClusterSplit {
178    /// The index of the split cluster in the torn body, before the tear.
179    pub source_cluster: u32,
180    /// The entity of the soft body holding the piece.
181    pub soft_body: Entity,
182    /// The piece's cluster index in that soft body.
183    pub cluster: u32,
184    /// The entity the piece's proxy rigid-body maps to: the [`SoftBodyCluster`] entity of the
185    /// cluster (a new one is spawned for a piece given a fresh proxy), or the soft body entity for
186    /// a cluster without entity.
187    ///
188    /// [`SoftBodyCluster`]: crate::dynamics::SoftBodyCluster
189    pub proxy: Option<Entity>,
190    /// Whether this piece kept the proxy of the source cluster.
191    pub keeps_proxy: bool,
192}
193
194/// An impulse joint moved to another cluster proxy by a tear (see
195/// [`SoftBodyTearEvent::moved_joints`]).
196///
197/// The [`ImpulseJoint`](crate::dynamics::ImpulseJoint) component of the joint entity is not
198/// modified: its `parent` may no longer match the Rapier joint.
199#[derive(Copy, Clone, Debug, PartialEq, Eq)]
200pub struct SoftBodyJointMove {
201    /// The entity of the joint.
202    pub joint: Option<Entity>,
203    /// The entity of the proxy the joint was attached to.
204    pub from: Option<Entity>,
205    /// The entity of the proxy the joint is attached to now.
206    pub to: Option<Entity>,
207}
208
209/// Forwards every event emitted by Rapier to Bevy's event queue and to the user-provided
210/// event handler (see [`RapierContextSimulation::set_event_handler`]), if any.
211pub(crate) struct EventHandlerFanOut<'a> {
212    pub queue: Option<&'a EventQueue<'a>>,
213    pub user: Option<&'a (dyn EventHandler + Send + Sync)>,
214    /// Collects the soft-body tear events, processed after the step to spawn the piece entities.
215    pub soft_body_tears: &'a RwLock<Vec<RapierSoftBodyTearEvent>>,
216}
217
218impl EventHandlerFanOut<'_> {
219    fn handlers(&self) -> impl Iterator<Item = &dyn EventHandler> {
220        self.queue
221            .map(|q| q as &dyn EventHandler)
222            .into_iter()
223            .chain(self.user.map(|h| h as &dyn EventHandler))
224    }
225}
226
227impl EventHandler for EventHandlerFanOut<'_> {
228    fn handle_collision_event(
229        &self,
230        bodies: &RigidBodySet,
231        colliders: &ColliderSet,
232        event: RapierCollisionEvent,
233        contact_pair: Option<&ContactPair>,
234    ) {
235        for handler in self.handlers() {
236            handler.handle_collision_event(bodies, colliders, event, contact_pair);
237        }
238    }
239
240    fn handle_contact_force_event(
241        &self,
242        dt: Real,
243        bodies: &RigidBodySet,
244        colliders: &ColliderSet,
245        contact_pair: &ContactPair,
246        total_force_magnitude: Real,
247    ) {
248        for handler in self.handlers() {
249            handler.handle_contact_force_event(
250                dt,
251                bodies,
252                colliders,
253                contact_pair,
254                total_force_magnitude,
255            );
256        }
257    }
258
259    fn handle_soft_body_tear_event(
260        &self,
261        soft_bodies: &SoftBodySet,
262        event: &RapierSoftBodyTearEvent,
263    ) {
264        if let Ok(mut tears) = self.soft_body_tears.write() {
265            tears.push(event.clone());
266        }
267        for handler in self.handlers() {
268            handler.handle_soft_body_tear_event(soft_bodies, event);
269        }
270    }
271}
272
273// TODO: it may be more efficient to use crossbeam channel.
274// However crossbeam channels cause a Segfault (I have not
275// investigated how to reproduce this exactly to open an
276// issue).
277/// A set of queues collecting events emitted by the physics engine.
278pub(crate) struct EventQueue<'a> {
279    // Used to retrieve the entity of colliders that have been removed from the simulation
280    // since the last physics step.
281    pub deleted_colliders: &'a HashMap<ColliderHandle, Entity>,
282    pub collision_events: RwLock<Vec<CollisionEvent>>,
283    pub contact_force_events: RwLock<Vec<ContactForceEvent>>,
284}
285
286impl EventQueue<'_> {
287    fn collider2entity(&self, colliders: &ColliderSet, handle: ColliderHandle) -> Option<Entity> {
288        let entity = colliders
289            .get(handle)
290            .and_then(|co| Entity::try_from_bits(co.user_data as u64))
291            .or_else(|| self.deleted_colliders.get(&handle).copied());
292        if entity.is_none() {
293            // This happens for colliders Rapier removed by itself (e.g. the colliders of a torn
294            // soft body rebuilt by the tear) before the event was emitted.
295            log::debug!("No entity found for collider {handle:?}; its event is dropped.");
296        }
297        entity
298    }
299}
300
301impl EventHandler for EventQueue<'_> {
302    fn handle_collision_event(
303        &self,
304        _bodies: &RigidBodySet,
305        colliders: &ColliderSet,
306        event: RapierCollisionEvent,
307        _: Option<&ContactPair>,
308    ) {
309        let event = match event {
310            RapierCollisionEvent::Started(h1, h2, flags) => {
311                let (Some(e1), Some(e2)) = (
312                    self.collider2entity(colliders, h1),
313                    self.collider2entity(colliders, h2),
314                ) else {
315                    return;
316                };
317                CollisionEvent::Started(e1, e2, flags)
318            }
319            RapierCollisionEvent::Stopped(h1, h2, flags) => {
320                let (Some(e1), Some(e2)) = (
321                    self.collider2entity(colliders, h1),
322                    self.collider2entity(colliders, h2),
323                ) else {
324                    return;
325                };
326                CollisionEvent::Stopped(e1, e2, flags)
327            }
328        };
329
330        if let Ok(mut events) = self.collision_events.write() {
331            events.push(event);
332        }
333    }
334
335    fn handle_contact_force_event(
336        &self,
337        dt: Real,
338        _bodies: &RigidBodySet,
339        colliders: &ColliderSet,
340        contact_pair: &ContactPair,
341        total_force_magnitude: Real,
342    ) {
343        let rapier_event =
344            RapierContactForceEvent::from_contact_pair(dt, contact_pair, total_force_magnitude);
345        let (Some(collider1), Some(collider2)) = (
346            self.collider2entity(colliders, rapier_event.collider1),
347            self.collider2entity(colliders, rapier_event.collider2),
348        ) else {
349            return;
350        };
351        let event = ContactForceEvent {
352            collider1,
353            collider2,
354            total_force: rapier_event.total_force,
355            total_force_magnitude: rapier_event.total_force_magnitude,
356            max_force_direction: rapier_event.max_force_direction,
357            max_force_magnitude: rapier_event.max_force_magnitude,
358            started: rapier_event.started,
359        };
360
361        if let Ok(mut events) = self.contact_force_events.write() {
362            events.push(event);
363        }
364    }
365    // Tear events are collected by `EventHandlerFanOut` and turned into Bevy messages after the
366    // step, once the entities of the new pieces are spawned.
367    fn handle_soft_body_tear_event(
368        &self,
369        _soft_bodies: &SoftBodySet,
370        _event: &RapierSoftBodyTearEvent,
371    ) {
372    }
373}
374
375#[cfg(test)]
376mod test {
377    use bevy::{
378        app::{App, Startup, Update},
379        prelude::{Commands, Component, Entity, Query, With},
380        time::{TimePlugin, TimeUpdateStrategy},
381        transform::{components::Transform, TransformPlugin},
382        MinimalPlugins,
383    };
384
385    use crate::{plugin::*, prelude::*};
386
387    #[cfg(feature = "dim3")]
388    fn cuboid(hx: Real, hy: Real, hz: Real) -> Collider {
389        Collider::cuboid(hx, hy, hz)
390    }
391    #[cfg(feature = "dim2")]
392    fn cuboid(hx: Real, hy: Real, _hz: Real) -> Collider {
393        Collider::cuboid(hx, hy)
394    }
395
396    #[test]
397    pub fn events_received() {
398        return main();
399
400        use bevy::prelude::*;
401
402        #[derive(Resource, Reflect)]
403        pub struct EventsSaver<E: Message> {
404            pub events: Vec<E>,
405        }
406        impl<E: Message> Default for EventsSaver<E> {
407            fn default() -> Self {
408                Self {
409                    events: Default::default(),
410                }
411            }
412        }
413        pub fn save_events<E: Message + Clone>(
414            mut events: MessageReader<E>,
415            mut saver: ResMut<EventsSaver<E>>,
416        ) {
417            for event in events.read() {
418                saver.events.push(event.clone());
419            }
420        }
421        fn run_test(app: &mut App) {
422            app.add_systems(PostUpdate, save_events::<CollisionEvent>)
423                .add_systems(PostUpdate, save_events::<ContactForceEvent>)
424                .init_resource::<EventsSaver<CollisionEvent>>()
425                .init_resource::<EventsSaver<ContactForceEvent>>();
426
427            // Simulates 60 updates per seconds
428            app.insert_resource(TimeUpdateStrategy::ManualDuration(
429                std::time::Duration::from_secs_f32(1f32 / 60f32),
430            ));
431            app.finish();
432            // 2 seconds should be plenty of time for the cube to fall on the
433            // lowest collider.
434            for _ in 0..120 {
435                app.update();
436            }
437            let saved_collisions = app
438                .world()
439                .get_resource::<EventsSaver<CollisionEvent>>()
440                .unwrap();
441            assert!(!saved_collisions.events.is_empty());
442            let saved_contact_forces = app
443                .world()
444                .get_resource::<EventsSaver<CollisionEvent>>()
445                .unwrap();
446            assert!(!saved_contact_forces.events.is_empty());
447        }
448
449        /// Adapted from events example
450        fn main() {
451            let mut app = App::new();
452            app.add_plugins((
453                TransformPlugin,
454                TimePlugin,
455                RapierPhysicsPlugin::<NoUserData>::default(),
456            ))
457            .add_systems(Startup, setup_physics);
458            run_test(&mut app);
459        }
460
461        pub fn setup_physics(mut commands: Commands) {
462            /*
463             * Ground
464             */
465            commands.spawn((Transform::from_xyz(0.0, -1.2, 0.0), cuboid(4.0, 1.0, 1.0)));
466
467            commands.spawn((
468                Transform::from_xyz(0.0, 5.0, 0.0),
469                cuboid(4.0, 1.5, 1.0),
470                Sensor,
471            ));
472
473            commands.spawn((
474                Transform::from_xyz(0.0, 13.0, 0.0),
475                RigidBody::Dynamic,
476                cuboid(0.5, 0.5, 0.5),
477                ActiveEvents::COLLISION_EVENTS,
478                ContactForceEventThreshold(30.0),
479            ));
480        }
481    }
482
483    fn falling_box_app() -> App {
484        let mut app = App::new();
485        app.add_plugins((
486            TransformPlugin,
487            TimePlugin,
488            RapierPhysicsPlugin::<NoUserData>::default(),
489        ))
490        .insert_resource(TimeUpdateStrategy::ManualDuration(
491            std::time::Duration::from_secs_f32(1f32 / 60f32),
492        ))
493        .add_systems(Startup, |mut commands: Commands| {
494            commands.spawn((Transform::from_xyz(0.0, -1.0, 0.0), cuboid(4.0, 1.0, 4.0)));
495            commands.spawn((
496                Transform::from_xyz(0.0, 0.6, 0.0),
497                RigidBody::Dynamic,
498                cuboid(0.5, 0.5, 0.5),
499                ActiveEvents::COLLISION_EVENTS | ActiveEvents::CONTACT_FORCE_EVENTS,
500                ContactForceEventThreshold(0.0),
501            ));
502        });
503        app
504    }
505
506    #[test]
507    pub fn contact_force_event_started_flag() {
508        use bevy::prelude::*;
509
510        #[derive(Resource, Default)]
511        struct Saved(Vec<ContactForceEvent>);
512
513        let mut app = falling_box_app();
514        app.init_resource::<Saved>().add_systems(
515            PostUpdate,
516            |mut events: MessageReader<ContactForceEvent>, mut saved: ResMut<Saved>| {
517                saved.0.extend(events.read().copied());
518            },
519        );
520        app.finish();
521        for _ in 0..120 {
522            app.update();
523        }
524
525        let events = &app.world().resource::<Saved>().0;
526        assert!(events.len() > 2);
527        // The first event is emitted when the force first crosses the threshold.
528        assert!(events[0].started);
529        // The box then rests on the ground, keeping the force above the threshold.
530        assert!(!events.last().unwrap().started);
531        assert!(events.iter().filter(|e| e.started).count() < events.len());
532    }
533
534    #[test]
535    pub fn custom_event_handler_receives_events() {
536        use bevy::prelude::*;
537        use rapier::dynamics::{RigidBodySet, SoftBodySet, SoftBodyTearEvent};
538        use rapier::geometry::{ColliderSet, CollisionEvent as RapierCollisionEvent, ContactPair};
539        use rapier::pipeline::EventHandler;
540        use std::sync::{
541            atomic::{AtomicUsize, Ordering},
542            Arc,
543        };
544
545        #[derive(Default, Clone)]
546        struct Counters {
547            collisions: Arc<AtomicUsize>,
548            collisions_with_pair: Arc<AtomicUsize>,
549            forces: Arc<AtomicUsize>,
550        }
551
552        impl EventHandler for Counters {
553            fn handle_collision_event(
554                &self,
555                _bodies: &RigidBodySet,
556                _colliders: &ColliderSet,
557                _event: RapierCollisionEvent,
558                contact_pair: Option<&ContactPair>,
559            ) {
560                self.collisions.fetch_add(1, Ordering::SeqCst);
561                if contact_pair.is_some() {
562                    self.collisions_with_pair.fetch_add(1, Ordering::SeqCst);
563                }
564            }
565
566            fn handle_contact_force_event(
567                &self,
568                _dt: crate::math::Real,
569                _bodies: &RigidBodySet,
570                _colliders: &ColliderSet,
571                _contact_pair: &ContactPair,
572                _total_force_magnitude: crate::math::Real,
573            ) {
574                self.forces.fetch_add(1, Ordering::SeqCst);
575            }
576
577            fn handle_soft_body_tear_event(
578                &self,
579                _soft_bodies: &SoftBodySet,
580                _event: &SoftBodyTearEvent,
581            ) {
582            }
583        }
584
585        #[derive(Resource, Default)]
586        struct BevyCounts {
587            collisions: usize,
588            forces: usize,
589        }
590
591        let counters = Counters::default();
592        let handler = counters.clone();
593        let mut app = falling_box_app();
594        app.init_resource::<BevyCounts>()
595            .add_systems(
596                Startup,
597                move |mut contexts: Query<&mut RapierContextSimulation>| {
598                    for mut context in contexts.iter_mut() {
599                        context.set_event_handler(handler.clone());
600                    }
601                },
602            )
603            .add_systems(
604                PostUpdate,
605                |mut collisions: MessageReader<CollisionEvent>,
606                 mut forces: MessageReader<ContactForceEvent>,
607                 mut counts: ResMut<BevyCounts>| {
608                    counts.collisions += collisions.read().count();
609                    counts.forces += forces.read().count();
610                },
611            );
612        app.finish();
613        for _ in 0..60 {
614            app.update();
615        }
616
617        let bevy_counts = app.world().resource::<BevyCounts>();
618        let collisions = counters.collisions.load(Ordering::SeqCst);
619        let forces = counters.forces.load(Ordering::SeqCst);
620        assert!(collisions > 0);
621        assert!(counters.collisions_with_pair.load(Ordering::SeqCst) > 0);
622        assert!(forces > 0);
623        // Bevy messages are still sent alongside the custom handler.
624        assert_eq!(bevy_counts.collisions, collisions);
625        assert_eq!(bevy_counts.forces, forces);
626    }
627
628    #[test]
629    pub fn spam_remove_rapier_entity_interpolated() {
630        let mut app = App::new();
631        app.add_plugins((
632            MinimalPlugins,
633            TransformPlugin,
634            RapierPhysicsPlugin::<NoUserData>::default(),
635        ))
636        .insert_resource(TimestepMode::Interpolated {
637            dt: 1.0 / 30.0,
638            time_scale: 1.0,
639            substeps: 2,
640        })
641        .add_systems(Startup, setup_physics)
642        .add_systems(Update, remove_collider);
643        // Simulates 60 updates per seconds
644        app.insert_resource(TimeUpdateStrategy::ManualDuration(
645            std::time::Duration::from_secs_f32(1f32 / 60f32),
646        ));
647
648        app.finish();
649
650        for _ in 0..100 {
651            app.update();
652        }
653        return;
654
655        #[derive(Component)]
656        pub struct ToRemove;
657
658        #[cfg(feature = "dim3")]
659        fn cuboid(hx: Real, hy: Real, hz: Real) -> Collider {
660            Collider::cuboid(hx, hy, hz)
661        }
662        #[cfg(feature = "dim2")]
663        fn cuboid(hx: Real, hy: Real, _hz: Real) -> Collider {
664            Collider::cuboid(hx, hy)
665        }
666        pub fn setup_physics(mut commands: Commands) {
667            for _i in 0..100 {
668                commands.spawn((
669                    Transform::from_xyz(0.0, 0.0, 0.0),
670                    RigidBody::Dynamic,
671                    cuboid(0.5, 0.5, 0.5),
672                    ActiveEvents::all(),
673                    ToRemove,
674                ));
675            }
676            /*
677             * Ground
678             */
679            let ground_size = 5.1;
680            let ground_height = 0.1;
681            let starting_y = -0.5 - ground_height;
682
683            commands.spawn((
684                Transform::from_xyz(0.0, starting_y, 0.0),
685                cuboid(ground_size, ground_height, ground_size),
686            ));
687        }
688
689        fn remove_collider(mut commands: Commands, query: Query<Entity, With<ToRemove>>) {
690            let Some(entity) = query.iter().next() else {
691                return;
692            };
693            commands.entity(entity).despawn();
694        }
695    }
696}