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::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#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
43#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
44pub struct ColliderBackendPlugin<C: ScalableCollider> {
69 schedule: Interned<dyn ScheduleLabel>,
70 _phantom: PhantomData<C>,
71}
72
73impl<C: ScalableCollider> ColliderBackendPlugin<C> {
74 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 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 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 hooks
115 .on_add(|mut world, ctx| {
116 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 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 app.world_mut()
165 .register_component_hooks::<C>()
166 .on_remove(|mut world, ctx| {
167 world
171 .commands()
172 .entity(ctx.entity)
173 .try_remove::<ColliderMarker>();
174
175 let entity_ref = world.entity_mut(ctx.entity);
176
177 let Some(ColliderOf { body }) = entity_ref.get::<ColliderOf>().copied() else {
179 return;
180 };
181
182 world
184 .commands()
185 .entity(body)
186 .try_insert(RecomputeMassProperties);
187 });
188
189 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 *collider_mass_properties == ColliderMassProperties::ZERO {
199 return;
200 }
201
202 if let Ok(mut entity_commands) = commands.get_entity(body) {
204 entity_commands.insert(RecomputeMassProperties);
205 }
206 }
207 },
208 );
209
210 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 *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#[derive(Reflect, Component, Clone, Copy, Debug, Default)]
255#[reflect(Component, Debug, Default)]
256pub struct ColliderMarker;
257
258#[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 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#[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 continue;
348 }
349 } else {
350 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 let Some(collider_data) = collider_data else {
389 continue;
390 };
391
392 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 continue;
406 };
407 let Some(mesh) = meshes.get(mesh_handle) else {
408 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#[allow(clippy::type_complexity)]
460pub fn update_collider_scale<C: ScalableCollider>(
461 mut colliders: ParamSet<(
462 Query<(&Transform, &mut C), (Without<ChildOf>, Or<(Changed<Transform>, Changed<C>)>)>,
464 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 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 collider.set_scale(scale, 10);
483 }
484 }
485 }
486 for (collider_transform, mut collider) in &mut colliders.p1() {
488 if collider_transform.scale != collider.scale() {
489 collider.set_scale(collider_transform.scale, 10);
492 }
493 }
494}
495
496#[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 *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 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 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}