use crate::prelude::*;
use bevy::prelude::*;
use bevy::utils::HashMap;
#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
#[cfg_attr(
feature = "2d",
doc = " // Spawn the scene and automatically generate circle colliders"
)]
#[cfg_attr(
feature = "3d",
doc = " // Spawn the scene and automatically generate triangle mesh colliders"
)]
#[cfg_attr(
feature = "2d",
doc = " ColliderConstructorHierarchy::new(ColliderConstructor::Circle { radius: 2.0 }),"
)]
#[cfg_attr(
feature = "3d",
doc = " ColliderConstructorHierarchy::new(ColliderConstructor::TrimeshFromMesh),"
)]
#[cfg_attr(
feature = "2d",
doc = " ColliderConstructorHierarchy::new(ColliderConstructor::Circle { radius: 2.0 })
.with_constructor_for_name(\"Tree\", ColliderConstructor::Rectangle { x_length: 1.0, y_length: 2.0 })"
)]
#[cfg_attr(
feature = "3d",
doc = " ColliderConstructorHierarchy::new(ColliderConstructor::TrimeshFromMesh)
.with_constructor_for_name(\"Tree\", ColliderConstructor::ConvexHullFromMesh)"
)]
#[cfg_attr(
feature = "2d",
doc = " .with_constructor_for_name(\"Tree\", ColliderConstructor::Circle { radius: 2.0 }),"
)]
#[cfg_attr(
feature = "3d",
doc = " .with_constructor_for_name(\"Tree\", ColliderConstructor::ConvexHullFromMesh),"
)]
#[cfg_attr(
feature = "2d",
doc = " ColliderConstructorHierarchy::new(ColliderConstructor::Circle { radius: 2.0 })
.without_constructor_for_name(\"Tree\"),"
)]
#[cfg_attr(
feature = "3d",
doc = " ColliderConstructorHierarchy::new(ColliderConstructor::TrimeshFromMeshWithConfig(
TrimeshFlags::MERGE_DUPLICATE_VERTICES
))
.without_constructor_for_name(\"Tree\"),"
)]
#[derive(Component, Clone, Debug, Default, PartialEq, Reflect)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
#[reflect(Component, Debug, PartialEq, Default)]
pub struct ColliderConstructorHierarchy {
pub default_constructor: Option<ColliderConstructor>,
pub default_layers: CollisionLayers,
pub default_density: ColliderDensity,
pub config: HashMap<String, Option<ColliderConstructorHierarchyConfig>>,
}
impl ColliderConstructorHierarchy {
pub fn new(default_constructor: impl Into<Option<ColliderConstructor>>) -> Self {
Self {
default_constructor: default_constructor.into(),
default_layers: CollisionLayers::ALL,
default_density: ColliderDensity(1.0),
config: default(),
}
}
pub fn with_default_layers(mut self, layers: CollisionLayers) -> Self {
self.default_layers = layers;
self
}
pub fn with_default_density(mut self, density: impl Into<ColliderDensity>) -> Self {
self.default_density = density.into();
self
}
pub fn with_constructor_for_name(
mut self,
name: &str,
constructor: ColliderConstructor,
) -> Self {
if let Some(Some(data)) = self.config.get_mut(name) {
data.constructor = Some(constructor);
} else {
self.config.insert(
name.to_string(),
Some(ColliderConstructorHierarchyConfig {
constructor: Some(constructor),
..default()
}),
);
}
self
}
pub fn with_layers_for_name(self, name: &str, layers: CollisionLayers) -> Self {
self.with_config_for_name(name, |config| config.layers = Some(layers))
}
pub fn with_density_for_name(self, name: &str, density: impl Into<ColliderDensity>) -> Self {
let density = density.into();
self.with_config_for_name(name, |config| config.density = Some(density))
}
pub fn without_constructor_for_name(mut self, name: &str) -> Self {
self.config.insert(name.to_string(), None);
self
}
fn with_config_for_name(
mut self,
name: &str,
mut mutate_config: impl FnMut(&mut ColliderConstructorHierarchyConfig),
) -> Self {
if let Some(Some(config)) = self.config.get_mut(name) {
mutate_config(config);
} else {
let mut config = ColliderConstructorHierarchyConfig::default();
mutate_config(&mut config);
self.config.insert(name.to_string(), Some(config));
}
self
}
}
#[derive(Clone, Debug, Default, PartialEq, Reflect)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
#[reflect(Debug, Default, PartialEq)]
pub struct ColliderConstructorHierarchyConfig {
pub constructor: Option<ColliderConstructor>,
pub layers: Option<CollisionLayers>,
pub density: Option<ColliderDensity>,
}
#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
#[cfg_attr(feature = "2d", doc = " // Spawn a circle with radius 2")]
#[cfg_attr(
feature = "3d",
doc = " // Spawn a cube with a convex hull collider generated from the mesh"
)]
#[cfg_attr(
feature = "2d",
doc = " ColliderConstructor::Circle { radius: 2.0 },"
)]
#[cfg_attr(
feature = "3d",
doc = " ColliderConstructor::ConvexHullFromMesh,"
)]
#[derive(Clone, Debug, PartialEq, Reflect, Component)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
#[cfg_attr(feature = "collider-from-mesh", derive(Default))]
#[cfg_attr(feature = "collider-from-mesh", reflect(Default))]
#[reflect(Debug, Component, PartialEq)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum ColliderConstructor {
#[cfg(feature = "2d")]
Circle { radius: Scalar },
#[cfg(feature = "3d")]
Sphere { radius: Scalar },
#[cfg(feature = "2d")]
Ellipse {
half_width: Scalar,
half_height: Scalar,
},
#[cfg(feature = "2d")]
Rectangle { x_length: Scalar, y_length: Scalar },
#[cfg(feature = "3d")]
Cuboid {
x_length: Scalar,
y_length: Scalar,
z_length: Scalar,
},
#[cfg(feature = "2d")]
RoundRectangle {
x_length: Scalar,
y_length: Scalar,
border_radius: Scalar,
},
#[cfg(feature = "3d")]
RoundCuboid {
x_length: Scalar,
y_length: Scalar,
z_length: Scalar,
border_radius: Scalar,
},
#[cfg(feature = "3d")]
Cylinder { radius: Scalar, height: Scalar },
#[cfg(feature = "3d")]
Cone { radius: Scalar, height: Scalar },
Capsule { radius: Scalar, height: Scalar },
CapsuleEndpoints {
radius: Scalar,
a: Vector,
b: Vector,
},
HalfSpace { outward_normal: Vector },
Segment { a: Vector, b: Vector },
Triangle { a: Vector, b: Vector, c: Vector },
#[cfg(feature = "2d")]
RegularPolygon { circumradius: f32, sides: usize },
Polyline {
vertices: Vec<Vector>,
indices: Option<Vec<[u32; 2]>>,
},
Trimesh {
vertices: Vec<Vector>,
indices: Vec<[u32; 3]>,
},
TrimeshWithConfig {
vertices: Vec<Vector>,
indices: Vec<[u32; 3]>,
flags: TrimeshFlags,
},
#[cfg(feature = "2d")]
ConvexDecomposition {
vertices: Vec<Vector>,
indices: Vec<[u32; 2]>,
},
#[cfg(feature = "3d")]
ConvexDecomposition {
vertices: Vec<Vector>,
indices: Vec<[u32; 3]>,
},
#[cfg(feature = "2d")]
ConvexDecompositionWithConfig {
vertices: Vec<Vector>,
indices: Vec<[u32; 2]>,
params: VhacdParameters,
},
#[cfg(feature = "3d")]
ConvexDecompositionWithConfig {
vertices: Vec<Vector>,
indices: Vec<[u32; 3]>,
params: VhacdParameters,
},
#[cfg(feature = "2d")]
ConvexHull { points: Vec<Vector> },
#[cfg(feature = "3d")]
ConvexHull { points: Vec<Vector> },
#[cfg(feature = "2d")]
Heightfield { heights: Vec<Scalar>, scale: Vector },
#[cfg(feature = "3d")]
Heightfield {
heights: Vec<Vec<Scalar>>,
scale: Vector,
},
#[cfg(feature = "collider-from-mesh")]
#[default]
TrimeshFromMesh,
#[cfg(all(
feature = "3d",
feature = "collider-from-mesh",
feature = "default-collider"
))]
TrimeshFromMeshWithConfig(TrimeshFlags),
#[cfg(feature = "collider-from-mesh")]
ConvexDecompositionFromMesh,
#[cfg(all(
feature = "3d",
feature = "collider-from-mesh",
feature = "default-collider"
))]
ConvexDecompositionFromMeshWithConfig(VhacdParameters),
#[cfg(feature = "collider-from-mesh")]
ConvexHullFromMesh,
}
impl ColliderConstructor {
#[cfg(feature = "collider-from-mesh")]
pub fn requires_mesh(&self) -> bool {
matches!(
self,
Self::TrimeshFromMesh
| Self::TrimeshFromMeshWithConfig(_)
| Self::ConvexDecompositionFromMesh
| Self::ConvexDecompositionFromMeshWithConfig(_)
| Self::ConvexHullFromMesh
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::ecs::query::QueryData;
#[cfg(feature = "bevy_scene")]
use bevy::scene::ScenePlugin;
#[test]
fn collider_constructor_requires_no_mesh_on_primitive() {
let mut app = create_test_app();
let entity = app.world_mut().spawn(PRIMITIVE_COLLIDER.clone()).id();
app.update();
assert!(app.query_ok::<&Collider>(entity));
assert!(app.query_err::<&ColliderConstructor>(entity));
}
#[cfg(feature = "collider-from-mesh")]
#[test]
#[should_panic]
fn collider_constructor_requires_mesh_on_computed() {
let mut app = create_test_app();
app.world_mut().spawn(COMPUTED_COLLIDER.clone());
app.update();
}
#[cfg(feature = "collider-from-mesh")]
#[test]
fn collider_constructor_converts_mesh_on_computed() {
let mut app = create_test_app();
let mesh_handle = app.add_mesh();
let entity = app
.world_mut()
.spawn((COMPUTED_COLLIDER.clone(), mesh_handle))
.id();
app.update();
assert!(app.query_ok::<&Collider>(entity));
assert!(app.query_ok::<&Handle<Mesh>>(entity));
assert!(app.query_err::<&ColliderConstructor>(entity));
}
#[test]
fn collider_constructor_hierarchy_does_nothing_on_self_with_primitive() {
let mut app = create_test_app();
let entity = app
.world_mut()
.spawn(ColliderConstructorHierarchy::new(
PRIMITIVE_COLLIDER.clone(),
))
.id();
app.update();
assert!(app.query_err::<&ColliderConstructorHierarchy>(entity));
assert!(app.query_err::<&Collider>(entity));
}
#[cfg(feature = "collider-from-mesh")]
#[test]
fn collider_constructor_hierarchy_does_nothing_on_self_with_computed() {
let mut app = create_test_app();
let mesh_handle = app.add_mesh();
let entity = app
.world_mut()
.spawn((
ColliderConstructorHierarchy::new(COMPUTED_COLLIDER.clone()),
mesh_handle,
))
.id();
app.update();
assert!(app.query_ok::<&Handle<Mesh>>(entity));
assert!(app.query_err::<&ColliderConstructorHierarchy>(entity));
assert!(app.query_err::<&Collider>(entity));
}
#[cfg(feature = "collider-from-mesh")]
#[test]
fn collider_constructor_hierarchy_does_not_require_mesh_on_self_with_computed() {
let mut app = create_test_app();
let entity = app
.world_mut()
.spawn(ColliderConstructorHierarchy::new(COMPUTED_COLLIDER.clone()))
.id();
app.update();
assert!(app.query_err::<&Collider>(entity));
assert!(app.query_err::<&ColliderConstructorHierarchy>(entity));
}
#[test]
fn collider_constructor_hierarchy_inserts_primitive_colliders_on_all_descendants() {
let mut app = create_test_app();
let parent = app
.world_mut()
.spawn(ColliderConstructorHierarchy::new(
PRIMITIVE_COLLIDER.clone(),
))
.id();
let child1 = app.world_mut().spawn(()).id();
let child2 = app.world_mut().spawn(()).id();
let child3 = app.world_mut().spawn(()).id();
app.world_mut()
.entity_mut(parent)
.push_children(&[child1, child2]);
app.world_mut().entity_mut(child2).push_children(&[child3]);
app.update();
assert!(app.query_err::<&ColliderConstructorHierarchy>(parent));
assert!(app.query_err::<&ColliderConstructorHierarchy>(child1));
assert!(app.query_err::<&ColliderConstructorHierarchy>(child2));
assert!(app.query_err::<&ColliderConstructorHierarchy>(child3));
assert!(app.query_err::<&Collider>(parent));
assert!(app.query_ok::<&Collider>(child1));
assert!(app.query_ok::<&Collider>(child2));
assert!(app.query_ok::<&Collider>(child3));
}
#[cfg(feature = "collider-from-mesh")]
#[test]
fn collider_constructor_hierarchy_inserts_computed_colliders_only_on_descendants_with_mesh() {
let mut app = create_test_app();
let mesh_handle = app.add_mesh();
let parent = app
.world_mut()
.spawn(ColliderConstructorHierarchy::new(COMPUTED_COLLIDER.clone()))
.id();
let child1 = app.world_mut().spawn(()).id();
let child2 = app.world_mut().spawn(()).id();
let child3 = app.world_mut().spawn(mesh_handle.clone()).id();
let child4 = app.world_mut().spawn(mesh_handle.clone()).id();
let child5 = app.world_mut().spawn(()).id();
let child6 = app.world_mut().spawn(mesh_handle.clone()).id();
let child7 = app.world_mut().spawn(mesh_handle.clone()).id();
let child8 = app.world_mut().spawn(mesh_handle.clone()).id();
app.world_mut()
.entity_mut(parent)
.push_children(&[child1, child2, child4, child6, child7]);
app.world_mut().entity_mut(child2).push_children(&[child3]);
app.world_mut().entity_mut(child4).push_children(&[child5]);
app.world_mut().entity_mut(child7).push_children(&[child8]);
app.update();
assert!(app.query_err::<&ColliderConstructorHierarchy>(parent));
assert!(app.query_err::<&ColliderConstructorHierarchy>(child1));
assert!(app.query_err::<&ColliderConstructorHierarchy>(child2));
assert!(app.query_err::<&ColliderConstructorHierarchy>(child3));
assert!(app.query_err::<&ColliderConstructorHierarchy>(child4));
assert!(app.query_err::<&ColliderConstructorHierarchy>(child5));
assert!(app.query_err::<&ColliderConstructorHierarchy>(child6));
assert!(app.query_err::<&ColliderConstructorHierarchy>(child7));
assert!(app.query_err::<&ColliderConstructorHierarchy>(child8));
assert!(app.query_err::<&Collider>(parent));
assert!(app.query_err::<&Collider>(child1));
assert!(app.query_err::<&Collider>(child2));
assert!(app.query_ok::<&Collider>(child3));
assert!(app.query_ok::<&Collider>(child4));
assert!(app.query_err::<&Collider>(child5));
assert!(app.query_ok::<&Collider>(child6));
assert!(app.query_ok::<&Collider>(child7));
assert!(app.query_ok::<&Collider>(child8));
}
#[cfg(all(feature = "collider-from-mesh", feature = "bevy_scene"))]
#[test]
fn collider_constructor_hierarchy_inserts_correct_configs_on_scene() {
use parry::shape::ShapeType;
let mut app = create_gltf_test_app();
let scene_handle = app
.world_mut()
.resource_mut::<AssetServer>()
.load("ferris.glb#Scene0");
let hierarchy = app
.world_mut()
.spawn((
SceneBundle {
scene: scene_handle,
..default()
},
ColliderConstructorHierarchy::new(ColliderConstructor::ConvexDecompositionFromMesh)
.with_constructor_for_name("armL_mesh", PRIMITIVE_COLLIDER)
.with_density_for_name("armL_mesh", 2.0)
.without_constructor_for_name("armR_mesh"),
RigidBody::Dynamic,
))
.id();
while app
.world()
.resource::<Events<bevy::scene::SceneInstanceReady>>()
.is_empty()
{
app.update();
}
app.update();
assert!(app.query_err::<&ColliderConstructorHierarchy>(hierarchy));
assert!(app.query_err::<&Collider>(hierarchy));
let densities: HashMap<_, _> = app
.world_mut()
.query::<(&Name, &ColliderDensity)>()
.iter(app.world())
.map(|(name, density)| (name.to_string(), density.0))
.collect();
assert_eq!(densities["eyes_mesh"], 1.0);
assert_eq!(densities["armL_mesh"], 2.0);
assert!(densities.get("armR_mesh").is_none());
let colliders: HashMap<_, _> = app
.world_mut()
.query::<(&Name, &Collider)>()
.iter(app.world())
.map(|(name, collider)| (name.to_string(), collider))
.collect();
assert_eq!(
colliders["eyes_mesh"].shape().shape_type(),
ShapeType::Compound
);
assert_eq!(
colliders["armL_mesh"].shape().shape_type(),
ShapeType::Capsule
);
assert!(colliders.get("armR_mesh").is_none());
}
const PRIMITIVE_COLLIDER: ColliderConstructor = ColliderConstructor::Capsule {
height: 1.0,
radius: 0.5,
};
#[cfg(feature = "collider-from-mesh")]
const COMPUTED_COLLIDER: ColliderConstructor = ColliderConstructor::TrimeshFromMesh;
fn create_test_app() -> App {
let mut app = App::new();
app.add_plugins((
MinimalPlugins,
AssetPlugin::default(),
#[cfg(feature = "bevy_scene")]
ScenePlugin,
HierarchyPlugin,
PhysicsPlugins::default(),
))
.init_resource::<Assets<Mesh>>();
app
}
#[cfg(all(feature = "collider-from-mesh", feature = "bevy_scene"))]
fn create_gltf_test_app() -> App {
use bevy::{diagnostic::DiagnosticsPlugin, winit::WinitPlugin};
let mut app = App::new();
app.add_plugins((
DefaultPlugins
.build()
.disable::<WinitPlugin>()
.disable::<DiagnosticsPlugin>(),
PhysicsPlugins::default(),
));
app.finish();
app.cleanup();
app
}
trait AppExt {
fn query_ok<D: QueryData>(&mut self, entity: Entity) -> bool;
fn query_err<D: QueryData>(&mut self, entity: Entity) -> bool {
!self.query_ok::<D>(entity)
}
#[cfg(feature = "collider-from-mesh")]
fn add_mesh(&mut self) -> Handle<Mesh>;
}
impl AppExt for App {
fn query_ok<D: QueryData>(&mut self, entity: Entity) -> bool {
let mut query = self.world_mut().query::<D>();
let component = query.get(self.world(), entity);
component.is_ok()
}
#[cfg(feature = "collider-from-mesh")]
fn add_mesh(&mut self) -> Handle<Mesh> {
self.world_mut()
.get_resource_mut::<Assets<Mesh>>()
.unwrap()
.add(Mesh::from(Cuboid::default()))
}
}
}