1use 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#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
41#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
42pub struct ColliderBackendPlugin<C: ScalableCollider> {
67 schedule: Interned<dyn ScheduleLabel>,
68 _phantom: PhantomData<C>,
69}
70
71impl<C: ScalableCollider> ColliderBackendPlugin<C> {
72 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 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 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 hooks
113 .on_add(|mut world, ctx| {
114 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 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 app.world_mut()
163 .register_component_hooks::<C>()
164 .on_remove(|mut world, ctx| {
165 world
169 .commands()
170 .entity(ctx.entity)
171 .try_remove::<ColliderMarker>();
172
173 let entity_ref = world.entity_mut(ctx.entity);
174
175 let Some(ColliderOf { body }) = entity_ref.get::<ColliderOf>().copied() else {
177 return;
178 };
179
180 world
182 .commands()
183 .entity(body)
184 .try_insert(RecomputeMassProperties);
185 });
186
187 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 *collider_mass_properties == ColliderMassProperties::ZERO {
197 return;
198 }
199
200 if let Ok(mut entity_commands) = commands.get_entity(body) {
202 entity_commands.insert(RecomputeMassProperties);
203 }
204 }
205 },
206 );
207
208 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 *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#[derive(Reflect, Component, Clone, Copy, Debug, Default)]
253#[reflect(Component, Debug, Default)]
254pub struct ColliderMarker;
255
256#[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 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#[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 continue;
346 }
347 } else {
348 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 let Some(collider_data) = collider_data else {
387 continue;
388 };
389
390 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 continue;
404 };
405 let Some(mesh) = meshes.get(mesh_handle) else {
406 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#[allow(clippy::type_complexity)]
458pub fn update_collider_scale<C: ScalableCollider>(
459 mut colliders: ParamSet<(
460 Query<(&Transform, &mut C), (Without<ChildOf>, Or<(Changed<Transform>, Changed<C>)>)>,
462 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 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 collider.set_scale(scale, 10);
481 }
482 }
483 }
484 for (collider_transform, mut collider) in &mut colliders.p1() {
486 if collider_transform.scale != collider.scale() {
487 collider.set_scale(collider_transform.scale, 10);
490 }
491 }
492}
493
494#[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 *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 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 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}