Skip to main content

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