avian3d/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::collider::EnlargedAabb,
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},
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, EnlargedAabb>();
100        let _ = app.try_register_required_components::<C, CollisionLayers>();
101        let _ = app.try_register_required_components::<C, ColliderDensity>();
102        let _ = app.try_register_required_components::<C, ColliderMassProperties>();
103
104        // Make sure the necessary resources are initialized.
105        app.init_resource::<PhysicsTransformConfig>();
106        app.init_resource::<NarrowPhaseConfig>();
107        app.init_resource::<PhysicsLengthUnit>();
108
109        let hooks = app.world_mut().register_component_hooks::<C>();
110
111        // Initialize missing components for colliders.
112        hooks
113            .on_add(|mut world, ctx| {
114                // Initialize the global physics transform for the collider.
115                // Avoid doing this twice for rigid bodies added at the same time.
116                // TODO: The special case for rigid bodies is a bit of a hack here.
117                if !world.entity(ctx.entity).contains::<RigidBody>() {
118                    init_physics_transform(&mut world, &ctx);
119                }
120            })
121            .on_insert(|mut world, ctx| {
122                let scale = world
123                    .entity(ctx.entity)
124                    .get::<GlobalTransform>()
125                    .map(|gt| gt.scale())
126                    .unwrap_or_default();
127                #[cfg(feature = "2d")]
128                let scale = scale.xy();
129
130                let mut entity_mut = world.entity_mut(ctx.entity);
131
132                // Make sure the collider is initialized with the correct scale.
133                // This overwrites the scale set by the constructor, but that one is
134                // meant to be only changed after initialization.
135                entity_mut
136                    .get_mut::<C>()
137                    .unwrap()
138                    .set_scale(scale.adjust_precision(), 10);
139
140                let collider = entity_mut.get::<C>().unwrap();
141
142                let density = entity_mut
143                    .get::<ColliderDensity>()
144                    .copied()
145                    .unwrap_or_default();
146
147                let mass_properties = if entity_mut.get::<Sensor>().is_some() {
148                    MassProperties::ZERO
149                } else {
150                    collider.mass_properties(density.0)
151                };
152
153                if let Some(mut collider_mass_properties) =
154                    entity_mut.get_mut::<ColliderMassProperties>()
155                {
156                    *collider_mass_properties = ColliderMassProperties::from(mass_properties);
157                }
158            });
159
160        // Register a component hook that removes `ColliderMarker` components
161        // and updates rigid bodies when their collider is removed.
162        app.world_mut()
163            .register_component_hooks::<C>()
164            .on_remove(|mut world, ctx| {
165                // Remove the `ColliderMarker` associated with the collider.
166                // TODO: If the same entity had multiple *different* types of colliders, this would
167                //       get removed even if just one collider was removed. This is a very niche edge case though.
168                world
169                    .commands()
170                    .entity(ctx.entity)
171                    .try_remove::<ColliderMarker>();
172
173                let entity_ref = world.entity_mut(ctx.entity);
174
175                // Get the rigid body entity that the collider is attached to.
176                let Some(ColliderOf { body }) = entity_ref.get::<ColliderOf>().copied() else {
177                    return;
178                };
179
180                // Queue the rigid body for a mass property update.
181                world
182                    .commands()
183                    .entity(body)
184                    .try_insert(RecomputeMassProperties);
185            });
186
187        // When the `Sensor` component is added to a collider, queue its rigid body for a mass property update.
188        app.add_observer(
189            |trigger: On<Add, Sensor>,
190             mut commands: Commands,
191             query: Query<(&ColliderMassProperties, &ColliderOf)>| {
192                if let Ok((collider_mass_properties, &ColliderOf { body })) =
193                    query.get(trigger.entity)
194                {
195                    // If the collider mass properties are zero, there is nothing to subtract.
196                    if *collider_mass_properties == ColliderMassProperties::ZERO {
197                        return;
198                    }
199
200                    // Queue the rigid body for a mass property update.
201                    if let Ok(mut entity_commands) = commands.get_entity(body) {
202                        entity_commands.insert(RecomputeMassProperties);
203                    }
204                }
205            },
206        );
207
208        // When the `Sensor` component is removed from a collider, update its mass properties.
209        app.add_observer(
210            |trigger: On<Remove, Sensor>,
211             mut collider_query: Query<(
212                Ref<C>,
213                &ColliderDensity,
214                &mut ColliderMassProperties,
215            )>| {
216                if let Ok((collider, density, mut collider_mass_properties)) =
217                    collider_query.get_mut(trigger.entity)
218                {
219                    // Update collider mass props.
220                    *collider_mass_properties =
221                        ColliderMassProperties::from(collider.mass_properties(density.0));
222                }
223            },
224        );
225
226        app.add_systems(
227            self.schedule,
228            (
229                update_collider_scale::<C>
230                    .in_set(PhysicsSystems::Prepare)
231                    .after(PhysicsTransformSystems::TransformToPosition),
232                update_collider_mass_properties::<C>
233                    .in_set(MassPropertySystems::UpdateColliderMassProperties),
234            )
235                .chain(),
236        );
237
238        #[cfg(feature = "default-collider")]
239        app.add_systems(
240            Update,
241            (
242                init_collider_constructors,
243                init_collider_constructor_hierarchies,
244            ),
245        );
246    }
247}
248
249/// A marker component for colliders. Inserted and removed automatically.
250///
251/// This is useful for filtering collider entities regardless of the [collider backend](ColliderBackendPlugin).
252#[derive(Reflect, Component, Clone, Copy, Debug, Default)]
253#[reflect(Component, Debug, Default)]
254pub struct ColliderMarker;
255
256/// Generates [`Collider`]s based on [`ColliderConstructor`]s.
257///
258/// If a [`ColliderConstructor`] requires a mesh, the system keeps running
259/// until the mesh associated with the mesh handle is available.
260///
261/// # Panics
262///
263/// Panics if the [`ColliderConstructor`] requires a mesh but no mesh handle is found.
264#[cfg(feature = "default-collider")]
265fn init_collider_constructors(
266    mut commands: Commands,
267    #[cfg(feature = "collider-from-mesh")] meshes: Res<Assets<Mesh>>,
268    #[cfg(feature = "collider-from-mesh")] mesh_handles: Query<&Mesh3d>,
269    #[cfg(feature = "collider-from-mesh")] mut collider_cache: Option<ResMut<ColliderCache>>,
270    constructors: Query<(
271        Entity,
272        Option<&Collider>,
273        Option<&Name>,
274        &ColliderConstructor,
275    )>,
276) {
277    for (entity, existing_collider, name, constructor) in constructors.iter() {
278        let name = pretty_name(name, entity);
279        if existing_collider.is_some() {
280            warn!(
281                "Tried to add a collider to entity {name} via {constructor:#?}, \
282                but that entity already holds a collider. Skipping.",
283            );
284            commands.entity(entity).remove::<ColliderConstructor>();
285            continue;
286        }
287        #[cfg(feature = "collider-from-mesh")]
288        let collider = if constructor.requires_mesh() {
289            let mesh_handle = mesh_handles.get(entity).unwrap_or_else(|_| panic!(
290                "Tried to add a collider to entity {name} via {constructor:#?} that requires a mesh, \
291                but no mesh handle was found"));
292            let Some(mesh) = meshes.get(mesh_handle) else {
293                // Mesh required, but not loaded yet
294                continue;
295            };
296            collider_cache
297                .as_mut()
298                .map(|cache| cache.get_or_insert(mesh_handle, mesh, constructor.clone()))
299                .unwrap_or_else(|| Collider::try_from_constructor(constructor.clone(), Some(mesh)))
300        } else {
301            Collider::try_from_constructor(constructor.clone(), None)
302        };
303
304        #[cfg(not(feature = "collider-from-mesh"))]
305        let collider = Collider::try_from_constructor(constructor.clone());
306
307        if let Some(collider) = collider {
308            commands.entity(entity).insert(collider);
309            commands.trigger(ColliderConstructorReady { entity })
310        } else {
311            error!(
312                "Tried to add a collider to entity {name} via {constructor:#?}, \
313                but the collider could not be generated. Skipping.",
314            );
315        }
316        commands.entity(entity).remove::<ColliderConstructor>();
317    }
318}
319
320/// Generates [`Collider`]s for descendants of entities with the [`ColliderConstructorHierarchy`] component.
321///
322/// If an entity has a `SceneInstance`, its collider hierarchy is only generated once the scene is ready.
323#[cfg(feature = "default-collider")]
324fn init_collider_constructor_hierarchies(
325    mut commands: Commands,
326    #[cfg(feature = "collider-from-mesh")] meshes: Res<Assets<Mesh>>,
327    #[cfg(feature = "collider-from-mesh")] mesh_handles: Query<&Mesh3d>,
328    #[cfg(feature = "collider-from-mesh")] mut collider_cache: Option<ResMut<ColliderCache>>,
329    #[cfg(feature = "bevy_scene")] scene_spawner: Res<SceneSpawner>,
330    #[cfg(feature = "bevy_scene")] scenes: Query<&SceneRoot>,
331    #[cfg(feature = "bevy_scene")] scene_instances: Query<&SceneInstance>,
332    collider_constructors: Query<(Entity, &ColliderConstructorHierarchy)>,
333    children: Query<&Children>,
334    child_query: Query<(Option<&Name>, Option<&Collider>)>,
335) {
336    use super::ColliderConstructorHierarchyConfig;
337
338    for (scene_entity, collider_constructor_hierarchy) in collider_constructors.iter() {
339        #[cfg(feature = "bevy_scene")]
340        {
341            if scenes.contains(scene_entity) {
342                if let Ok(scene_instance) = scene_instances.get(scene_entity) {
343                    if !scene_spawner.instance_is_ready(**scene_instance) {
344                        // Wait for the scene to be ready
345                        continue;
346                    }
347                } else {
348                    // SceneInstance is added in the SpawnScene schedule, so it might not be available yet
349                    continue;
350                }
351            }
352        }
353
354        for child_entity in children.iter_descendants(scene_entity) {
355            let Ok((name, existing_collider)) = child_query.get(child_entity) else {
356                continue;
357            };
358
359            let pretty_name = pretty_name(name, child_entity);
360
361            let default_collider = || {
362                Some(ColliderConstructorHierarchyConfig {
363                    constructor: collider_constructor_hierarchy.default_constructor.clone(),
364                    ..default()
365                })
366            };
367
368            let collider_data = if let Some(name) = name {
369                collider_constructor_hierarchy
370                    .config
371                    .get(name.as_str())
372                    .cloned()
373                    .unwrap_or_else(default_collider)
374            } else if existing_collider.is_some() {
375                warn!(
376                    "Tried to add a collider to entity {pretty_name} via {collider_constructor_hierarchy:#?}, \
377                        but that entity already holds a collider. Skipping. \
378                        If this was intentional, add the name of the collider to overwrite to `ColliderConstructorHierarchy.config`."
379                );
380                continue;
381            } else {
382                default_collider()
383            };
384
385            // If the configuration is explicitly set to `None`, skip this entity.
386            let Some(collider_data) = collider_data else {
387                continue;
388            };
389
390            // Use the configured constructor if specified, otherwise use the default constructor.
391            // If both are `None`, skip this entity.
392            let Some(constructor) = collider_data
393                .constructor
394                .or_else(|| collider_constructor_hierarchy.default_constructor.clone())
395            else {
396                continue;
397            };
398
399            #[cfg(feature = "collider-from-mesh")]
400            let collider = if constructor.requires_mesh() {
401                let Ok(mesh_handle) = mesh_handles.get(child_entity) else {
402                    // This child entity does not have a mesh, so we skip it.
403                    continue;
404                };
405                let Some(mesh) = meshes.get(mesh_handle) else {
406                    // Mesh required, but not loaded yet
407                    continue;
408                };
409                collider_cache
410                    .as_mut()
411                    .map(|cache| cache.get_or_insert(mesh_handle, mesh, constructor.clone()))
412                    .unwrap_or_else(|| {
413                        Collider::try_from_constructor(constructor.clone(), Some(mesh))
414                    })
415            } else {
416                Collider::try_from_constructor(constructor.clone(), None)
417            };
418
419            #[cfg(not(feature = "collider-from-mesh"))]
420            let collider = Collider::try_from_constructor(constructor);
421
422            if let Some(collider) = collider {
423                commands.entity(child_entity).insert((
424                    collider,
425                    collider_data
426                        .layers
427                        .unwrap_or(collider_constructor_hierarchy.default_layers),
428                    collider_data
429                        .density
430                        .unwrap_or(collider_constructor_hierarchy.default_density),
431                ));
432            } else {
433                error!(
434                    "Tried to add a collider to entity {pretty_name} via {collider_constructor_hierarchy:#?}, \
435                        but the collider could not be generated. Skipping.",
436                );
437            }
438        }
439
440        commands
441            .entity(scene_entity)
442            .remove::<ColliderConstructorHierarchy>();
443
444        commands.trigger(ColliderConstructorHierarchyReady {
445            entity: scene_entity,
446        })
447    }
448}
449
450#[cfg(feature = "default-collider")]
451fn pretty_name(name: Option<&Name>, entity: Entity) -> String {
452    name.map(|n| n.to_string())
453        .unwrap_or_else(|| format!("<unnamed entity {}>", entity.index()))
454}
455
456/// Updates the scale of colliders based on [`Transform`] scale.
457#[allow(clippy::type_complexity)]
458pub fn update_collider_scale<C: ScalableCollider>(
459    mut colliders: ParamSet<(
460        // Root bodies
461        Query<(&Transform, &mut C), (Without<ChildOf>, Or<(Changed<Transform>, Changed<C>)>)>,
462        // Child colliders
463        Query<
464            (&ColliderTransform, &mut C),
465            (With<ChildOf>, Or<(Changed<ColliderTransform>, Changed<C>)>),
466        >,
467    )>,
468    config: Res<PhysicsTransformConfig>,
469) {
470    if config.transform_to_collider_scale {
471        // Update collider scale for root bodies
472        for (transform, mut collider) in &mut colliders.p0() {
473            #[cfg(feature = "2d")]
474            let scale = transform.scale.truncate().adjust_precision();
475            #[cfg(feature = "3d")]
476            let scale = transform.scale.adjust_precision();
477            if scale != collider.scale() {
478                // TODO: Support configurable subdivision count for shapes that
479                //       can't be represented without approximations after scaling.
480                collider.set_scale(scale, 10);
481            }
482        }
483    }
484    // Update collider scale for child colliders
485    for (collider_transform, mut collider) in &mut colliders.p1() {
486        if collider_transform.scale != collider.scale() {
487            // TODO: Support configurable subdivision count for shapes that
488            //       can't be represented without approximations after scaling.
489            collider.set_scale(collider_transform.scale, 10);
490        }
491    }
492}
493
494/// Updates the mass properties of [`Collider`].
495#[allow(clippy::type_complexity)]
496pub(crate) fn update_collider_mass_properties<C: AnyCollider>(
497    mut query: Query<
498        (Ref<C>, &ColliderDensity, &mut ColliderMassProperties),
499        (Or<(Changed<C>, Changed<ColliderDensity>)>, Without<Sensor>),
500    >,
501) {
502    for (collider, density, mut collider_mass_properties) in &mut query {
503        // Update the collider's mass properties.
504        *collider_mass_properties =
505            ColliderMassProperties::from(collider.mass_properties(density.0));
506    }
507}
508
509#[cfg(test)]
510mod tests {
511    #![allow(clippy::unnecessary_cast)]
512
513    #[cfg(feature = "default-collider")]
514    use super::*;
515
516    #[test]
517    #[cfg(feature = "default-collider")]
518    fn sensor_mass_properties() {
519        let mut app = App::new();
520
521        app.init_schedule(PhysicsSchedule)
522            .init_schedule(SubstepSchedule);
523
524        app.add_plugins((
525            MassPropertyPlugin::new(FixedPostUpdate),
526            ColliderHierarchyPlugin,
527            ColliderTransformPlugin::default(),
528            ColliderBackendPlugin::<Collider>::new(FixedPostUpdate),
529        ));
530
531        let collider = Collider::capsule(0.5, 2.0);
532        let mass_properties = MassPropertiesBundle::from_shape(&collider, 1.0);
533
534        let parent = app
535            .world_mut()
536            .spawn((
537                RigidBody::Dynamic,
538                mass_properties.clone(),
539                Transform::default(),
540            ))
541            .id();
542
543        let child = app
544            .world_mut()
545            .spawn((
546                collider,
547                Transform::from_xyz(1.0, 0.0, 0.0),
548                ChildOf(parent),
549            ))
550            .id();
551
552        app.world_mut().run_schedule(FixedPostUpdate);
553
554        assert_eq!(
555            app.world()
556                .entity(parent)
557                .get::<ComputedMass>()
558                .expect("rigid body should have mass")
559                .value() as f32,
560            2.0 * mass_properties.mass.0,
561        );
562        assert!(
563            app.world()
564                .entity(parent)
565                .get::<ComputedCenterOfMass>()
566                .expect("rigid body should have a center of mass")
567                .x
568                > 0.0,
569        );
570
571        // Mark the collider as a sensor. It should no longer contribute to the mass properties of the rigid body.
572        let mut entity_mut = app.world_mut().entity_mut(child);
573        entity_mut.insert(Sensor);
574
575        app.world_mut().run_schedule(FixedPostUpdate);
576
577        assert_eq!(
578            app.world()
579                .entity(parent)
580                .get::<ComputedMass>()
581                .expect("rigid body should have mass")
582                .value() as f32,
583            mass_properties.mass.0,
584        );
585        assert!(
586            app.world()
587                .entity(parent)
588                .get::<ComputedCenterOfMass>()
589                .expect("rigid body should have a center of mass")
590                .x
591                == 0.0,
592        );
593
594        // Remove the sensor component. The collider should contribute to the mass properties of the rigid body again.
595        let mut entity_mut = app.world_mut().entity_mut(child);
596        entity_mut.remove::<Sensor>();
597
598        app.world_mut().run_schedule(FixedPostUpdate);
599
600        assert_eq!(
601            app.world()
602                .entity(parent)
603                .get::<ComputedMass>()
604                .expect("rigid body should have mass")
605                .value() as f32,
606            2.0 * mass_properties.mass.0,
607        );
608        assert!(
609            app.world()
610                .entity(parent)
611                .get::<ComputedCenterOfMass>()
612                .expect("rigid body should have a center of mass")
613                .x
614                > 0.0,
615        );
616    }
617}