1use crate::prelude::*;
2use bevy::{platform::collections::HashMap, prelude::*};
3
4#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
32#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
33#[cfg_attr(
39 feature = "2d",
40 doc = " // Spawn the scene and automatically generate circle colliders"
41)]
42#[cfg_attr(
43 feature = "3d",
44 doc = " // Spawn the scene and automatically generate triangle mesh colliders"
45)]
46#[cfg_attr(
49 feature = "2d",
50 doc = " ColliderConstructorHierarchy::new(ColliderConstructor::Circle { radius: 2.0 }),"
51)]
52#[cfg_attr(
53 feature = "3d",
54 doc = " ColliderConstructorHierarchy::new(ColliderConstructor::TrimeshFromMesh),"
55)]
56#[cfg_attr(
62 feature = "2d",
63 doc = " ColliderConstructorHierarchy::new(ColliderConstructor::Circle { radius: 2.0 })
64 .with_constructor_for_name(\"Tree\", ColliderConstructor::Rectangle { x_length: 1.0, y_length: 2.0 })"
65)]
66#[cfg_attr(
67 feature = "3d",
68 doc = " ColliderConstructorHierarchy::new(ColliderConstructor::TrimeshFromMesh)
69 .with_constructor_for_name(\"Tree\", ColliderConstructor::ConvexHullFromMesh)"
70)]
71#[cfg_attr(
80 feature = "2d",
81 doc = " .with_constructor_for_name(\"Tree\", ColliderConstructor::Circle { radius: 2.0 }),"
82)]
83#[cfg_attr(
84 feature = "3d",
85 doc = " .with_constructor_for_name(\"Tree\", ColliderConstructor::ConvexHullFromMesh),"
86)]
87#[cfg_attr(
93 feature = "2d",
94 doc = " ColliderConstructorHierarchy::new(ColliderConstructor::Circle { radius: 2.0 })
95 .without_constructor_for_name(\"Tree\"),"
96)]
97#[cfg_attr(
98 feature = "3d",
99 doc = " ColliderConstructorHierarchy::new(ColliderConstructor::TrimeshFromMeshWithConfig(
100 TrimeshFlags::MERGE_DUPLICATE_VERTICES
101 ))
102 .without_constructor_for_name(\"Tree\"),"
103)]
104#[derive(Component, Clone, Debug, Default, PartialEq, Reflect)]
108#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
109#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
110#[reflect(Component, Debug, PartialEq, Default)]
111pub struct ColliderConstructorHierarchy {
112 pub default_constructor: Option<ColliderConstructor>,
115 pub default_layers: CollisionLayers,
119 pub default_density: ColliderDensity,
123 pub config: HashMap<String, Option<ColliderConstructorHierarchyConfig>>,
129}
130
131impl ColliderConstructorHierarchy {
132 pub fn new(default_constructor: impl Into<Option<ColliderConstructor>>) -> Self {
141 Self {
142 default_constructor: default_constructor.into(),
143 default_layers: CollisionLayers::default(),
144 default_density: ColliderDensity(1.0),
145 config: default(),
146 }
147 }
148
149 pub fn with_default_layers(mut self, layers: CollisionLayers) -> Self {
151 self.default_layers = layers;
152 self
153 }
154
155 pub fn with_default_density(mut self, density: impl Into<ColliderDensity>) -> Self {
157 self.default_density = density.into();
158 self
159 }
160
161 pub fn with_constructor_for_name(
163 mut self,
164 name: &str,
165 constructor: ColliderConstructor,
166 ) -> Self {
167 if let Some(Some(data)) = self.config.get_mut(name) {
168 data.constructor = Some(constructor);
169 } else {
170 self.config.insert(
171 name.to_string(),
172 Some(ColliderConstructorHierarchyConfig {
173 constructor: Some(constructor),
174 ..default()
175 }),
176 );
177 }
178 self
179 }
180
181 pub fn with_layers_for_name(self, name: &str, layers: CollisionLayers) -> Self {
183 self.with_config_for_name(name, |config| config.layers = Some(layers))
184 }
185
186 pub fn with_density_for_name(self, name: &str, density: impl Into<ColliderDensity>) -> Self {
188 let density = density.into();
189 self.with_config_for_name(name, |config| config.density = Some(density))
190 }
191
192 pub fn without_constructor_for_name(mut self, name: &str) -> Self {
195 self.config.insert(name.to_string(), None);
196 self
197 }
198
199 fn with_config_for_name(
200 mut self,
201 name: &str,
202 mut mutate_config: impl FnMut(&mut ColliderConstructorHierarchyConfig),
203 ) -> Self {
204 if let Some(Some(config)) = self.config.get_mut(name) {
205 mutate_config(config);
206 } else {
207 let mut config = ColliderConstructorHierarchyConfig::default();
208 mutate_config(&mut config);
209 self.config.insert(name.to_string(), Some(config));
210 }
211 self
212 }
213}
214
215#[derive(Clone, Debug, Default, PartialEq, Reflect)]
217#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
218#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
219#[reflect(Debug, Default, PartialEq)]
220pub struct ColliderConstructorHierarchyConfig {
221 pub constructor: Option<ColliderConstructor>,
225 pub layers: Option<CollisionLayers>,
229 pub density: Option<ColliderDensity>,
233}
234
235#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
255#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
256#[cfg_attr(feature = "2d", doc = " // Spawn a circle with radius 2")]
260#[cfg_attr(
261 feature = "3d",
262 doc = " // Spawn a cube with a convex hull collider generated from the mesh"
263)]
264#[cfg_attr(
266 feature = "2d",
267 doc = " ColliderConstructor::Circle { radius: 2.0 },"
268)]
269#[cfg_attr(
270 feature = "3d",
271 doc = " ColliderConstructor::ConvexHullFromMesh,"
272)]
273#[derive(Clone, Debug, PartialEq, Reflect, Component)]
278#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
279#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
280#[cfg_attr(feature = "collider-from-mesh", derive(Default))]
281#[cfg_attr(feature = "collider-from-mesh", reflect(Default))]
282#[reflect(Debug, Component, PartialEq)]
283#[non_exhaustive]
284#[allow(missing_docs)]
285pub enum ColliderConstructor {
286 #[cfg(feature = "2d")]
288 Circle { radius: Scalar },
289 #[cfg(feature = "3d")]
291 Sphere { radius: Scalar },
292 #[cfg(feature = "2d")]
294 Ellipse {
295 half_width: Scalar,
296 half_height: Scalar,
297 },
298 #[cfg(feature = "2d")]
300 Rectangle { x_length: Scalar, y_length: Scalar },
301 #[cfg(feature = "3d")]
303 Cuboid {
304 x_length: Scalar,
305 y_length: Scalar,
306 z_length: Scalar,
307 },
308 #[cfg(feature = "2d")]
310 RoundRectangle {
311 x_length: Scalar,
312 y_length: Scalar,
313 border_radius: Scalar,
314 },
315 #[cfg(feature = "3d")]
317 RoundCuboid {
318 x_length: Scalar,
319 y_length: Scalar,
320 z_length: Scalar,
321 border_radius: Scalar,
322 },
323 #[cfg(feature = "3d")]
325 Cylinder { radius: Scalar, height: Scalar },
326 #[cfg(feature = "3d")]
328 Cone { radius: Scalar, height: Scalar },
329 Capsule { radius: Scalar, height: Scalar },
331 CapsuleEndpoints {
333 radius: Scalar,
334 a: Vector,
335 b: Vector,
336 },
337 HalfSpace { outward_normal: Vector },
339 Segment { a: Vector, b: Vector },
341 Triangle { a: Vector, b: Vector, c: Vector },
343 #[cfg(feature = "2d")]
345 RegularPolygon { circumradius: f32, sides: u32 },
346 Polyline {
348 vertices: Vec<Vector>,
349 indices: Option<Vec<[u32; 2]>>,
350 },
351 Trimesh {
353 vertices: Vec<Vector>,
354 indices: Vec<[u32; 3]>,
355 },
356 TrimeshWithConfig {
358 vertices: Vec<Vector>,
359 indices: Vec<[u32; 3]>,
360 flags: TrimeshFlags,
361 },
362 #[cfg(feature = "2d")]
364 ConvexDecomposition {
365 vertices: Vec<Vector>,
366 indices: Vec<[u32; 2]>,
367 },
368 #[cfg(feature = "3d")]
370 ConvexDecomposition {
371 vertices: Vec<Vector>,
372 indices: Vec<[u32; 3]>,
373 },
374 #[cfg(feature = "2d")]
376 ConvexDecompositionWithConfig {
377 vertices: Vec<Vector>,
378 indices: Vec<[u32; 2]>,
379 params: VhacdParameters,
380 },
381 #[cfg(feature = "3d")]
383 ConvexDecompositionWithConfig {
384 vertices: Vec<Vector>,
385 indices: Vec<[u32; 3]>,
386 params: VhacdParameters,
387 },
388 #[cfg(feature = "2d")]
390 ConvexHull { points: Vec<Vector> },
391 #[cfg(feature = "3d")]
393 ConvexHull { points: Vec<Vector> },
394 #[cfg(feature = "2d")]
396 Heightfield { heights: Vec<Scalar>, scale: Vector },
397 #[cfg(feature = "3d")]
399 Heightfield {
400 heights: Vec<Vec<Scalar>>,
401 scale: Vector,
402 },
403 #[cfg(feature = "collider-from-mesh")]
405 #[default]
406 TrimeshFromMesh,
407 #[cfg(all(
409 feature = "3d",
410 feature = "collider-from-mesh",
411 feature = "default-collider"
412 ))]
413 TrimeshFromMeshWithConfig(TrimeshFlags),
414 #[cfg(feature = "collider-from-mesh")]
416 ConvexDecompositionFromMesh,
417 #[cfg(all(
419 feature = "3d",
420 feature = "collider-from-mesh",
421 feature = "default-collider"
422 ))]
423 ConvexDecompositionFromMeshWithConfig(VhacdParameters),
424 #[cfg(feature = "collider-from-mesh")]
426 ConvexHullFromMesh,
427}
428
429impl ColliderConstructor {
430 #[cfg(feature = "collider-from-mesh")]
432 pub fn requires_mesh(&self) -> bool {
433 matches!(
434 self,
435 Self::TrimeshFromMesh
436 | Self::TrimeshFromMeshWithConfig(_)
437 | Self::ConvexDecompositionFromMesh
438 | Self::ConvexDecompositionFromMeshWithConfig(_)
439 | Self::ConvexHullFromMesh
440 )
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447 use bevy::ecs::query::QueryData;
448 #[cfg(feature = "bevy_scene")]
449 use bevy::scene::ScenePlugin;
450
451 #[test]
452 fn collider_constructor_requires_no_mesh_on_primitive() {
453 let mut app = create_test_app();
454
455 let entity = app.world_mut().spawn(PRIMITIVE_COLLIDER.clone()).id();
456
457 app.update();
458
459 assert!(app.query_ok::<&Collider>(entity));
460 assert!(app.query_err::<&ColliderConstructor>(entity));
461 }
462
463 #[cfg(feature = "collider-from-mesh")]
464 #[test]
465 #[should_panic]
466 fn collider_constructor_requires_mesh_on_computed() {
467 let mut app = create_test_app();
468
469 app.world_mut().spawn(COMPUTED_COLLIDER.clone());
470
471 app.update();
472 }
473
474 #[cfg(feature = "collider-from-mesh")]
475 #[test]
476 fn collider_constructor_converts_mesh_on_computed() {
477 let mut app = create_test_app();
478
479 let mesh = app.add_mesh();
480 let entity = app
481 .world_mut()
482 .spawn((COMPUTED_COLLIDER.clone(), Mesh3d(mesh)))
483 .id();
484
485 app.update();
486
487 assert!(app.query_ok::<&Collider>(entity));
488 assert!(app.query_ok::<&Mesh3d>(entity));
489 assert!(app.query_err::<&ColliderConstructor>(entity));
490 }
491
492 #[test]
493 fn collider_constructor_hierarchy_does_nothing_on_self_with_primitive() {
494 let mut app = create_test_app();
495
496 let entity = app
497 .world_mut()
498 .spawn(ColliderConstructorHierarchy::new(
499 PRIMITIVE_COLLIDER.clone(),
500 ))
501 .id();
502
503 app.update();
504
505 assert!(app.query_err::<&ColliderConstructorHierarchy>(entity));
506 assert!(app.query_err::<&Collider>(entity));
507 }
508
509 #[cfg(feature = "collider-from-mesh")]
510 #[test]
511 fn collider_constructor_hierarchy_does_nothing_on_self_with_computed() {
512 let mut app = create_test_app();
513
514 let mesh = app.add_mesh();
515 let entity = app
516 .world_mut()
517 .spawn((
518 ColliderConstructorHierarchy::new(COMPUTED_COLLIDER.clone()),
519 Mesh3d(mesh),
520 ))
521 .id();
522
523 app.update();
524
525 assert!(app.query_ok::<&Mesh3d>(entity));
526 assert!(app.query_err::<&ColliderConstructorHierarchy>(entity));
527 assert!(app.query_err::<&Collider>(entity));
528 }
529
530 #[cfg(feature = "collider-from-mesh")]
531 #[test]
532 fn collider_constructor_hierarchy_does_not_require_mesh_on_self_with_computed() {
533 let mut app = create_test_app();
534
535 let entity = app
536 .world_mut()
537 .spawn(ColliderConstructorHierarchy::new(COMPUTED_COLLIDER.clone()))
538 .id();
539
540 app.update();
541
542 assert!(app.query_err::<&Collider>(entity));
543 assert!(app.query_err::<&ColliderConstructorHierarchy>(entity));
544 }
545
546 #[test]
547 fn collider_constructor_hierarchy_inserts_primitive_colliders_on_all_descendants() {
548 let mut app = create_test_app();
549
550 let parent = app
557 .world_mut()
558 .spawn(ColliderConstructorHierarchy::new(
559 PRIMITIVE_COLLIDER.clone(),
560 ))
561 .id();
562 let child1 = app.world_mut().spawn(()).id();
563 let child2 = app.world_mut().spawn(()).id();
564 let child3 = app.world_mut().spawn(()).id();
565
566 app.world_mut()
567 .entity_mut(parent)
568 .add_children(&[child1, child2]);
569 app.world_mut().entity_mut(child2).add_children(&[child3]);
570
571 app.update();
572
573 assert!(app.query_err::<&ColliderConstructorHierarchy>(parent));
575 assert!(app.query_err::<&ColliderConstructorHierarchy>(child1));
576 assert!(app.query_err::<&ColliderConstructorHierarchy>(child2));
577 assert!(app.query_err::<&ColliderConstructorHierarchy>(child3));
578
579 assert!(app.query_err::<&Collider>(parent));
580 assert!(app.query_ok::<&Collider>(child1));
581 assert!(app.query_ok::<&Collider>(child2));
582 assert!(app.query_ok::<&Collider>(child3));
583 }
584
585 #[cfg(feature = "collider-from-mesh")]
586 #[test]
587 fn collider_constructor_hierarchy_inserts_computed_colliders_only_on_descendants_with_mesh() {
588 let mut app = create_test_app();
589 let mesh = Mesh3d(app.add_mesh());
590
591 let parent = app
603 .world_mut()
604 .spawn(ColliderConstructorHierarchy::new(COMPUTED_COLLIDER.clone()))
605 .id();
606 let child1 = app.world_mut().spawn(()).id();
607 let child2 = app.world_mut().spawn(()).id();
608 let child3 = app.world_mut().spawn(mesh.clone()).id();
609 let child4 = app.world_mut().spawn(mesh.clone()).id();
610 let child5 = app.world_mut().spawn(()).id();
611 let child6 = app.world_mut().spawn(mesh.clone()).id();
612 let child7 = app.world_mut().spawn(mesh.clone()).id();
613 let child8 = app.world_mut().spawn(mesh.clone()).id();
614
615 app.world_mut()
616 .entity_mut(parent)
617 .add_children(&[child1, child2, child4, child6, child7]);
618 app.world_mut().entity_mut(child2).add_child(child3);
619 app.world_mut().entity_mut(child4).add_child(child5);
620 app.world_mut().entity_mut(child7).add_child(child8);
621
622 app.update();
623
624 assert!(app.query_err::<&ColliderConstructorHierarchy>(parent));
626 assert!(app.query_err::<&ColliderConstructorHierarchy>(child1));
627 assert!(app.query_err::<&ColliderConstructorHierarchy>(child2));
628 assert!(app.query_err::<&ColliderConstructorHierarchy>(child3));
629 assert!(app.query_err::<&ColliderConstructorHierarchy>(child4));
630 assert!(app.query_err::<&ColliderConstructorHierarchy>(child5));
631 assert!(app.query_err::<&ColliderConstructorHierarchy>(child6));
632 assert!(app.query_err::<&ColliderConstructorHierarchy>(child7));
633 assert!(app.query_err::<&ColliderConstructorHierarchy>(child8));
634
635 assert!(app.query_err::<&Collider>(parent));
636 assert!(app.query_err::<&Collider>(child1));
637 assert!(app.query_err::<&Collider>(child2));
638 assert!(app.query_ok::<&Collider>(child3));
639 assert!(app.query_ok::<&Collider>(child4));
640 assert!(app.query_err::<&Collider>(child5));
641 assert!(app.query_ok::<&Collider>(child6));
642 assert!(app.query_ok::<&Collider>(child7));
643 assert!(app.query_ok::<&Collider>(child8));
644 }
645
646 #[cfg(all(feature = "collider-from-mesh", feature = "bevy_scene"))]
647 #[test]
648 fn collider_constructor_hierarchy_inserts_correct_configs_on_scene() {
649 use parry::shape::ShapeType;
650
651 #[derive(Resource)]
652 struct SceneReady;
653
654 let mut app = create_gltf_test_app();
655
656 app.add_observer(
657 |_trigger: Trigger<bevy::scene::SceneInstanceReady>, mut commands: Commands| {
658 commands.insert_resource(SceneReady);
659 },
660 );
661
662 let scene_handle = app
663 .world_mut()
664 .resource_mut::<AssetServer>()
665 .load("ferris.glb#Scene0");
666
667 let hierarchy = app
668 .world_mut()
669 .spawn((
670 SceneRoot(scene_handle),
671 ColliderConstructorHierarchy::new(ColliderConstructor::ConvexDecompositionFromMesh)
672 .with_constructor_for_name("armL_mesh", PRIMITIVE_COLLIDER)
674 .with_density_for_name("armL_mesh", 2.0)
675 .without_constructor_for_name("armR_mesh"),
677 RigidBody::Dynamic,
678 ))
679 .id();
680
681 for _ in 0..1000 {
682 if app.world().contains_resource::<SceneReady>() {
683 break;
684 }
685 app.update();
686 }
687 app.update();
688
689 assert!(app.query_err::<&ColliderConstructorHierarchy>(hierarchy));
690 assert!(app.query_err::<&Collider>(hierarchy));
691
692 let densities: HashMap<_, _> = app
694 .world_mut()
695 .query::<(&Name, &ColliderDensity)>()
696 .iter(app.world())
697 .map(|(name, density)| (name.to_string(), density.0))
698 .collect();
699
700 assert_eq!(densities["eyes_mesh"], 1.0);
701 assert_eq!(densities["armL_mesh"], 2.0);
702 assert!(densities.get("armR_mesh").is_none());
703
704 let colliders: HashMap<_, _> = app
706 .world_mut()
707 .query::<(&Name, &Collider)>()
708 .iter(app.world())
709 .map(|(name, collider)| (name.to_string(), collider))
710 .collect();
711
712 assert_eq!(
713 colliders["eyes_mesh"].shape().shape_type(),
714 ShapeType::Compound
715 );
716 assert_eq!(
717 colliders["armL_mesh"].shape().shape_type(),
718 ShapeType::Capsule
719 );
720 assert!(colliders.get("armR_mesh").is_none());
721 }
722
723 const PRIMITIVE_COLLIDER: ColliderConstructor = ColliderConstructor::Capsule {
724 height: 1.0,
725 radius: 0.5,
726 };
727
728 #[cfg(feature = "collider-from-mesh")]
729 const COMPUTED_COLLIDER: ColliderConstructor = ColliderConstructor::TrimeshFromMesh;
730
731 fn create_test_app() -> App {
732 let mut app = App::new();
733 app.add_plugins((
734 MinimalPlugins,
735 AssetPlugin::default(),
736 #[cfg(feature = "bevy_scene")]
737 ScenePlugin,
738 PhysicsPlugins::default(),
739 ))
740 .init_resource::<Assets<Mesh>>();
741
742 app
743 }
744
745 #[cfg(all(feature = "collider-from-mesh", feature = "bevy_scene"))]
746 fn create_gltf_test_app() -> App {
747 use bevy::{diagnostic::DiagnosticsPlugin, winit::WinitPlugin};
748
749 let mut app = App::new();
750 app.add_plugins((
751 DefaultPlugins
752 .build()
753 .disable::<WinitPlugin>()
754 .disable::<DiagnosticsPlugin>(),
755 PhysicsPlugins::default(),
756 ));
757 app.finish();
758 app.cleanup();
759 app
760 }
761
762 trait AppExt {
763 fn query_ok<D: QueryData>(&mut self, entity: Entity) -> bool;
764 fn query_err<D: QueryData>(&mut self, entity: Entity) -> bool {
765 !self.query_ok::<D>(entity)
766 }
767
768 #[cfg(feature = "collider-from-mesh")]
769 fn add_mesh(&mut self) -> Handle<Mesh>;
770 }
771
772 impl AppExt for App {
773 fn query_ok<D: QueryData>(&mut self, entity: Entity) -> bool {
774 let mut query = self.world_mut().query::<D>();
775 let component = query.get(self.world(), entity);
776 component.is_ok()
777 }
778
779 #[cfg(feature = "collider-from-mesh")]
780 fn add_mesh(&mut self) -> Handle<Mesh> {
781 self.world_mut()
782 .get_resource_mut::<Assets<Mesh>>()
783 .unwrap()
784 .add(Mesh::from(Cuboid::default()))
785 }
786 }
787}