avian2d/collision/narrow_phase/
mod.rs

1//! Manages contacts and generates contact constraints.
2//!
3//! See [`NarrowPhasePlugin`].
4
5mod system_param;
6use system_param::ContactStatusBits;
7pub use system_param::NarrowPhase;
8#[cfg(feature = "parallel")]
9use system_param::ThreadLocalContactStatusBits;
10
11use core::marker::PhantomData;
12
13use crate::{
14    dynamics::solver::{
15        ContactConstraints,
16        constraint_graph::ConstraintGraph,
17        islands::{BodyIslandNode, PhysicsIslands},
18        joint_graph::JointGraph,
19    },
20    prelude::*,
21};
22use bevy::{
23    ecs::{
24        entity_disabling::Disabled,
25        intern::Interned,
26        schedule::ScheduleLabel,
27        system::{StaticSystemParam, SystemParam, SystemParamItem, SystemState},
28    },
29    prelude::*,
30};
31
32use super::{CollisionDiagnostics, contact_types::ContactEdgeFlags};
33
34/// Manages contacts and generates contact constraints.
35///
36/// # Overview
37///
38/// Before the narrow phase, the [broad phase](super::broad_phase) creates a contact pair
39/// in the [`ContactGraph`] resource for each pair of intersecting [`ColliderAabb`]s.
40///
41/// The narrow phase then determines which contact pairs found in the [`ContactGraph`] are touching,
42/// and computes updated contact points and normals in a parallel loop.
43///
44/// Afterwards, the narrow phase removes contact pairs whose AABBs no longer overlap,
45/// and writes collision events for colliders that started or stopped touching.
46/// This is done in a fast serial loop to preserve determinism.
47///
48/// Finally, a [`ContactConstraint`] is generated for each contact pair that is touching
49/// or expected to touch during the time step. These constraints are added to the [`ContactConstraints`]
50/// resource, and are later used by the [`SolverPlugin`] to solve contacts.
51///
52/// [`ContactConstraint`]: dynamics::solver::contact::ContactConstraint
53///
54/// # Collider Types
55///
56/// The plugin takes a collider type. This should be [`Collider`] for
57/// the vast majority of applications, but for custom collision backends
58/// you may use any collider that implements the [`AnyCollider`] trait.
59pub struct NarrowPhasePlugin<C: AnyCollider, H: CollisionHooks = ()> {
60    schedule: Interned<dyn ScheduleLabel>,
61    /// If `true`, the narrow phase will generate [`ContactConstraint`]s
62    /// and add them to the [`ContactConstraints`] resource.
63    ///
64    /// Contact constraints are used by the [`SolverPlugin`] for solving contacts.
65    ///
66    /// [`ContactConstraint`]: dynamics::solver::contact::ContactConstraint
67    generate_constraints: bool,
68    _phantom: PhantomData<(C, H)>,
69}
70
71impl<C: AnyCollider, H: CollisionHooks> NarrowPhasePlugin<C, H> {
72    /// Creates a [`NarrowPhasePlugin`] with the schedule used for running its systems
73    /// and whether it should generate [`ContactConstraint`]s for the [`ContactConstraints`] resource.
74    ///
75    /// Contact constraints are used by the [`SolverPlugin`] for solving contacts.
76    ///
77    /// The default schedule is [`PhysicsSchedule`].
78    ///
79    /// [`ContactConstraint`]: dynamics::solver::contact::ContactConstraint
80    pub fn new(schedule: impl ScheduleLabel, generate_constraints: bool) -> Self {
81        Self {
82            schedule: schedule.intern(),
83            generate_constraints,
84            _phantom: PhantomData,
85        }
86    }
87}
88
89impl<C: AnyCollider, H: CollisionHooks> Default for NarrowPhasePlugin<C, H> {
90    fn default() -> Self {
91        Self::new(PhysicsSchedule, true)
92    }
93}
94
95/// A resource that indicates that the narrow phase has been initialized.
96///
97/// This is used to ensure that some systems are only added once
98/// even with multiple collider types.
99#[derive(Resource, Default)]
100struct NarrowPhaseInitialized;
101
102impl<C: AnyCollider, H: CollisionHooks + 'static> Plugin for NarrowPhasePlugin<C, H>
103where
104    for<'w, 's> SystemParamItem<'w, 's, H>: CollisionHooks,
105{
106    fn build(&self, app: &mut App) {
107        let already_initialized = app.world().is_resource_added::<NarrowPhaseInitialized>();
108
109        app.init_resource::<NarrowPhaseConfig>()
110            .init_resource::<ContactGraph>()
111            .init_resource::<ContactStatusBits>()
112            .init_resource::<DefaultFriction>()
113            .init_resource::<DefaultRestitution>();
114
115        #[cfg(feature = "parallel")]
116        app.init_resource::<ThreadLocalContactStatusBits>();
117
118        app.add_message::<CollisionStart>()
119            .add_message::<CollisionEnd>();
120
121        if self.generate_constraints {
122            app.init_resource::<ContactConstraints>();
123        }
124
125        // Set up system set scheduling.
126        app.configure_sets(
127            self.schedule,
128            (
129                NarrowPhaseSystems::First,
130                NarrowPhaseSystems::Update,
131                NarrowPhaseSystems::Last,
132            )
133                .chain()
134                .in_set(PhysicsStepSystems::NarrowPhase),
135        );
136        app.configure_sets(
137            self.schedule,
138            CollisionEventSystems.in_set(PhysicsStepSystems::Finalize),
139        );
140
141        // Perform narrow phase collision detection.
142        app.add_systems(
143            self.schedule,
144            update_narrow_phase::<C, H>
145                .in_set(NarrowPhaseSystems::Update)
146                // Allowing ambiguities is required so that it's possible
147                // to have multiple collision backends at the same time.
148                .ambiguous_with_all(),
149        );
150
151        if !already_initialized {
152            // Remove collision pairs when colliders are disabled or removed.
153            app.add_observer(remove_collider_on::<Add, (Disabled, ColliderDisabled)>);
154            app.add_observer(remove_collider_on::<Remove, ColliderMarker>);
155
156            // Add colliders to the constraint graph when `Sensor` is removed,
157            // and remove them when `Sensor` is added.
158            // TODO: If we separate sensors from normal colliders, this won't be needed.
159            app.add_observer(on_add_sensor);
160            app.add_observer(on_remove_sensor);
161
162            // Add contacts to the constraint graph when a body is enabled,
163            // and remove them when a body is disabled.
164            app.add_observer(on_body_remove_rigid_body_disabled);
165            app.add_observer(on_disable_body);
166
167            // Remove contacts when the body body is disabled or `RigidBody` is replaced or removed.
168            app.add_observer(remove_body_on::<Insert, RigidBody>);
169            app.add_observer(remove_body_on::<Remove, RigidBody>);
170
171            // Trigger collision events for colliders that started or stopped touching.
172            app.add_systems(
173                self.schedule,
174                trigger_collision_events
175                    .in_set(CollisionEventSystems)
176                    // TODO: Ideally we don't need to make this ambiguous, but currently it is
177                    //       to avoid conflicts since the system has exclusive world access.
178                    .ambiguous_with(PhysicsStepSystems::Finalize),
179            );
180        }
181
182        app.init_resource::<NarrowPhaseInitialized>();
183    }
184
185    fn finish(&self, app: &mut App) {
186        // Register timer and counter diagnostics for collision detection.
187        app.register_physics_diagnostics::<CollisionDiagnostics>();
188    }
189}
190
191/// A system set for triggering the [`CollisionStart`] and [`CollisionEnd`] events.
192///
193/// Runs in [`PhysicsStepSystems::Finalize`], after the solver has run and contact impulses
194/// have been computed and applied.
195#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
196pub struct CollisionEventSystems;
197
198/// A resource for configuring the [narrow phase](NarrowPhasePlugin).
199#[derive(Resource, Reflect, Clone, Debug, PartialEq)]
200#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
201#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
202#[reflect(Debug, Resource, PartialEq)]
203pub struct NarrowPhaseConfig {
204    /// The default maximum [speculative margin](SpeculativeMargin) used for
205    /// [speculative collisions](dynamics::ccd#speculative-collision). This can be overridden
206    /// for individual entities with the [`SpeculativeMargin`] component.
207    ///
208    /// By default, the maximum speculative margin is unbounded, so contacts can be predicted
209    /// from any distance, provided that the bodies are moving fast enough. As the prediction distance
210    /// grows, the contact data becomes more and more approximate, and in rare cases, it can even cause
211    /// [issues](dynamics::ccd#caveats-of-speculative-collision) such as ghost collisions.
212    ///
213    /// By limiting the maximum speculative margin, these issues can be mitigated, at the cost
214    /// of an increased risk of tunneling. Setting it to `0.0` disables speculative collision
215    /// altogether for entities without [`SpeculativeMargin`].
216    ///
217    /// This is implicitly scaled by the [`PhysicsLengthUnit`].
218    ///
219    /// Default: `MAX` (unbounded)
220    pub default_speculative_margin: Scalar,
221
222    /// A contact tolerance that acts as a minimum bound for the [speculative margin](dynamics::ccd#speculative-collision).
223    ///
224    /// A small, positive contact tolerance helps ensure that contacts are not missed
225    /// due to numerical issues or solver jitter for objects that are in continuous
226    /// contact, such as pushing against each other.
227    ///
228    /// Making the contact tolerance too large will have a negative impact on performance,
229    /// as contacts will be computed even for objects that are not in close proximity.
230    ///
231    /// This is implicitly scaled by the [`PhysicsLengthUnit`].
232    ///
233    /// Default: `0.005`
234    pub contact_tolerance: Scalar,
235
236    /// If `true`, the current contacts will be matched with the previous contacts
237    /// based on feature IDs or contact positions, and the contact impulses from
238    /// the previous frame will be copied over for the new contacts.
239    ///
240    /// Using these impulses as the initial guess is referred to as *warm starting*,
241    /// and it can help the contact solver resolve overlap and stabilize much faster.
242    ///
243    /// Default: `true`
244    pub match_contacts: bool,
245}
246
247impl Default for NarrowPhaseConfig {
248    fn default() -> Self {
249        Self {
250            default_speculative_margin: Scalar::MAX,
251            contact_tolerance: 0.005,
252            match_contacts: true,
253        }
254    }
255}
256
257/// System sets for systems running in [`PhysicsStepSystems::NarrowPhase`].
258#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
259pub enum NarrowPhaseSystems {
260    /// Runs at the start of the narrow phase. Empty by default.
261    First,
262    /// Updates contacts in the [`ContactGraph`] and processes contact state changes.
263    Update,
264    /// Runs at the end of the narrow phase. Empty by default.
265    Last,
266}
267
268/// A deprecated alias for [`NarrowPhaseSystems`].
269#[deprecated(since = "0.4.0", note = "Renamed to `NarrowPhaseSystems`")]
270pub type NarrowPhaseSet = NarrowPhaseSystems;
271
272fn update_narrow_phase<C: AnyCollider, H: CollisionHooks + 'static>(
273    mut narrow_phase: NarrowPhase<C>,
274    mut collision_started_writer: MessageWriter<CollisionStart>,
275    mut collision_ended_writer: MessageWriter<CollisionEnd>,
276    time: Res<Time>,
277    hooks: StaticSystemParam<H>,
278    context: StaticSystemParam<C::Context>,
279    mut commands: ParallelCommands,
280    mut diagnostics: ResMut<CollisionDiagnostics>,
281) where
282    for<'w, 's> SystemParamItem<'w, 's, H>: CollisionHooks,
283{
284    let start = crate::utils::Instant::now();
285
286    narrow_phase.update::<H>(
287        &mut collision_started_writer,
288        &mut collision_ended_writer,
289        time.delta_seconds_adjusted(),
290        &hooks,
291        &context,
292        &mut commands,
293    );
294
295    diagnostics.narrow_phase = start.elapsed();
296    diagnostics.contact_count = narrow_phase.contact_graph.edges.edge_count() as u32;
297}
298
299#[derive(SystemParam)]
300struct TriggerCollisionEventsContext<'w, 's> {
301    query: Query<'w, 's, Has<CollisionEventsEnabled>>,
302    started: MessageReader<'w, 's, CollisionStart>,
303    ended: MessageReader<'w, 's, CollisionEnd>,
304}
305
306/// Triggers [`CollisionStart`] and [`CollisionEnd`] events for colliders
307/// that started or stopped touching and have the [`CollisionEventsEnabled`] component.
308fn trigger_collision_events(
309    // We use exclusive access here to avoid queuing a new command for each event.
310    world: &mut World,
311    state: &mut SystemState<TriggerCollisionEventsContext>,
312    // Cache pairs in buffers to avoid reallocating every time.
313    mut started: Local<Vec<CollisionStart>>,
314    mut ended: Local<Vec<CollisionEnd>>,
315) {
316    let mut state = state.get_mut(world);
317
318    // Collect `CollisionStart` events.
319    for event in state.started.read() {
320        let Ok([events_enabled1, events_enabled2]) =
321            state.query.get_many([event.collider1, event.collider2])
322        else {
323            continue;
324        };
325
326        if events_enabled1 {
327            started.push(CollisionStart {
328                collider1: event.collider1,
329                collider2: event.collider2,
330                body1: event.body1,
331                body2: event.body2,
332            });
333        }
334        if events_enabled2 {
335            started.push(CollisionStart {
336                collider1: event.collider2,
337                collider2: event.collider1,
338                body1: event.body2,
339                body2: event.body1,
340            });
341        }
342    }
343
344    // Collect `CollisionEnd` events.
345    for event in state.ended.read() {
346        let Ok([events_enabled1, events_enabled2]) =
347            state.query.get_many([event.collider1, event.collider2])
348        else {
349            continue;
350        };
351
352        if events_enabled1 {
353            ended.push(CollisionEnd {
354                collider1: event.collider1,
355                collider2: event.collider2,
356                body1: event.body1,
357                body2: event.body2,
358            });
359        }
360        if events_enabled2 {
361            ended.push(CollisionEnd {
362                collider1: event.collider2,
363                collider2: event.collider1,
364                body1: event.body2,
365                body2: event.body1,
366            });
367        }
368    }
369
370    // Trigger the events, draining the buffers in the process.
371    started.drain(..).for_each(|event| {
372        world.trigger(event);
373    });
374    ended.drain(..).for_each(|event| {
375        world.trigger(event);
376    });
377}
378
379// ===============================================================
380// The rest of this module contains observers and helper functions
381// for updating the contact graph, constraint graph, and islands
382// when bodies or colliders are added/removed or enabled/disabled.
383// ===============================================================
384
385// Cases to consider:
386// - Collider is removed -> remove all contacts
387// - Collider is disabled -> remove all contacts
388// - Collider becomes a sensor -> remove all touching contacts from constraint graph and islands
389// - Collider stops being a sensor -> add all touching contacts to constraint graph and islands
390// - Body is removed -> remove all touching contacts from constraint graph and islands
391// - Body is disabled -> remove all touching contacts from constraint graph and islands
392// - Body is enabled -> add all touching contacts to constraint graph and islands
393// - Body becomes static -> remove all static-static contacts
394
395/// Removes a collider from the [`ContactGraph`].
396///
397/// Also removes the collider from the [`CollidingEntities`] of the other entity,
398/// wakes up the other body, and writes a [`CollisionEnd`] event.
399fn remove_collider(
400    entity: Entity,
401    contact_graph: &mut ContactGraph,
402    joint_graph: &JointGraph,
403    constraint_graph: &mut ConstraintGraph,
404    mut islands: Option<&mut PhysicsIslands>,
405    body_islands: &mut Query<&mut BodyIslandNode, Or<(With<Disabled>, Without<Disabled>)>>,
406    colliding_entities_query: &mut Query<
407        &mut CollidingEntities,
408        Or<(With<Disabled>, Without<Disabled>)>,
409    >,
410    message_writer: &mut MessageWriter<CollisionEnd>,
411) {
412    // TODO: Wake up the island of the other bodies.
413    contact_graph.remove_collider_with(entity, |contact_graph, contact_id| {
414        // Get the contact edge.
415        let contact_edge = contact_graph.edge_weight(contact_id.into()).unwrap();
416
417        // If the contact pair was not touching, we don't need to do anything.
418        if !contact_edge.flags.contains(ContactEdgeFlags::TOUCHING) {
419            return;
420        }
421
422        // Send a collision ended event.
423        if contact_edge
424            .flags
425            .contains(ContactEdgeFlags::CONTACT_EVENTS)
426        {
427            message_writer.write(CollisionEnd {
428                collider1: contact_edge.collider1,
429                collider2: contact_edge.collider2,
430                body1: contact_edge.body1,
431                body2: contact_edge.body2,
432            });
433        }
434
435        // Remove the entity from the `CollidingEntities` of the other entity.
436        let other_entity = if contact_edge.collider1 == entity {
437            contact_edge.collider2
438        } else {
439            contact_edge.collider1
440        };
441        if let Ok(mut colliding_entities) = colliding_entities_query.get_mut(other_entity) {
442            colliding_entities.remove(&entity);
443        }
444
445        let has_island = contact_edge.island.is_some();
446
447        // Remove the contact edge from the constraint graph.
448        if let (Some(body1), Some(body2)) = (contact_edge.body1, contact_edge.body2) {
449            for _ in 0..contact_edge.constraint_handles.len() {
450                constraint_graph.pop_manifold(contact_graph, contact_id, body1, body2);
451            }
452        }
453
454        // Unlink the contact pair from its island.
455        if has_island && let Some(ref mut islands) = islands {
456            islands.remove_contact(contact_id, body_islands, contact_graph, joint_graph);
457        }
458    });
459}
460
461/// Removes contacts from the [`ConstraintGraph`], [`ContactGraph`], and [`PhysicsIslands`]
462/// when both bodies in a contact pair become static.
463fn remove_body_on<E: EntityEvent, B: Bundle>(
464    trigger: On<E, B>,
465    body_collider_query: Query<&RigidBodyColliders>,
466    mut colliding_entities_query: Query<
467        &mut CollidingEntities,
468        Or<(With<Disabled>, Without<Disabled>)>,
469    >,
470    mut message_writer: MessageWriter<CollisionEnd>,
471    mut body_islands: Query<&mut BodyIslandNode, Or<(With<Disabled>, Without<Disabled>)>>,
472    mut islands: Option<ResMut<PhysicsIslands>>,
473    mut constraint_graph: ResMut<ConstraintGraph>,
474    mut contact_graph: ResMut<ContactGraph>,
475    joint_graph: ResMut<JointGraph>,
476    mut commands: Commands,
477) {
478    let Ok(colliders) = body_collider_query.get(trigger.event_target()) else {
479        return;
480    };
481
482    // Wake up the body's island.
483    if let Ok(body_island) = body_islands.get_mut(trigger.event_target()) {
484        commands.queue(WakeIslands(vec![body_island.island_id]));
485    }
486
487    // TODO: Only remove static-static contacts and unlink from islands.
488    for collider in colliders {
489        remove_collider(
490            collider,
491            &mut contact_graph,
492            &joint_graph,
493            &mut constraint_graph,
494            islands.as_deref_mut(),
495            &mut body_islands,
496            &mut colliding_entities_query,
497            &mut message_writer,
498        );
499    }
500}
501
502/// Removes colliders from the [`ContactGraph`] when the given trigger is activated.
503///
504/// Also removes the collider from the [`CollidingEntities`] of the other entity,
505/// wakes up the other body, and writes a [`CollisionEnd`] event.
506fn remove_collider_on<E: EntityEvent, B: Bundle>(
507    trigger: On<E, B>,
508    mut contact_graph: ResMut<ContactGraph>,
509    joint_graph: ResMut<JointGraph>,
510    mut constraint_graph: ResMut<ConstraintGraph>,
511    mut islands: Option<ResMut<PhysicsIslands>>,
512    mut body_islands: Query<&mut BodyIslandNode, Or<(With<Disabled>, Without<Disabled>)>>,
513    // TODO: Change this hack to include disabled entities with `Allows<T>` for 0.17
514    mut query: Query<&mut CollidingEntities, Or<(With<Disabled>, Without<Disabled>)>>,
515    collider_of: Query<&ColliderOf, Or<(With<Disabled>, Without<Disabled>)>>,
516    mut message_writer: MessageWriter<CollisionEnd>,
517    mut commands: Commands,
518) {
519    let entity = trigger.event_target();
520
521    let body1 = collider_of
522        .get(entity)
523        .map(|&ColliderOf { body }| body)
524        .ok();
525
526    // If the collider was attached to a rigid body, wake its island.
527    if let Some(body) = body1
528        && let Ok(body_island) = body_islands.get_mut(body)
529    {
530        commands.queue(WakeIslands(vec![body_island.island_id]));
531    }
532
533    // Remove the collider from the contact graph.
534    remove_collider(
535        entity,
536        &mut contact_graph,
537        &joint_graph,
538        &mut constraint_graph,
539        islands.as_deref_mut(),
540        &mut body_islands,
541        &mut query,
542        &mut message_writer,
543    );
544}
545
546/// Adds the touching contacts of a body to the [`ConstraintGraph`] and [`PhysicsIslands`]
547/// when the body is enabled by removing [`RigidBodyDisabled`].
548fn on_body_remove_rigid_body_disabled(
549    trigger: On<Add, BodyIslandNode>,
550    body_collider_query: Query<&RigidBodyColliders>,
551    mut constraint_graph: ResMut<ConstraintGraph>,
552    mut contact_graph: ResMut<ContactGraph>,
553    joint_graph: ResMut<JointGraph>,
554    mut islands: Option<ResMut<PhysicsIslands>>,
555    mut body_islands: Query<&mut BodyIslandNode, Or<(With<Disabled>, Without<Disabled>)>>,
556    mut colliding_entities_query: Query<
557        &mut CollidingEntities,
558        Or<(With<Disabled>, Without<Disabled>)>,
559    >,
560    mut message_writer: MessageWriter<CollisionEnd>,
561) {
562    let Ok(colliders) = body_collider_query.get(trigger.entity) else {
563        return;
564    };
565
566    for collider in colliders {
567        remove_collider(
568            collider,
569            &mut contact_graph,
570            &joint_graph,
571            &mut constraint_graph,
572            islands.as_deref_mut(),
573            &mut body_islands,
574            &mut colliding_entities_query,
575            &mut message_writer,
576        );
577    }
578}
579
580/// Removes the touching contacts of a body from the [`ConstraintGraph`] and [`PhysicsIslands`]
581/// when the body is disabled with [`Disabled`] or [`RigidBodyDisabled`].
582fn on_disable_body(
583    trigger: On<Add, (Disabled, RigidBodyDisabled)>,
584    body_collider_query: Query<&RigidBodyColliders, Or<(With<Disabled>, Without<Disabled>)>>,
585    mut constraint_graph: ResMut<ConstraintGraph>,
586    mut contact_graph: ResMut<ContactGraph>,
587    joint_graph: Res<JointGraph>,
588    mut islands: Option<ResMut<PhysicsIslands>>,
589    mut body_islands: Query<&mut BodyIslandNode, Or<(With<Disabled>, Without<Disabled>)>>,
590    mut colliding_entities_query: Query<
591        &mut CollidingEntities,
592        Or<(With<Disabled>, Without<Disabled>)>,
593    >,
594    mut message_writer: MessageWriter<CollisionEnd>,
595) {
596    let Ok(colliders) = body_collider_query.get(trigger.entity) else {
597        return;
598    };
599
600    for collider in colliders {
601        remove_collider(
602            collider,
603            &mut contact_graph,
604            &joint_graph,
605            &mut constraint_graph,
606            islands.as_deref_mut(),
607            &mut body_islands,
608            &mut colliding_entities_query,
609            &mut message_writer,
610        );
611    }
612}
613
614// TODO: These are currently used just for sensors. It wouldn't be needed if sensor logic
615//       was separate from normal colliders and didn't compute contact manifolds.
616
617/// Removes the touching contacts of a collider from the [`ConstraintGraph`] and [`PhysicsIslands`]
618/// when a collider becomes a [`Sensor`].
619fn on_add_sensor(
620    trigger: On<Add, Sensor>,
621    mut constraint_graph: ResMut<ConstraintGraph>,
622    mut contact_graph: ResMut<ContactGraph>,
623    joint_graph: Res<JointGraph>,
624    mut islands: Option<ResMut<PhysicsIslands>>,
625    mut body_islands: Query<&mut BodyIslandNode, Or<(With<Disabled>, Without<Disabled>)>>,
626    mut colliding_entities_query: Query<
627        &mut CollidingEntities,
628        Or<(With<Disabled>, Without<Disabled>)>,
629    >,
630    mut message_writer: MessageWriter<CollisionEnd>,
631) {
632    remove_collider(
633        trigger.entity,
634        &mut contact_graph,
635        &joint_graph,
636        &mut constraint_graph,
637        islands.as_deref_mut(),
638        &mut body_islands,
639        &mut colliding_entities_query,
640        &mut message_writer,
641    );
642}
643
644/// Adds the touching contacts of a collider to the [`ConstraintGraph`] and [`PhysicsIslands`]
645/// when a collider stops being a [`Sensor`].
646fn on_remove_sensor(
647    trigger: On<Remove, Sensor>,
648    mut constraint_graph: ResMut<ConstraintGraph>,
649    mut contact_graph: ResMut<ContactGraph>,
650    joint_graph: ResMut<JointGraph>,
651    mut islands: Option<ResMut<PhysicsIslands>>,
652    mut body_islands: Query<&mut BodyIslandNode, Or<(With<Disabled>, Without<Disabled>)>>,
653    mut colliding_entities_query: Query<
654        &mut CollidingEntities,
655        Or<(With<Disabled>, Without<Disabled>)>,
656    >,
657    mut message_writer: MessageWriter<CollisionEnd>,
658) {
659    remove_collider(
660        trigger.entity,
661        &mut contact_graph,
662        &joint_graph,
663        &mut constraint_graph,
664        islands.as_deref_mut(),
665        &mut body_islands,
666        &mut colliding_entities_query,
667        &mut message_writer,
668    );
669}