avian2d/collision/narrow_phase/
mod.rs

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