use core::marker::PhantomData;
#[cfg(all(feature = "collider-from-mesh", feature = "default-collider"))]
use crate::collision::collider::cache::ColliderCache;
use crate::{
collision::collider::EnlargedAabb,
physics_transform::{PhysicsTransformConfig, PhysicsTransformSystems, init_physics_transform},
prelude::*,
};
#[cfg(all(feature = "bevy_scene", feature = "default-collider"))]
use bevy::scene::SceneInstance;
use bevy::{
ecs::{intern::Interned, schedule::ScheduleLabel},
prelude::*,
};
use mass_properties::{MassPropertySystems, components::RecomputeMassProperties};
#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
pub struct ColliderBackendPlugin<C: ScalableCollider> {
schedule: Interned<dyn ScheduleLabel>,
_phantom: PhantomData<C>,
}
impl<C: ScalableCollider> ColliderBackendPlugin<C> {
pub fn new(schedule: impl ScheduleLabel) -> Self {
Self {
schedule: schedule.intern(),
_phantom: PhantomData,
}
}
}
impl<C: ScalableCollider> Default for ColliderBackendPlugin<C> {
fn default() -> Self {
Self {
schedule: FixedPostUpdate.intern(),
_phantom: PhantomData,
}
}
}
impl<C: ScalableCollider> Plugin for ColliderBackendPlugin<C> {
fn build(&self, app: &mut App) {
let _ = app.try_register_required_components_with::<C, Position>(|| Position::PLACEHOLDER);
let _ = app.try_register_required_components_with::<C, Rotation>(|| Rotation::PLACEHOLDER);
let _ = app.try_register_required_components::<C, ColliderMarker>();
let _ = app.try_register_required_components::<C, ColliderAabb>();
let _ = app.try_register_required_components::<C, EnlargedAabb>();
let _ = app.try_register_required_components::<C, CollisionLayers>();
let _ = app.try_register_required_components::<C, ColliderDensity>();
let _ = app.try_register_required_components::<C, ColliderMassProperties>();
app.init_resource::<PhysicsTransformConfig>();
app.init_resource::<NarrowPhaseConfig>();
app.init_resource::<PhysicsLengthUnit>();
let hooks = app.world_mut().register_component_hooks::<C>();
hooks
.on_add(|mut world, ctx| {
if !world.entity(ctx.entity).contains::<RigidBody>() {
init_physics_transform(&mut world, &ctx);
}
})
.on_insert(|mut world, ctx| {
let scale = world
.entity(ctx.entity)
.get::<GlobalTransform>()
.map(|gt| gt.scale())
.unwrap_or_default();
#[cfg(feature = "2d")]
let scale = scale.xy();
let mut entity_mut = world.entity_mut(ctx.entity);
entity_mut
.get_mut::<C>()
.unwrap()
.set_scale(scale.adjust_precision(), 10);
let collider = entity_mut.get::<C>().unwrap();
let density = entity_mut
.get::<ColliderDensity>()
.copied()
.unwrap_or_default();
let mass_properties = if entity_mut.get::<Sensor>().is_some() {
MassProperties::ZERO
} else {
collider.mass_properties(density.0)
};
if let Some(mut collider_mass_properties) =
entity_mut.get_mut::<ColliderMassProperties>()
{
*collider_mass_properties = ColliderMassProperties::from(mass_properties);
}
});
app.world_mut()
.register_component_hooks::<C>()
.on_remove(|mut world, ctx| {
world
.commands()
.entity(ctx.entity)
.try_remove::<ColliderMarker>();
let entity_ref = world.entity_mut(ctx.entity);
let Some(ColliderOf { body }) = entity_ref.get::<ColliderOf>().copied() else {
return;
};
world
.commands()
.entity(body)
.try_insert(RecomputeMassProperties);
});
app.add_observer(
|trigger: On<Add, Sensor>,
mut commands: Commands,
query: Query<(&ColliderMassProperties, &ColliderOf)>| {
if let Ok((collider_mass_properties, &ColliderOf { body })) =
query.get(trigger.entity)
{
if *collider_mass_properties == ColliderMassProperties::ZERO {
return;
}
if let Ok(mut entity_commands) = commands.get_entity(body) {
entity_commands.insert(RecomputeMassProperties);
}
}
},
);
app.add_observer(
|trigger: On<Remove, Sensor>,
mut collider_query: Query<(
Ref<C>,
&ColliderDensity,
&mut ColliderMassProperties,
)>| {
if let Ok((collider, density, mut collider_mass_properties)) =
collider_query.get_mut(trigger.entity)
{
*collider_mass_properties =
ColliderMassProperties::from(collider.mass_properties(density.0));
}
},
);
app.add_systems(
self.schedule,
(
update_collider_scale::<C>
.in_set(PhysicsSystems::Prepare)
.after(PhysicsTransformSystems::TransformToPosition),
update_collider_mass_properties::<C>
.in_set(MassPropertySystems::UpdateColliderMassProperties),
)
.chain(),
);
#[cfg(feature = "default-collider")]
app.add_systems(
Update,
(
init_collider_constructors,
init_collider_constructor_hierarchies,
),
);
}
}
#[derive(Reflect, Component, Clone, Copy, Debug, Default)]
#[reflect(Component, Debug, Default)]
pub struct ColliderMarker;
#[cfg(feature = "default-collider")]
fn init_collider_constructors(
mut commands: Commands,
#[cfg(feature = "collider-from-mesh")] meshes: Res<Assets<Mesh>>,
#[cfg(feature = "collider-from-mesh")] mesh_handles: Query<&Mesh3d>,
#[cfg(feature = "collider-from-mesh")] mut collider_cache: Option<ResMut<ColliderCache>>,
constructors: Query<(
Entity,
Option<&Collider>,
Option<&Name>,
&ColliderConstructor,
)>,
) {
for (entity, existing_collider, name, constructor) in constructors.iter() {
let name = pretty_name(name, entity);
if existing_collider.is_some() {
warn!(
"Tried to add a collider to entity {name} via {constructor:#?}, \
but that entity already holds a collider. Skipping.",
);
commands.entity(entity).remove::<ColliderConstructor>();
continue;
}
#[cfg(feature = "collider-from-mesh")]
let collider = if constructor.requires_mesh() {
let mesh_handle = mesh_handles.get(entity).unwrap_or_else(|_| panic!(
"Tried to add a collider to entity {name} via {constructor:#?} that requires a mesh, \
but no mesh handle was found"));
let Some(mesh) = meshes.get(mesh_handle) else {
continue;
};
collider_cache
.as_mut()
.map(|cache| cache.get_or_insert(mesh_handle, mesh, constructor.clone()))
.unwrap_or_else(|| Collider::try_from_constructor(constructor.clone(), Some(mesh)))
} else {
Collider::try_from_constructor(constructor.clone(), None)
};
#[cfg(not(feature = "collider-from-mesh"))]
let collider = Collider::try_from_constructor(constructor.clone());
if let Some(collider) = collider {
commands.entity(entity).insert(collider);
commands.trigger(ColliderConstructorReady { entity })
} else {
error!(
"Tried to add a collider to entity {name} via {constructor:#?}, \
but the collider could not be generated. Skipping.",
);
}
commands.entity(entity).remove::<ColliderConstructor>();
}
}
#[cfg(feature = "default-collider")]
fn init_collider_constructor_hierarchies(
mut commands: Commands,
#[cfg(feature = "collider-from-mesh")] meshes: Res<Assets<Mesh>>,
#[cfg(feature = "collider-from-mesh")] mesh_handles: Query<&Mesh3d>,
#[cfg(feature = "collider-from-mesh")] mut collider_cache: Option<ResMut<ColliderCache>>,
#[cfg(feature = "bevy_scene")] scene_spawner: Res<SceneSpawner>,
#[cfg(feature = "bevy_scene")] scenes: Query<&SceneRoot>,
#[cfg(feature = "bevy_scene")] scene_instances: Query<&SceneInstance>,
collider_constructors: Query<(Entity, &ColliderConstructorHierarchy)>,
children: Query<&Children>,
child_query: Query<(Option<&Name>, Option<&Collider>)>,
) {
use super::ColliderConstructorHierarchyConfig;
for (scene_entity, collider_constructor_hierarchy) in collider_constructors.iter() {
#[cfg(feature = "bevy_scene")]
{
if scenes.contains(scene_entity) {
if let Ok(scene_instance) = scene_instances.get(scene_entity) {
if !scene_spawner.instance_is_ready(**scene_instance) {
continue;
}
} else {
continue;
}
}
}
for child_entity in children.iter_descendants(scene_entity) {
let Ok((name, existing_collider)) = child_query.get(child_entity) else {
continue;
};
let pretty_name = pretty_name(name, child_entity);
let default_collider = || {
Some(ColliderConstructorHierarchyConfig {
constructor: collider_constructor_hierarchy.default_constructor.clone(),
..default()
})
};
let collider_data = if let Some(name) = name {
collider_constructor_hierarchy
.config
.get(name.as_str())
.cloned()
.unwrap_or_else(default_collider)
} else if existing_collider.is_some() {
warn!(
"Tried to add a collider to entity {pretty_name} via {collider_constructor_hierarchy:#?}, \
but that entity already holds a collider. Skipping. \
If this was intentional, add the name of the collider to overwrite to `ColliderConstructorHierarchy.config`."
);
continue;
} else {
default_collider()
};
let Some(collider_data) = collider_data else {
continue;
};
let Some(constructor) = collider_data
.constructor
.or_else(|| collider_constructor_hierarchy.default_constructor.clone())
else {
continue;
};
#[cfg(feature = "collider-from-mesh")]
let collider = if constructor.requires_mesh() {
let Ok(mesh_handle) = mesh_handles.get(child_entity) else {
continue;
};
let Some(mesh) = meshes.get(mesh_handle) else {
continue;
};
collider_cache
.as_mut()
.map(|cache| cache.get_or_insert(mesh_handle, mesh, constructor.clone()))
.unwrap_or_else(|| {
Collider::try_from_constructor(constructor.clone(), Some(mesh))
})
} else {
Collider::try_from_constructor(constructor.clone(), None)
};
#[cfg(not(feature = "collider-from-mesh"))]
let collider = Collider::try_from_constructor(constructor);
if let Some(collider) = collider {
commands.entity(child_entity).insert((
collider,
collider_data
.layers
.unwrap_or(collider_constructor_hierarchy.default_layers),
collider_data
.density
.unwrap_or(collider_constructor_hierarchy.default_density),
));
} else {
error!(
"Tried to add a collider to entity {pretty_name} via {collider_constructor_hierarchy:#?}, \
but the collider could not be generated. Skipping.",
);
}
}
commands
.entity(scene_entity)
.remove::<ColliderConstructorHierarchy>();
commands.trigger(ColliderConstructorHierarchyReady {
entity: scene_entity,
})
}
}
#[cfg(feature = "default-collider")]
fn pretty_name(name: Option<&Name>, entity: Entity) -> String {
name.map(|n| n.to_string())
.unwrap_or_else(|| format!("<unnamed entity {}>", entity.index()))
}
#[allow(clippy::type_complexity)]
pub fn update_collider_scale<C: ScalableCollider>(
mut colliders: ParamSet<(
// Root bodies
Query<(&Transform, &mut C), (Without<ChildOf>, Or<(Changed<Transform>, Changed<C>)>)>,
// Child colliders
Query<
(&ColliderTransform, &mut C),
(With<ChildOf>, Or<(Changed<ColliderTransform>, Changed<C>)>),
>,
)>,
config: Res<PhysicsTransformConfig>,
) {
if config.transform_to_collider_scale {
for (transform, mut collider) in &mut colliders.p0() {
#[cfg(feature = "2d")]
let scale = transform.scale.truncate().adjust_precision();
#[cfg(feature = "3d")]
let scale = transform.scale.adjust_precision();
if scale != collider.scale() {
collider.set_scale(scale, 10);
}
}
}
for (collider_transform, mut collider) in &mut colliders.p1() {
if collider_transform.scale != collider.scale() {
collider.set_scale(collider_transform.scale, 10);
}
}
}
#[allow(clippy::type_complexity)]
pub(crate) fn update_collider_mass_properties<C: AnyCollider>(
mut query: Query<
(Ref<C>, &ColliderDensity, &mut ColliderMassProperties),
(Or<(Changed<C>, Changed<ColliderDensity>)>, Without<Sensor>),
>,
) {
for (collider, density, mut collider_mass_properties) in &mut query {
*collider_mass_properties =
ColliderMassProperties::from(collider.mass_properties(density.0));
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unnecessary_cast)]
#[cfg(feature = "default-collider")]
use super::*;
#[test]
#[cfg(feature = "default-collider")]
fn sensor_mass_properties() {
let mut app = App::new();
app.init_schedule(PhysicsSchedule)
.init_schedule(SubstepSchedule);
app.add_plugins((
MassPropertyPlugin::new(FixedPostUpdate),
ColliderHierarchyPlugin,
ColliderTransformPlugin::default(),
ColliderBackendPlugin::<Collider>::new(FixedPostUpdate),
));
let collider = Collider::capsule(0.5, 2.0);
let mass_properties = MassPropertiesBundle::from_shape(&collider, 1.0);
let parent = app
.world_mut()
.spawn((
RigidBody::Dynamic,
mass_properties.clone(),
Transform::default(),
))
.id();
let child = app
.world_mut()
.spawn((
collider,
Transform::from_xyz(1.0, 0.0, 0.0),
ChildOf(parent),
))
.id();
app.world_mut().run_schedule(FixedPostUpdate);
assert_eq!(
app.world()
.entity(parent)
.get::<ComputedMass>()
.expect("rigid body should have mass")
.value() as f32,
2.0 * mass_properties.mass.0,
);
assert!(
app.world()
.entity(parent)
.get::<ComputedCenterOfMass>()
.expect("rigid body should have a center of mass")
.x
> 0.0,
);
let mut entity_mut = app.world_mut().entity_mut(child);
entity_mut.insert(Sensor);
app.world_mut().run_schedule(FixedPostUpdate);
assert_eq!(
app.world()
.entity(parent)
.get::<ComputedMass>()
.expect("rigid body should have mass")
.value() as f32,
mass_properties.mass.0,
);
assert!(
app.world()
.entity(parent)
.get::<ComputedCenterOfMass>()
.expect("rigid body should have a center of mass")
.x
== 0.0,
);
let mut entity_mut = app.world_mut().entity_mut(child);
entity_mut.remove::<Sensor>();
app.world_mut().run_schedule(FixedPostUpdate);
assert_eq!(
app.world()
.entity(parent)
.get::<ComputedMass>()
.expect("rigid body should have mass")
.value() as f32,
2.0 * mass_properties.mass.0,
);
assert!(
app.world()
.entity(parent)
.get::<ComputedCenterOfMass>()
.expect("rigid body should have a center of mass")
.x
> 0.0,
);
}
}