avian2d/collision/collider/
backend.rs

1//! Handles generic collider backend logic, like initializing colliders and AABBs and updating related components.
2//!
3//! See [`ColliderBackendPlugin`].
4
5use core::marker::PhantomData;
6
7#[cfg(all(feature = "collider-from-mesh", feature = "default-collider"))]
8use crate::collision::collider::cache::ColliderCache;
9use crate::{
10    collision::broad_phase::BroadPhaseSystems,
11    physics_transform::{PhysicsTransformConfig, PhysicsTransformSystems, init_physics_transform},
12    prelude::*,
13};
14#[cfg(all(feature = "bevy_scene", feature = "default-collider"))]
15use bevy::scene::SceneInstance;
16use bevy::{
17    ecs::{intern::Interned, schedule::ScheduleLabel, system::StaticSystemParam},
18    prelude::*,
19};
20use mass_properties::{MassPropertySystems, components::RecomputeMassProperties};
21
22/// A plugin for handling generic collider backend logic.
23///
24/// - Initializes colliders, handles [`ColliderConstructor`] and [`ColliderConstructorHierarchy`].
25/// - Updates [`ColliderAabb`]s.
26/// - Updates collider scale based on `Transform` scale.
27/// - Updates [`ColliderMassProperties`].
28///
29/// This plugin should typically be used together with the [`ColliderHierarchyPlugin`].
30///
31/// # Custom Collision Backends
32///
33/// By default, [`PhysicsPlugins`] adds this plugin for the [`Collider`] component.
34/// You can also create custom collider backends by implementing the [`AnyCollider`]
35/// and [`ScalableCollider`] traits for a type.
36///
37/// To use a custom collider backend, simply add the [`ColliderBackendPlugin`] with your collider type:
38///
39/// ```no_run
40#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
41#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
42/// use bevy::prelude::*;
43/// #
44/// # type MyCollider = Collider;
45///
46/// fn main() {
47///     App::new()
48///         .add_plugins((
49///             DefaultPlugins,
50///             PhysicsPlugins::default(),
51///             // MyCollider must implement AnyCollider and ScalableCollider.
52///             ColliderBackendPlugin::<MyCollider>::default(),
53///             // To enable collision detection for the collider,
54///             // we also need to add the NarrowPhasePlugin for it.
55///             NarrowPhasePlugin::<MyCollider>::default(),
56///         ))
57///         // ...your other plugins, systems and resources
58///         .run();
59/// }
60/// ```
61///
62/// Assuming you have implemented the required traits correctly,
63/// it should now work with the rest of the engine just like normal [`Collider`]s!
64///
65/// **Note**: [Spatial queries](spatial_query) are not supported for custom colliders yet.
66pub struct ColliderBackendPlugin<C: ScalableCollider> {
67    schedule: Interned<dyn ScheduleLabel>,
68    _phantom: PhantomData<C>,
69}
70
71impl<C: ScalableCollider> ColliderBackendPlugin<C> {
72    /// Creates a [`ColliderBackendPlugin`] with the schedule that is used for running the [`PhysicsSchedule`].
73    ///
74    /// The default schedule is `FixedPostUpdate`.
75    pub fn new(schedule: impl ScheduleLabel) -> Self {
76        Self {
77            schedule: schedule.intern(),
78            _phantom: PhantomData,
79        }
80    }
81}
82
83impl<C: ScalableCollider> Default for ColliderBackendPlugin<C> {
84    fn default() -> Self {
85        Self {
86            schedule: FixedPostUpdate.intern(),
87            _phantom: PhantomData,
88        }
89    }
90}
91
92impl<C: ScalableCollider> Plugin for ColliderBackendPlugin<C> {
93    fn build(&self, app: &mut App) {
94        // Register required components for the collider type.
95        let _ = app.try_register_required_components_with::<C, Position>(|| Position::PLACEHOLDER);
96        let _ = app.try_register_required_components_with::<C, Rotation>(|| Rotation::PLACEHOLDER);
97        let _ = app.try_register_required_components::<C, ColliderMarker>();
98        let _ = app.try_register_required_components::<C, ColliderAabb>();
99        let _ = app.try_register_required_components::<C, CollisionLayers>();
100        let _ = app.try_register_required_components::<C, ColliderDensity>();
101        let _ = app.try_register_required_components::<C, ColliderMassProperties>();
102
103        // Make sure the necessary resources are initialized.
104        app.init_resource::<PhysicsTransformConfig>();
105        app.init_resource::<NarrowPhaseConfig>();
106        app.init_resource::<PhysicsLengthUnit>();
107
108        let hooks = app.world_mut().register_component_hooks::<C>();
109
110        // Initialize missing components for colliders.
111        hooks.on_add(|mut world, ctx| {
112            // Initialize the global physics transform for the collider.
113            init_physics_transform(&mut world, &ctx);
114
115            let scale = world
116                .entity(ctx.entity)
117                .get::<GlobalTransform>()
118                .map(|gt| gt.scale())
119                .unwrap_or_default();
120            #[cfg(feature = "2d")]
121            let scale = scale.xy();
122
123            let mut entity_mut = world.entity_mut(ctx.entity);
124
125            // Make sure the collider is initialized with the correct scale.
126            // This overwrites the scale set by the constructor, but that one is
127            // meant to be only changed after initialization.
128            entity_mut
129                .get_mut::<C>()
130                .unwrap()
131                .set_scale(scale.adjust_precision(), 10);
132
133            let collider = entity_mut.get::<C>().unwrap();
134
135            let density = entity_mut
136                .get::<ColliderDensity>()
137                .copied()
138                .unwrap_or_default();
139
140            let mass_properties = if entity_mut.get::<Sensor>().is_some() {
141                MassProperties::ZERO
142            } else {
143                collider.mass_properties(density.0)
144            };
145
146            if let Some(mut collider_mass_properties) =
147                entity_mut.get_mut::<ColliderMassProperties>()
148            {
149                *collider_mass_properties = ColliderMassProperties::from(mass_properties);
150            }
151        });
152
153        // Register a component hook that removes `ColliderMarker` components
154        // and updates rigid bodies when their collider is removed.
155        app.world_mut()
156            .register_component_hooks::<C>()
157            .on_remove(|mut world, ctx| {
158                // Remove the `ColliderMarker` associated with the collider.
159                // TODO: If the same entity had multiple *different* types of colliders, this would
160                //       get removed even if just one collider was removed. This is a very niche edge case though.
161                world
162                    .commands()
163                    .entity(ctx.entity)
164                    .try_remove::<ColliderMarker>();
165
166                let entity_ref = world.entity_mut(ctx.entity);
167
168                // Get the rigid body entity that the collider is attached to.
169                let Some(ColliderOf { body }) = entity_ref.get::<ColliderOf>().copied() else {
170                    return;
171                };
172
173                // Queue the rigid body for a mass property update.
174                world
175                    .commands()
176                    .entity(body)
177                    .try_insert(RecomputeMassProperties);
178            });
179
180        // Initialize `ColliderAabb` for colliders.
181        app.add_observer(
182            |trigger: On<Add, C>,
183             mut query: Query<(
184                &C,
185                &Position,
186                &Rotation,
187                Option<&CollisionMargin>,
188                &mut ColliderAabb,
189            )>,
190             narrow_phase_config: Res<NarrowPhaseConfig>,
191             length_unit: Res<PhysicsLengthUnit>,
192             collider_context: StaticSystemParam<C::Context>| {
193                let contact_tolerance = length_unit.0 * narrow_phase_config.contact_tolerance;
194                let aabb_context = AabbContext::new(trigger.entity, &*collider_context);
195
196                if let Ok((collider, pos, rot, collision_margin, mut aabb)) =
197                    query.get_mut(trigger.entity)
198                {
199                    let collision_margin = collision_margin.map_or(0.0, |m| m.0);
200                    *aabb = collider
201                        .aabb_with_context(pos.0, *rot, aabb_context)
202                        .grow(Vector::splat(contact_tolerance + collision_margin));
203                }
204            },
205        );
206
207        // When the `Sensor` component is added to a collider, queue its rigid body for a mass property update.
208        app.add_observer(
209            |trigger: On<Add, Sensor>,
210             mut commands: Commands,
211             query: Query<(&ColliderMassProperties, &ColliderOf)>| {
212                if let Ok((collider_mass_properties, &ColliderOf { body })) =
213                    query.get(trigger.entity)
214                {
215                    // If the collider mass properties are zero, there is nothing to subtract.
216                    if *collider_mass_properties == ColliderMassProperties::ZERO {
217                        return;
218                    }
219
220                    // Queue the rigid body for a mass property update.
221                    if let Ok(mut entity_commands) = commands.get_entity(body) {
222                        entity_commands.insert(RecomputeMassProperties);
223                    }
224                }
225            },
226        );
227
228        // When the `Sensor` component is removed from a collider, update its mass properties.
229        app.add_observer(
230            |trigger: On<Remove, Sensor>,
231             mut collider_query: Query<(
232                Ref<C>,
233                &ColliderDensity,
234                &mut ColliderMassProperties,
235            )>| {
236                if let Ok((collider, density, mut collider_mass_properties)) =
237                    collider_query.get_mut(trigger.entity)
238                {
239                    // Update collider mass props.
240                    *collider_mass_properties =
241                        ColliderMassProperties::from(collider.mass_properties(density.0));
242                }
243            },
244        );
245
246        app.add_systems(
247            self.schedule,
248            (
249                update_collider_scale::<C>
250                    .in_set(PhysicsSystems::Prepare)
251                    .after(PhysicsTransformSystems::TransformToPosition),
252                update_collider_mass_properties::<C>
253                    .in_set(MassPropertySystems::UpdateColliderMassProperties),
254            )
255                .chain(),
256        );
257
258        let physics_schedule = app
259            .get_schedule_mut(PhysicsSchedule)
260            .expect("add PhysicsSchedule first");
261
262        // Allowing ambiguities is required so that it's possible
263        // to have multiple collision backends at the same time.
264        physics_schedule.add_systems(
265            update_aabb::<C>
266                .in_set(PhysicsStepSystems::BroadPhase)
267                .after(BroadPhaseSystems::First)
268                .before(BroadPhaseSystems::UpdateStructures)
269                .ambiguous_with_all(),
270        );
271
272        #[cfg(feature = "default-collider")]
273        app.add_systems(
274            Update,
275            (
276                init_collider_constructors,
277                init_collider_constructor_hierarchies,
278            ),
279        );
280    }
281}
282
283/// A marker component for colliders. Inserted and removed automatically.
284///
285/// This is useful for filtering collider entities regardless of the [collider backend](ColliderBackendPlugin).
286#[derive(Reflect, Component, Clone, Copy, Debug, Default)]
287#[reflect(Component, Debug, Default)]
288pub struct ColliderMarker;
289
290/// Generates [`Collider`]s based on [`ColliderConstructor`]s.
291///
292/// If a [`ColliderConstructor`] requires a mesh, the system keeps running
293/// until the mesh associated with the mesh handle is available.
294///
295/// # Panics
296///
297/// Panics if the [`ColliderConstructor`] requires a mesh but no mesh handle is found.
298#[cfg(feature = "default-collider")]
299fn init_collider_constructors(
300    mut commands: Commands,
301    #[cfg(feature = "collider-from-mesh")] meshes: Res<Assets<Mesh>>,
302    #[cfg(feature = "collider-from-mesh")] mesh_handles: Query<&Mesh3d>,
303    #[cfg(feature = "collider-from-mesh")] mut collider_cache: Option<ResMut<ColliderCache>>,
304    constructors: Query<(
305        Entity,
306        Option<&Collider>,
307        Option<&Name>,
308        &ColliderConstructor,
309    )>,
310) {
311    for (entity, existing_collider, name, constructor) in constructors.iter() {
312        let name = pretty_name(name, entity);
313        if existing_collider.is_some() {
314            warn!(
315                "Tried to add a collider to entity {name} via {constructor:#?}, \
316                but that entity already holds a collider. Skipping.",
317            );
318            commands.entity(entity).remove::<ColliderConstructor>();
319            continue;
320        }
321        #[cfg(feature = "collider-from-mesh")]
322        let collider = if constructor.requires_mesh() {
323            let mesh_handle = mesh_handles.get(entity).unwrap_or_else(|_| panic!(
324                "Tried to add a collider to entity {name} via {constructor:#?} that requires a mesh, \
325                but no mesh handle was found"));
326            let Some(mesh) = meshes.get(mesh_handle) else {
327                // Mesh required, but not loaded yet
328                continue;
329            };
330            collider_cache
331                .as_mut()
332                .map(|cache| cache.get_or_insert(mesh_handle, mesh, constructor.clone()))
333                .unwrap_or_else(|| Collider::try_from_constructor(constructor.clone(), Some(mesh)))
334        } else {
335            Collider::try_from_constructor(constructor.clone(), None)
336        };
337
338        #[cfg(not(feature = "collider-from-mesh"))]
339        let collider = Collider::try_from_constructor(constructor.clone());
340
341        if let Some(collider) = collider {
342            commands.entity(entity).insert(collider);
343            commands.trigger(ColliderConstructorReady { entity })
344        } else {
345            error!(
346                "Tried to add a collider to entity {name} via {constructor:#?}, \
347                but the collider could not be generated. Skipping.",
348            );
349        }
350        commands.entity(entity).remove::<ColliderConstructor>();
351    }
352}
353
354/// Generates [`Collider`]s for descendants of entities with the [`ColliderConstructorHierarchy`] component.
355///
356/// If an entity has a `SceneInstance`, its collider hierarchy is only generated once the scene is ready.
357#[cfg(feature = "default-collider")]
358fn init_collider_constructor_hierarchies(
359    mut commands: Commands,
360    #[cfg(feature = "collider-from-mesh")] meshes: Res<Assets<Mesh>>,
361    #[cfg(feature = "collider-from-mesh")] mesh_handles: Query<&Mesh3d>,
362    #[cfg(feature = "collider-from-mesh")] mut collider_cache: Option<ResMut<ColliderCache>>,
363    #[cfg(feature = "bevy_scene")] scene_spawner: Res<SceneSpawner>,
364    #[cfg(feature = "bevy_scene")] scenes: Query<&SceneRoot>,
365    #[cfg(feature = "bevy_scene")] scene_instances: Query<&SceneInstance>,
366    collider_constructors: Query<(Entity, &ColliderConstructorHierarchy)>,
367    children: Query<&Children>,
368    child_query: Query<(Option<&Name>, Option<&Collider>)>,
369) {
370    use super::ColliderConstructorHierarchyConfig;
371
372    for (scene_entity, collider_constructor_hierarchy) in collider_constructors.iter() {
373        #[cfg(feature = "bevy_scene")]
374        {
375            if scenes.contains(scene_entity) {
376                if let Ok(scene_instance) = scene_instances.get(scene_entity) {
377                    if !scene_spawner.instance_is_ready(**scene_instance) {
378                        // Wait for the scene to be ready
379                        continue;
380                    }
381                } else {
382                    // SceneInstance is added in the SpawnScene schedule, so it might not be available yet
383                    continue;
384                }
385            }
386        }
387
388        for child_entity in children.iter_descendants(scene_entity) {
389            let Ok((name, existing_collider)) = child_query.get(child_entity) else {
390                continue;
391            };
392
393            let pretty_name = pretty_name(name, child_entity);
394
395            let default_collider = || {
396                Some(ColliderConstructorHierarchyConfig {
397                    constructor: collider_constructor_hierarchy.default_constructor.clone(),
398                    ..default()
399                })
400            };
401
402            let collider_data = if let Some(name) = name {
403                collider_constructor_hierarchy
404                    .config
405                    .get(name.as_str())
406                    .cloned()
407                    .unwrap_or_else(default_collider)
408            } else if existing_collider.is_some() {
409                warn!(
410                    "Tried to add a collider to entity {pretty_name} via {collider_constructor_hierarchy:#?}, \
411                        but that entity already holds a collider. Skipping. \
412                        If this was intentional, add the name of the collider to overwrite to `ColliderConstructorHierarchy.config`."
413                );
414                continue;
415            } else {
416                default_collider()
417            };
418
419            // If the configuration is explicitly set to `None`, skip this entity.
420            let Some(collider_data) = collider_data else {
421                continue;
422            };
423
424            // Use the configured constructor if specified, otherwise use the default constructor.
425            // If both are `None`, skip this entity.
426            let Some(constructor) = collider_data
427                .constructor
428                .or_else(|| collider_constructor_hierarchy.default_constructor.clone())
429            else {
430                continue;
431            };
432
433            #[cfg(feature = "collider-from-mesh")]
434            let collider = if constructor.requires_mesh() {
435                let Ok(mesh_handle) = mesh_handles.get(child_entity) else {
436                    // This child entity does not have a mesh, so we skip it.
437                    continue;
438                };
439                let Some(mesh) = meshes.get(mesh_handle) else {
440                    // Mesh required, but not loaded yet
441                    continue;
442                };
443                collider_cache
444                    .as_mut()
445                    .map(|cache| cache.get_or_insert(mesh_handle, mesh, constructor.clone()))
446                    .unwrap_or_else(|| {
447                        Collider::try_from_constructor(constructor.clone(), Some(mesh))
448                    })
449            } else {
450                Collider::try_from_constructor(constructor.clone(), None)
451            };
452
453            #[cfg(not(feature = "collider-from-mesh"))]
454            let collider = Collider::try_from_constructor(constructor);
455
456            if let Some(collider) = collider {
457                commands.entity(child_entity).insert((
458                    collider,
459                    collider_data
460                        .layers
461                        .unwrap_or(collider_constructor_hierarchy.default_layers),
462                    collider_data
463                        .density
464                        .unwrap_or(collider_constructor_hierarchy.default_density),
465                ));
466            } else {
467                error!(
468                    "Tried to add a collider to entity {pretty_name} via {collider_constructor_hierarchy:#?}, \
469                        but the collider could not be generated. Skipping.",
470                );
471            }
472        }
473
474        commands
475            .entity(scene_entity)
476            .remove::<ColliderConstructorHierarchy>();
477
478        commands.trigger(ColliderConstructorHierarchyReady {
479            entity: scene_entity,
480        })
481    }
482}
483
484#[cfg(feature = "default-collider")]
485fn pretty_name(name: Option<&Name>, entity: Entity) -> String {
486    name.map(|n| n.to_string())
487        .unwrap_or_else(|| format!("<unnamed entity {}>", entity.index()))
488}
489
490/// Updates the Axis-Aligned Bounding Boxes of all colliders.
491#[allow(clippy::type_complexity)]
492fn update_aabb<C: AnyCollider>(
493    mut colliders: Query<
494        (
495            Entity,
496            &C,
497            &mut ColliderAabb,
498            &Position,
499            &Rotation,
500            Option<&ColliderOf>,
501            Option<&CollisionMargin>,
502            Option<&SpeculativeMargin>,
503            Has<SweptCcd>,
504            Option<&LinearVelocity>,
505            Option<&AngularVelocity>,
506        ),
507        Or<(
508            Changed<Position>,
509            Changed<Rotation>,
510            Changed<LinearVelocity>,
511            Changed<AngularVelocity>,
512            Changed<C>,
513        )>,
514    >,
515    rb_velocities: Query<
516        (
517            &Position,
518            &Rotation,
519            &ComputedCenterOfMass,
520            &LinearVelocity,
521            &AngularVelocity,
522        ),
523        With<Children>,
524    >,
525    narrow_phase_config: Res<NarrowPhaseConfig>,
526    length_unit: Res<PhysicsLengthUnit>,
527    time: Res<Time>,
528    collider_context: StaticSystemParam<C::Context>,
529) {
530    let delta_secs = time.delta_seconds_adjusted();
531    let default_speculative_margin = length_unit.0 * narrow_phase_config.default_speculative_margin;
532    let contact_tolerance = length_unit.0 * narrow_phase_config.contact_tolerance;
533
534    for (
535        entity,
536        collider,
537        mut aabb,
538        pos,
539        rot,
540        collider_of,
541        collision_margin,
542        speculative_margin,
543        has_swept_ccd,
544        lin_vel,
545        ang_vel,
546    ) in &mut colliders
547    {
548        let collision_margin = collision_margin.map_or(0.0, |margin| margin.0);
549        let speculative_margin = if has_swept_ccd {
550            Scalar::MAX
551        } else {
552            speculative_margin.map_or(default_speculative_margin, |margin| margin.0)
553        };
554
555        let context = AabbContext::new(entity, &*collider_context);
556
557        if speculative_margin <= 0.0 {
558            *aabb = collider
559                .aabb_with_context(pos.0, *rot, context)
560                .grow(Vector::splat(contact_tolerance + collision_margin));
561            continue;
562        }
563
564        // Expand the AABB based on the body's velocity and CCD speculative margin.
565        let (lin_vel, ang_vel) = if let (Some(lin_vel), Some(ang_vel)) = (lin_vel, ang_vel) {
566            (*lin_vel, *ang_vel)
567        } else if let Some(Ok((rb_pos, rb_rot, center_of_mass, lin_vel, ang_vel))) =
568            collider_of.map(|&ColliderOf { body }| rb_velocities.get(body))
569        {
570            // If the rigid body is rotating, off-center colliders will orbit around it,
571            // which affects their linear velocities. We need to compute the linear velocity
572            // at the offset position.
573            // TODO: This assumes that the colliders would continue moving in the same direction,
574            //       but because they are orbiting, the direction will change. We should take
575            //       into account the uniform circular motion.
576            let offset = pos.0 - rb_pos.0 - rb_rot * center_of_mass.0;
577            #[cfg(feature = "2d")]
578            let vel_at_offset =
579                lin_vel.0 + Vector::new(-ang_vel.0 * offset.y, ang_vel.0 * offset.x) * 1.0;
580            #[cfg(feature = "3d")]
581            let vel_at_offset = lin_vel.0 + ang_vel.cross(offset);
582            (LinearVelocity(vel_at_offset), *ang_vel)
583        } else {
584            (LinearVelocity::ZERO, AngularVelocity::ZERO)
585        };
586
587        // Current position and predicted position for next feame
588        let (start_pos, start_rot) = (*pos, *rot);
589        let (end_pos, end_rot) = {
590            #[cfg(feature = "2d")]
591            {
592                (
593                    pos.0
594                        + (lin_vel.0 * delta_secs)
595                            .clamp_length_max(speculative_margin.max(contact_tolerance)),
596                    *rot * Rotation::radians(ang_vel.0 * delta_secs),
597                )
598            }
599            #[cfg(feature = "3d")]
600            {
601                let end_rot =
602                    Rotation(Quaternion::from_scaled_axis(ang_vel.0 * delta_secs) * rot.0)
603                        .fast_renormalize();
604                (
605                    pos.0
606                        + (lin_vel.0 * delta_secs)
607                            .clamp_length_max(speculative_margin.max(contact_tolerance)),
608                    end_rot,
609                )
610            }
611        };
612        // Compute swept AABB, the space that the body would occupy if it was integrated for one frame
613        // TODO: Should we expand the AABB in all directions for speculative contacts?
614        *aabb = collider
615            .swept_aabb_with_context(start_pos.0, start_rot, end_pos, end_rot, context)
616            .grow(Vector::splat(contact_tolerance + collision_margin));
617    }
618}
619
620/// Updates the scale of colliders based on [`Transform`] scale.
621#[allow(clippy::type_complexity)]
622pub fn update_collider_scale<C: ScalableCollider>(
623    mut colliders: ParamSet<(
624        // Root bodies
625        Query<(&Transform, &mut C), (Without<ChildOf>, Or<(Changed<Transform>, Changed<C>)>)>,
626        // Child colliders
627        Query<
628            (&ColliderTransform, &mut C),
629            (With<ChildOf>, Or<(Changed<ColliderTransform>, Changed<C>)>),
630        >,
631    )>,
632    config: Res<PhysicsTransformConfig>,
633) {
634    if config.transform_to_collider_scale {
635        // Update collider scale for root bodies
636        for (transform, mut collider) in &mut colliders.p0() {
637            #[cfg(feature = "2d")]
638            let scale = transform.scale.truncate().adjust_precision();
639            #[cfg(feature = "3d")]
640            let scale = transform.scale.adjust_precision();
641            if scale != collider.scale() {
642                // TODO: Support configurable subdivision count for shapes that
643                //       can't be represented without approximations after scaling.
644                collider.set_scale(scale, 10);
645            }
646        }
647    }
648    // Update collider scale for child colliders
649    for (collider_transform, mut collider) in &mut colliders.p1() {
650        if collider_transform.scale != collider.scale() {
651            // TODO: Support configurable subdivision count for shapes that
652            //       can't be represented without approximations after scaling.
653            collider.set_scale(collider_transform.scale, 10);
654        }
655    }
656}
657
658/// Updates the mass properties of [`Collider`].
659#[allow(clippy::type_complexity)]
660pub(crate) fn update_collider_mass_properties<C: AnyCollider>(
661    mut query: Query<
662        (Ref<C>, &ColliderDensity, &mut ColliderMassProperties),
663        (Or<(Changed<C>, Changed<ColliderDensity>)>, Without<Sensor>),
664    >,
665) {
666    for (collider, density, mut collider_mass_properties) in &mut query {
667        // Update the collider's mass properties.
668        *collider_mass_properties =
669            ColliderMassProperties::from(collider.mass_properties(density.0));
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    #![allow(clippy::unnecessary_cast)]
676
677    #[cfg(feature = "default-collider")]
678    use super::*;
679
680    #[test]
681    #[cfg(feature = "default-collider")]
682    fn sensor_mass_properties() {
683        let mut app = App::new();
684
685        app.init_schedule(PhysicsSchedule)
686            .init_schedule(SubstepSchedule);
687
688        app.add_plugins((
689            MassPropertyPlugin::new(FixedPostUpdate),
690            ColliderHierarchyPlugin,
691            ColliderTransformPlugin::default(),
692            ColliderBackendPlugin::<Collider>::new(FixedPostUpdate),
693        ));
694
695        let collider = Collider::capsule(0.5, 2.0);
696        let mass_properties = MassPropertiesBundle::from_shape(&collider, 1.0);
697
698        let parent = app
699            .world_mut()
700            .spawn((
701                RigidBody::Dynamic,
702                mass_properties.clone(),
703                Transform::default(),
704            ))
705            .id();
706
707        let child = app
708            .world_mut()
709            .spawn((
710                collider,
711                Transform::from_xyz(1.0, 0.0, 0.0),
712                ChildOf(parent),
713            ))
714            .id();
715
716        app.world_mut().run_schedule(FixedPostUpdate);
717
718        assert_eq!(
719            app.world()
720                .entity(parent)
721                .get::<ComputedMass>()
722                .expect("rigid body should have mass")
723                .value() as f32,
724            2.0 * mass_properties.mass.0,
725        );
726        assert!(
727            app.world()
728                .entity(parent)
729                .get::<ComputedCenterOfMass>()
730                .expect("rigid body should have a center of mass")
731                .x
732                > 0.0,
733        );
734
735        // Mark the collider as a sensor. It should no longer contribute to the mass properties of the rigid body.
736        let mut entity_mut = app.world_mut().entity_mut(child);
737        entity_mut.insert(Sensor);
738
739        app.world_mut().run_schedule(FixedPostUpdate);
740
741        assert_eq!(
742            app.world()
743                .entity(parent)
744                .get::<ComputedMass>()
745                .expect("rigid body should have mass")
746                .value() as f32,
747            mass_properties.mass.0,
748        );
749        assert!(
750            app.world()
751                .entity(parent)
752                .get::<ComputedCenterOfMass>()
753                .expect("rigid body should have a center of mass")
754                .x
755                == 0.0,
756        );
757
758        // Remove the sensor component. The collider should contribute to the mass properties of the rigid body again.
759        let mut entity_mut = app.world_mut().entity_mut(child);
760        entity_mut.remove::<Sensor>();
761
762        app.world_mut().run_schedule(FixedPostUpdate);
763
764        assert_eq!(
765            app.world()
766                .entity(parent)
767                .get::<ComputedMass>()
768                .expect("rigid body should have mass")
769                .value() as f32,
770            2.0 * mass_properties.mass.0,
771        );
772        assert!(
773            app.world()
774                .entity(parent)
775                .get::<ComputedCenterOfMass>()
776                .expect("rigid body should have a center of mass")
777                .x
778                > 0.0,
779        );
780    }
781}