diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00d92557..5622bb69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,9 +29,11 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.78 - name: Run cargo test - run: cargo test --no-default-features --features enhanced-determinism,collider-from-mesh,serialize,bevy_xpbd_2d/2d,bevy_xpbd_3d/3d,bevy_xpbd_2d/f64,bevy_xpbd_3d/f64,default-collider,parry-f64 + run: cargo test --no-default-features --features enhanced-determinism,collider-from-mesh,serialize,bevy_xpbd_2d/2d,bevy_xpbd_3d/3d,bevy_xpbd_2d/f64,bevy_xpbd_3d/f64,default-collider,parry-f64,bevy_scene lints: name: Lints diff --git a/crates/bevy_xpbd_2d/Cargo.toml b/crates/bevy_xpbd_2d/Cargo.toml index de02e986..f2139b9d 100644 --- a/crates/bevy_xpbd_2d/Cargo.toml +++ b/crates/bevy_xpbd_2d/Cargo.toml @@ -12,7 +12,14 @@ keywords = ["gamedev", "physics", "simulation", "xpbd", "bevy"] categories = ["game-development", "science", "simulation"] [features] -default = ["2d", "f32", "parry-f32", "debug-plugin", "parallel"] +default = [ + "2d", + "f32", + "parry-f32", + "debug-plugin", + "parallel", + "bevy_scene", +] 2d = [] f32 = [] f64 = [] @@ -32,6 +39,7 @@ default-collider = ["dep:nalgebra"] parry-f32 = ["f32", "dep:parry2d", "default-collider"] parry-f64 = ["f64", "dep:parry2d-f64", "default-collider"] +bevy_scene = ["bevy/bevy_scene"] serialize = [ "dep:serde", "bevy/serialize", @@ -56,6 +64,7 @@ derive_more = "0.99" indexmap = "2.0.0" fxhash = "0.2.1" itertools = "0.12" +bitflags = "2.5.0" [dev-dependencies] examples_common_2d = { path = "../examples_common_2d" } diff --git a/crates/bevy_xpbd_3d/Cargo.toml b/crates/bevy_xpbd_3d/Cargo.toml index 9685ac44..41c8c1b4 100644 --- a/crates/bevy_xpbd_3d/Cargo.toml +++ b/crates/bevy_xpbd_3d/Cargo.toml @@ -16,7 +16,8 @@ default = [ "3d", "f32", "parry-f32", - "async-collider", + "collider-from-mesh", + "bevy_scene", "debug-plugin", "parallel", ] @@ -39,8 +40,8 @@ default-collider = ["dep:nalgebra"] parry-f32 = ["f32", "dep:parry3d", "default-collider"] parry-f64 = ["f64", "dep:parry3d-f64", "default-collider"] -collider-from-mesh = ["bevy/bevy_render"] -async-collider = ["bevy/bevy_scene", "bevy/bevy_gltf", "collider-from-mesh"] +collider-from-mesh = ["bevy/bevy_render", "3d"] +bevy_scene = ["bevy/bevy_scene"] serialize = [ "dep:serde", "bevy/serialize", @@ -65,8 +66,10 @@ derive_more = "0.99" indexmap = "2.0.0" fxhash = "0.2.1" itertools = "0.12" +bitflags = "2.5.0" [dev-dependencies] +bevy = { version = "0.13", default-features = false, features = ["bevy_gltf"] } examples_common_3d = { path = "../examples_common_3d" } benches_common_3d = { path = "../benches_common_3d" } bevy_math = { version = "0.13", features = ["approx"] } @@ -76,11 +79,11 @@ insta = "1.0" [[example]] name = "dynamic_character_3d" -required-features = ["3d", "default-collider", "async-collider"] +required-features = ["3d", "default-collider", "bevy_scene"] [[example]] name = "kinematic_character_3d" -required-features = ["3d", "default-collider", "async-collider"] +required-features = ["3d", "default-collider", "bevy_scene"] [[example]] name = "cast_ray_predicate" @@ -120,11 +123,11 @@ required-features = ["3d", "default-collider"] [[example]] name = "trimesh_shapes_3d" -required-features = ["3d", "default-collider"] +required-features = ["3d", "default-collider", "bevy_scene"] [[example]] -name = "async_colliders" -required-features = ["3d", "default-collider", "async-collider"] +name = "collider_constructors" +required-features = ["3d", "default-collider", "bevy_scene"] [[bench]] name = "cubes" diff --git a/crates/bevy_xpbd_3d/examples/async_colliders.rs b/crates/bevy_xpbd_3d/examples/collider_constructors.rs similarity index 77% rename from crates/bevy_xpbd_3d/examples/async_colliders.rs rename to crates/bevy_xpbd_3d/examples/collider_constructors.rs index d2c0e55b..273cbb2c 100644 --- a/crates/bevy_xpbd_3d/examples/async_colliders.rs +++ b/crates/bevy_xpbd_3d/examples/collider_constructors.rs @@ -1,5 +1,5 @@ //! An example showcasing how to create colliders for meshes and scenes -//! using `AsyncCollider` and `AsyncSceneCollider` respectively. +//! using `ColliderConstructor` and `ColliderConstructorHierarchy` respectively. use bevy::prelude::*; use bevy_xpbd_3d::prelude::*; @@ -18,18 +18,18 @@ fn setup( mut meshes: ResMut>, assets: ResMut, ) { - // Spawn ground and generate a collider for the mesh using AsyncCollider + // Spawn ground and generate a collider for the mesh using ColliderConstructor commands.spawn(( PbrBundle { mesh: meshes.add(Plane3d::default().mesh().size(8.0, 8.0)), material: materials.add(Color::rgb(0.3, 0.5, 0.3)), ..default() }, - AsyncCollider(ComputedCollider::TriMesh), + ColliderConstructor::TrimeshFromMesh, RigidBody::Static, )); - // Spawn Ferris the crab and generate colliders for the scene using AsyncSceneCollider + // Spawn Ferris the crab and generate colliders for the scene using ColliderConstructorHierarchy commands.spawn(( SceneBundle { // The model was made by RayMarch, licenced under CC0-1.0, and can be found here: @@ -41,12 +41,10 @@ fn setup( // Create colliders using convex decomposition. // This takes longer than creating a trimesh or convex hull collider, // but is more performant for collision detection. - AsyncSceneCollider::new(Some(ComputedCollider::ConvexDecomposition( - VHACDParameters::default(), - ))) - // Make the arms heavier to make it easier to stand upright - .with_density_for_name("armL_mesh", 3.0) - .with_density_for_name("armR_mesh", 3.0), + ColliderConstructorHierarchy::new(ColliderConstructor::ConvexDecompositionFromMesh) + // Make the arms heavier to make it easier to stand upright + .with_density_for_name("armL_mesh", 3.0) + .with_density_for_name("armR_mesh", 3.0), RigidBody::Dynamic, )); diff --git a/crates/bevy_xpbd_3d/examples/dynamic_character_3d/main.rs b/crates/bevy_xpbd_3d/examples/dynamic_character_3d/main.rs index 7313eee9..11b73036 100644 --- a/crates/bevy_xpbd_3d/examples/dynamic_character_3d/main.rs +++ b/crates/bevy_xpbd_3d/examples/dynamic_character_3d/main.rs @@ -65,14 +65,14 @@ fn setup( }, )); - // Environment (see `async_colliders` example for creating colliders from scenes) + // Environment (see the `collider_constructors` example for creating colliders from scenes) commands.spawn(( SceneBundle { scene: assets.load("character_controller_demo.glb#Scene0"), transform: Transform::from_rotation(Quat::from_rotation_y(-std::f32::consts::PI * 0.5)), ..default() }, - AsyncSceneCollider::new(Some(ComputedCollider::ConvexHull)), + ColliderConstructorHierarchy::new(ColliderConstructor::ConvexHullFromMesh), RigidBody::Static, )); diff --git a/crates/bevy_xpbd_3d/examples/kinematic_character_3d/main.rs b/crates/bevy_xpbd_3d/examples/kinematic_character_3d/main.rs index 74851eda..fcc02b0c 100644 --- a/crates/bevy_xpbd_3d/examples/kinematic_character_3d/main.rs +++ b/crates/bevy_xpbd_3d/examples/kinematic_character_3d/main.rs @@ -59,14 +59,14 @@ fn setup( }, )); - // Environment (see `async_colliders` example for creating colliders from scenes) + // Environment (see the `collider_constructors` example for creating colliders from scenes) commands.spawn(( SceneBundle { scene: assets.load("character_controller_demo.glb#Scene0"), transform: Transform::from_rotation(Quat::from_rotation_y(-std::f32::consts::PI * 0.5)), ..default() }, - AsyncSceneCollider::new(Some(ComputedCollider::ConvexHull)), + ColliderConstructorHierarchy::new(ColliderConstructor::ConvexHullFromMesh), RigidBody::Static, )); diff --git a/crates/examples_common_3d/Cargo.toml b/crates/examples_common_3d/Cargo.toml index 04711867..30d50450 100644 --- a/crates/examples_common_3d/Cargo.toml +++ b/crates/examples_common_3d/Cargo.toml @@ -16,6 +16,7 @@ bevy = { version = "0.13", default-features = false, features = [ "bevy_sprite", "bevy_pbr", "bevy_gizmos", + "bevy_gltf", "default_font", "tonemapping_luts", "ktx2", diff --git a/src/components/mass_properties.rs b/src/components/mass_properties.rs index 1e6cff03..8043bb7f 100644 --- a/src/components/mass_properties.rs +++ b/src/components/mass_properties.rs @@ -290,6 +290,12 @@ impl Default for ColliderDensity { } } +impl From for ColliderDensity { + fn from(density: Scalar) -> Self { + Self(density) + } +} + /// An automatically added component that contains the read-only mass properties of a [`Collider`]. /// The density used for computing the mass properties can be configured using the [`ColliderDensity`] /// component. diff --git a/src/lib.rs b/src/lib.rs index 057e75d4..b258b2f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(unexpected_cfgs, clippy::type_complexity, clippy::too_many_arguments)] //! # Bevy XPBD //! //! **Bevy XPBD** is a 2D and 3D physics engine based on @@ -59,10 +60,7 @@ feature = "3d", doc = "| `collider-from-mesh` | Allows you to create [`Collider`]s from `Mesh`es. | Yes |" )] -#![cfg_attr( - feature = "3d", - doc = "| `async-collider` | Allows you to generate [`Collider`]s from mesh handles and scenes. | Yes |" -)] +//! | `bevy_scene` | Enables [`ColliderConstructorHierarchy`] to wait until a [`Scene`] has loaded before processing it. | Yes | //! | `debug-plugin` | Enables physics debug rendering using the [`PhysicsDebugPlugin`]. The plugin must be added separately. | Yes | //! | `enhanced-determinism` | Enables increased determinism. | No | //! | `parallel` | Enables some extra multithreading, which improves performance for larger simulations but can add some overhead for smaller ones. | Yes | @@ -144,7 +142,7 @@ //! - [Sensors](Sensor) #![cfg_attr( feature = "3d", - doc = " - Creating colliders from meshes with [`AsyncCollider`] and [`AsyncSceneCollider`]" + doc = " - Creating colliders from meshes with [`ColliderConstructor`] and [`ColliderConstructorHierarchy`]" )] //! - [Get colliding entities](CollidingEntities) //! - [Collision events](ContactReportingPlugin#collision-events) diff --git a/src/plugins/collision/collider/mod.rs b/src/plugins/collision/collider/mod.rs index 561ed73b..766d9889 100644 --- a/src/plugins/collision/collider/mod.rs +++ b/src/plugins/collision/collider/mod.rs @@ -1,5 +1,4 @@ use crate::prelude::*; -#[cfg(all(feature = "3d", feature = "async-collider"))] use bevy::utils::HashMap; use bevy::{ ecs::entity::{EntityMapper, MapEntities}, @@ -92,54 +91,74 @@ pub trait ScalableCollider: AnyCollider { } } -/// A component that will automatically generate a [`Collider`] based on the entity's `Mesh`. -/// The type of the generated collider can be specified using [`ComputedCollider`]. +/// A component that will automatically generate [`Collider`]s on its descendants at runtime. +/// The type of the generated collider can be specified using [`ColliderConstructor`]. +/// This supports computing the shape dynamically from the mesh, in which case only the descendants +/// with a [`Mesh`] will have colliders generated. /// -/// ## Example +/// In contrast to [`ColliderConstructor`], this component will *not* generate a collider on its own entity. /// -/// ``` -/// use bevy::prelude::*; -/// use bevy_xpbd_3d::prelude::*; +/// If this component is used on a scene, such as one spawned by a [`SceneBundle`], it will +/// wait until the scene is loaded before generating colliders. /// -/// fn setup(mut commands: Commands, mut assets: ResMut, mut meshes: Assets) { -/// // Spawn a cube with a convex hull collider generated from the mesh -/// commands.spawn(( -/// AsyncCollider(ComputedCollider::ConvexHull), -/// PbrBundle { -/// mesh: meshes.add(Mesh::from(Cuboid::default())), -/// ..default() -/// }, -/// )); -/// } -/// ``` -#[cfg(all(feature = "3d", feature = "async-collider"))] -#[derive(Component, Clone, Debug, Default, Deref, DerefMut)] -pub struct AsyncCollider(pub ComputedCollider); - -/// A component that will automatically generate colliders for the meshes in a scene -/// once the scene has been loaded. The type of the generated collider can be specified -/// using [`ComputedCollider`]. +/// The exact configuration for each descendant can be specified using the helper methods +/// such as [`with_constructor_for_name`](Self::with_constructor_for_name). +/// +/// This component will only override a pre-existing [`Collider`] component on a descendant entity +/// when it has been explicitly mentioned in the `config`. +/// +/// ## See also +/// +/// For inserting colliders on the same entity, use [`ColliderConstructor`]. +/// +/// ## Caveats +/// +/// When a component has multiple ancestors with [`ColliderConstructorHierarchy`], the insertion order is undefined. /// /// ## Example /// /// ``` /// use bevy::prelude::*; -/// use bevy_xpbd_3d::prelude::*; +#[cfg_attr(feature = "2d", doc = "use bevy_xpbd_2d::prelude::*;")] +#[cfg_attr(feature = "3d", doc = "use bevy_xpbd_3d::prelude::*;")] /// /// fn setup(mut commands: Commands, mut assets: ResMut) { /// let scene = assets.load("my_model.gltf#Scene0"); /// -/// // Spawn the scene and automatically generate triangle mesh colliders +#[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" +)] +/// /// commands.spawn(( /// SceneBundle { scene: scene.clone(), ..default() }, -/// AsyncSceneCollider::new(Some(ComputedCollider::TriMesh)), +#[cfg_attr( + feature = "2d", + doc = " ColliderConstructorHierarchy::new(ColliderConstructor::Circle { radius: 2.0 })," +)] +#[cfg_attr( + feature = "3d", + doc = " ColliderConstructorHierarchy::new(ColliderConstructor::TrimeshFromMesh)," +)] /// )); /// /// // Specify configuration for specific meshes by name /// commands.spawn(( /// SceneBundle { scene: scene.clone(), ..default() }, -/// AsyncSceneCollider::new(Some(ComputedCollider::TriMesh)) -/// .with_shape_for_name("Tree", ComputedCollider::ConvexHull) +#[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)" +)] /// .with_layers_for_name("Tree", CollisionLayers::from_bits(0b0010, 0b1111)) /// .with_density_for_name("Tree", 2.5), /// )); @@ -147,154 +166,382 @@ pub struct AsyncCollider(pub ComputedCollider); /// // Only generate colliders for specific meshes by name /// commands.spawn(( /// SceneBundle { scene: scene.clone(), ..default() }, -/// AsyncSceneCollider::new(None) -/// .with_shape_for_name("Tree", ComputedCollider::ConvexHull), +/// ColliderConstructorHierarchy::new(None) +#[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)," +)] /// )); /// /// // Generate colliders for everything except specific meshes by name /// commands.spawn(( /// SceneBundle { scene, ..default() }, -/// AsyncSceneCollider::new(Some(ComputedCollider::TriMeshWithFlags( -/// TriMeshFlags::MERGE_DUPLICATE_VERTICES -/// ))) -/// .without_shape_with_name("Tree"), +#[cfg_attr( + feature = "2d", + doc = " ColliderConstructorHierarchy::new(ColliderConstructor::Circle { radius: 2.0 })" +)] +#[cfg_attr( + feature = "3d", + doc = " ColliderConstructorHierarchy::new(ColliderConstructor::TrimeshFromMeshWithConfig( + TrimeshFlags::MERGE_DUPLICATE_VERTICES + ))" +)] +/// .without_constructor_for_name("Tree"), /// )); /// } /// ``` -#[cfg(all(feature = "3d", feature = "async-collider"))] -#[derive(Component, Clone, Debug, Default, PartialEq)] -pub struct AsyncSceneCollider { - /// The default collider type used for each mesh that isn't included in [`meshes_by_name`](#structfield.meshes_by_name). - /// If `None`, all meshes except the ones in [`meshes_by_name`](#structfield.meshes_by_name) will be skipped. - pub default_shape: Option, +#[derive(Component, Clone, Debug, Default, PartialEq, Reflect)] +#[reflect(Debug, Component, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))] +pub struct ColliderConstructorHierarchy { + /// The default collider type used for each mesh that isn't included in [`config`](#structfield.config). + /// If `None`, all meshes except the ones in [`config`](#structfield.config) will be skipped. + pub default_constructor: Option, /// Specifies data like the collider type and [`CollisionLayers`] for meshes by name. /// Entries with a `None` value will be skipped. - /// For the meshes not found in this `HashMap`, [`default_shape`](#structfield.default_shape) + /// For the meshes not found in this `HashMap`, [`default_constructor`](#structfield.default_constructor) /// and all collision layers will be used instead. - pub meshes_by_name: HashMap>, + pub config: HashMap>, } -#[cfg(all(feature = "3d", feature = "async-collider"))] -impl AsyncSceneCollider { - /// Creates a new [`AsyncSceneCollider`] with the default collider type used for - /// meshes set to the given `default_shape`. +impl ColliderConstructorHierarchy { + /// Creates a new [`ColliderConstructorHierarchy`] with the default collider type used for + /// meshes set to the given `default_constructor`. /// /// If the given collider type is `None`, all meshes except the ones in - /// [`meshes_by_name`](#structfield.meshes_by_name) will be skipped. - /// You can add named shapes using [`with_shape_for_name`](Self::with_shape_for_name). - pub fn new(default_shape: Option) -> Self { + /// [`config`](#structfield.config) will be skipped. + /// You can add named shapes using [`with_constructor_for_name`](Self::with_constructor_for_name). + pub fn new(default_constructor: impl Into>) -> Self { Self { - default_shape, - meshes_by_name: default(), + default_constructor: default_constructor.into(), + config: default(), } } /// Specifies the collider type used for a mesh with the given `name`. - pub fn with_shape_for_name(mut self, name: &str, shape: ComputedCollider) -> Self { - if let Some(Some(data)) = self.meshes_by_name.get_mut(name) { - data.shape = shape; + pub fn with_constructor_for_name(mut self, name: &str, shape: ColliderConstructor) -> Self { + if let Some(Some(data)) = self.config.get_mut(name) { + data.constructor = shape; } else { - self.meshes_by_name.insert( + self.config.insert( name.to_string(), - Some(AsyncSceneColliderData { shape, ..default() }), + Some(ColliderConstructorHierarchyConfig::from_constructor(shape)), ); } self } /// Specifies the [`CollisionLayers`] used for a mesh with the given `name`. - pub fn with_layers_for_name(mut self, name: &str, layers: CollisionLayers) -> Self { - if let Some(Some(data)) = self.meshes_by_name.get_mut(name) { - data.layers = layers; - } else { - self.meshes_by_name.insert( - name.to_string(), - Some(AsyncSceneColliderData { - layers, - ..default() - }), - ); - } - self + pub fn with_layers_for_name(self, name: &str, layers: CollisionLayers) -> Self { + self.with_config_for_name(name, |config| config.layers = layers) } /// Specifies the [`ColliderDensity`] used for a mesh with the given `name`. - pub fn with_density_for_name(mut self, name: &str, density: Scalar) -> Self { - if let Some(Some(data)) = self.meshes_by_name.get_mut(name) { - data.density = density; - } else { - self.meshes_by_name.insert( - name.to_string(), - Some(AsyncSceneColliderData { - density, - ..default() - }), - ); - } - self + pub fn with_density_for_name(self, name: &str, density: impl Into) -> Self { + let density = density.into(); + self.with_config_for_name(name, |config| config.density = density) } /// Sets collider for the mesh associated with the given `name` to `None`, skipping /// collider generation for it. - pub fn without_shape_with_name(mut self, name: &str) -> Self { - self.meshes_by_name.insert(name.to_string(), None); + 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 = self.base_constructor_hierarchy_data().unwrap_or_else(|| + panic!("Failed to configure collider constructor for \"{name}\" because it has no associated constructor. \ + Either specify a default constructor by passing one to `ColliderConstructorHierarchy::new` or call `with_constructor_for_name` first, \ + or enable the `collider-from-mesh` feature to use a default construction method by reading the mesh.")); + mutate_config(&mut config); + self.config.insert(name.to_string(), Some(config)); + } self } + + fn base_constructor_hierarchy_data(&self) -> Option { + self.default_constructor + .clone() + .map(ColliderConstructorHierarchyConfig::from_constructor) + .or({ + #[cfg(all(feature = "3d", feature = "collider-from-mesh"))] + { + Some(ColliderConstructorHierarchyConfig::default()) + } + #[cfg(not(all(feature = "3d", feature = "collider-from-mesh")))] + { + None + } + }) + } } -/// Configuration for a specific collider generated from a scene using [`AsyncSceneCollider`]. -#[cfg(all(feature = "3d", feature = "async-collider"))] -#[derive(Clone, Debug, PartialEq)] -pub struct AsyncSceneColliderData { +/// Configuration for a specific collider generated from a scene using [`ColliderConstructorHierarchy`]. +#[derive(Clone, Debug, PartialEq, Reflect)] +#[reflect(Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))] +#[cfg_attr(all(feature = "3d", feature = "collider-from-mesh"), reflect(Default))] +pub struct ColliderConstructorHierarchyConfig { /// The type of collider generated for the mesh. - pub shape: ComputedCollider, + pub constructor: ColliderConstructor, /// The [`CollisionLayers`] used for this collider. pub layers: CollisionLayers, /// The [`ColliderDensity`] used for this collider. - pub density: Scalar, + pub density: ColliderDensity, } -#[cfg(all(feature = "3d", feature = "async-collider"))] -impl Default for AsyncSceneColliderData { - fn default() -> Self { +impl ColliderConstructorHierarchyConfig { + /// Creates a new [`ColliderConstructorHierarchyConfig`] with the given `constructor`, [`CollisionLayers`] set to belong and collide with everything, + /// and a density of [`ColliderConstructorHierarchyConfig::DEFAULT_DENSITY`]. + pub fn from_constructor(constructor: ColliderConstructor) -> Self { Self { - shape: ComputedCollider::TriMesh, + constructor, layers: CollisionLayers::default(), - density: 1.0, + density: Self::DEFAULT_DENSITY, } } + + /// Density used for generated colliders if not specified otherwise. + pub const DEFAULT_DENSITY: ColliderDensity = ColliderDensity(1.0); } -/// Determines how a [`Collider`] is generated from a `Mesh`. +#[cfg(all(feature = "3d", feature = "collider-from-mesh"))] +impl Default for ColliderConstructorHierarchyConfig { + fn default() -> Self { + Self::from_constructor(ColliderConstructor::TrimeshFromMesh) + } +} + +/// A component that will automatically generate a [`Collider`] at runtime using [`Collider::try_from_constructor`]. +/// Enabling the `collider-from-mesh` feature activates support for computing the shape dynamically from the mesh attached to the same entity. /// -/// Colliders can be created from meshes with the following components and methods: +/// Since [`Collider`] is not [`Reflect`], you can use this type to statically statically +/// specify a collider's shape instead. /// -/// - [`AsyncCollider`] (requires `async-collider` features) -/// - [`AsyncSceneCollider`] (requires `async-collider` features) -/// - [`Collider::trimesh_from_mesh`] -/// - [`Collider::convex_hull_from_mesh`] -/// - [`Collider::convex_decomposition_from_mesh`] -#[cfg(all(feature = "3d", feature = "collider-from-mesh"))] -#[derive(Component, Clone, Debug, Default, PartialEq)] -pub enum ComputedCollider { - /// A triangle mesh. +/// This component will never override a pre-existing [`Collider`] component on the same entity. +/// +/// ## See also +/// +/// For inserting colliders on an entity's descendants, use [`ColliderConstructorHierarchy`]. +/// +/// ## Panics +/// +/// The system handling the generation of colliders will panic if the specified [`ColliderConstructor`] +/// requires a mesh, but the entity does not have a `Handle` component. +/// +/// ## Example +/// +/// ``` +/// use bevy::prelude::*; +#[cfg_attr(feature = "2d", doc = "use bevy_xpbd_2d::prelude::*;")] +#[cfg_attr(feature = "3d", doc = "use bevy_xpbd_3d::prelude::*;")] +/// +/// fn setup(mut commands: Commands, mut assets: ResMut, mut meshes: Assets) { +#[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" +)] +/// commands.spawn(( +#[cfg_attr( + feature = "2d", + doc = " ColliderConstructor::Circle { radius: 2.0 }," +)] +#[cfg_attr( + feature = "3d", + doc = " ColliderConstructor::ConvexHullFromMesh," +)] +/// PbrBundle { +/// mesh: meshes.add(Mesh::from(Cuboid::default())), +/// ..default() +/// }, +/// )); +/// } +/// ``` +#[derive(Clone, Debug, PartialEq, Reflect, Component)] +#[reflect(Debug, Component, PartialEq)] +#[non_exhaustive] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))] +#[cfg_attr(all(feature = "3d", feature = "collider-from-mesh"), derive(Default))] +#[cfg_attr(all(feature = "3d", feature = "collider-from-mesh"), reflect(Default))] +#[allow(missing_docs)] +pub enum ColliderConstructor { + /// Constructs a collider with [`Collider::circle`]. + #[cfg(feature = "2d")] + Circle { radius: Scalar }, + /// Constructs a collider with [`Collider::sphere`]. + #[cfg(feature = "3d")] + Sphere { radius: Scalar }, + /// Constructs a collider with [`Collider::ellipse`]. + #[cfg(feature = "2d")] + Ellipse { + half_width: Scalar, + half_height: Scalar, + }, + /// Constructs a collider with [`Collider::rectangle`]. + #[cfg(feature = "2d")] + Rectangle { x_length: Scalar, y_length: Scalar }, + /// Constructs a collider with [`Collider::cuboid`]. + #[cfg(feature = "3d")] + Cuboid { + x_length: Scalar, + y_length: Scalar, + z_length: Scalar, + }, + /// Constructs a collider with [`Collider::round_rectangle`]. + #[cfg(feature = "2d")] + RoundRectangle { + x_length: Scalar, + y_length: Scalar, + border_radius: Scalar, + }, + /// Constructs a collider with [`Collider::round_cuboid`]. + #[cfg(feature = "3d")] + RoundCuboid { + x_length: Scalar, + y_length: Scalar, + z_length: Scalar, + border_radius: Scalar, + }, + /// Constructs a collider with [`Collider::cylinder`]. + #[cfg(feature = "3d")] + Cylinder { height: Scalar, radius: Scalar }, + /// Constructs a collider with [`Collider::cone`]. + #[cfg(feature = "3d")] + Cone { height: Scalar, radius: Scalar }, + /// Constructs a collider with [`Collider::capsule`]. + Capsule { height: Scalar, radius: Scalar }, + /// Constructs a collider with [`Collider::capsule_endpoints`]. + CapsuleEndpoints { + a: Vector, + b: Vector, + radius: Scalar, + }, + /// Constructs a collider with [`Collider::half_space`]. + HalfSpace { outward_normal: Vector }, + /// Constructs a collider with [`Collider::segment`]. + Segment { a: Vector, b: Vector }, + /// Constructs a collider with [`Collider::triangle`]. + Triangle { a: Vector, b: Vector, c: Vector }, + /// Constructs a collider with [`Collider::regular_polygon`]. + #[cfg(feature = "2d")] + RegularPolygon { circumradius: f32, sides: usize }, + /// Constructs a collider with [`Collider::polyline`]. + Polyline { + vertices: Vec, + indices: Option>, + }, + /// Constructs a collider with [`Collider::trimesh`]. + Trimesh { + vertices: Vec, + indices: Vec<[u32; 3]>, + }, + /// Constructs a collider with [`Collider::trimesh_with_config`]. + TrimeshWithConfig { + vertices: Vec, + indices: Vec<[u32; 3]>, + flags: TrimeshFlags, + }, + /// Constructs a collider with [`Collider::convex_decomposition`]. + #[cfg(feature = "2d")] + ConvexDecomposition { + vertices: Vec, + indices: Vec<[u32; 2]>, + }, + /// Constructs a collider with [`Collider::convex_decomposition`]. + #[cfg(feature = "3d")] + ConvexDecomposition { + vertices: Vec, + indices: Vec<[u32; 3]>, + }, + /// Constructs a collider with [`Collider::convex_decomposition_with_config`]. + #[cfg(feature = "2d")] + ConvexDecompositionWithConfig { + vertices: Vec, + indices: Vec<[u32; 2]>, + params: VhacdParameters, + }, + /// Constructs a collider with [`Collider::convex_decomposition_with_config`]. + #[cfg(feature = "3d")] + ConvexDecompositionWithConfig { + vertices: Vec, + indices: Vec<[u32; 3]>, + params: VhacdParameters, + }, + /// Constructs a collider with [`Collider::convex_hull`]. + #[cfg(feature = "2d")] + ConvexHull { points: Vec }, + /// Constructs a collider with [`Collider::convex_hull`]. + #[cfg(feature = "3d")] + ConvexHull { points: Vec }, + /// Constructs a collider with [`Collider::heightfield`]. + #[cfg(feature = "2d")] + Heightfield { heights: Vec, scale: Vector }, + /// Constructs a collider with [`Collider::heightfield`]. + #[cfg(feature = "3d")] + Heightfield { + heights: Vec>, + scale: Vector, + }, + /// Constructs a collider with [`Collider::trimesh_from_mesh`]. + #[cfg(all(feature = "3d", feature = "collider-from-mesh"))] #[default] - TriMesh, - /// A triangle mesh with a custom configuration. + TrimeshFromMesh, + /// Constructs a collider with [`Collider::trimesh_from_mesh_with_config`]. #[cfg(all( - feature = "default-collider", - any(feature = "parry-f32", feature = "parry-f64") + feature = "3d", + feature = "collider-from-mesh", + feature = "default-collider" ))] - TriMeshWithFlags(TriMeshFlags), - /// A convex hull. - ConvexHull, - /// A compound shape obtained from a decomposition into convex parts using the specified - /// [`VHACDParameters`]. + TrimeshFromMeshWithConfig(TrimeshFlags), + /// Constructs a collider with [`Collider::convex_decomposition_from_mesh`]. + #[cfg(all(feature = "3d", feature = "collider-from-mesh"))] + ConvexDecompositionFromMesh, + /// Constructs a collider with [`Collider::convex_decomposition_from_mesh_with_config`]. #[cfg(all( - feature = "default-collider", - any(feature = "parry-f32", feature = "parry-f64") + feature = "3d", + feature = "collider-from-mesh", + feature = "default-collider" ))] - ConvexDecomposition(VHACDParameters), + ConvexDecompositionFromMeshWithConfig(VhacdParameters), + /// Constructs a collider with [`Collider::convex_hull_from_mesh`]. + #[cfg(all(feature = "3d", feature = "collider-from-mesh"))] + ConvexHullFromMesh, +} +impl ColliderConstructor { + /// Returns `true` if the collider type requires a mesh to be generated. + pub fn requires_mesh(&self) -> bool { + #[cfg(all(feature = "3d", feature = "collider-from-mesh"))] + { + matches!( + self, + Self::TrimeshFromMesh + | Self::TrimeshFromMeshWithConfig(_) + | Self::ConvexDecompositionFromMesh + | Self::ConvexDecompositionFromMeshWithConfig(_) + | Self::ConvexHullFromMesh + ) + } + #[cfg(not(all(feature = "3d", feature = "collider-from-mesh")))] + { + false + } + } } /// A component that stores the `Entity` ID of the [`RigidBody`] that a [`Collider`] is attached to. @@ -541,3 +788,317 @@ impl MapEntities for CollidingEntities { .collect() } } + +#[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.spawn(PRIMITIVE_COLLIDER.clone()).id(); + + app.update(); + + assert!(app.query_ok::<&Collider>(entity)); + assert!(app.query_err::<&ColliderConstructor>(entity)); + } + + #[cfg(all(feature = "3d", feature = "collider-from-mesh"))] + #[test] + #[should_panic] + fn collider_constructor_requires_mesh_on_computed() { + let mut app = create_test_app(); + + app.world.spawn(COMPUTED_COLLIDER.clone()); + + app.update(); + } + + #[cfg(all(feature = "3d", 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 + .spawn((COMPUTED_COLLIDER.clone(), mesh_handle)) + .id(); + + app.update(); + + assert!(app.query_ok::<&Collider>(entity)); + assert!(app.query_ok::<&Handle>(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 + .spawn(ColliderConstructorHierarchy::new( + PRIMITIVE_COLLIDER.clone(), + )) + .id(); + + app.update(); + + assert!(app.query_err::<&ColliderConstructorHierarchy>(entity)); + assert!(app.query_err::<&Collider>(entity)); + } + + #[cfg(all(feature = "3d", 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 + .spawn(( + ColliderConstructorHierarchy::new(COMPUTED_COLLIDER.clone()), + mesh_handle, + )) + .id(); + + app.update(); + + assert!(app.query_ok::<&Handle>(entity)); + assert!(app.query_err::<&ColliderConstructorHierarchy>(entity)); + assert!(app.query_err::<&Collider>(entity)); + } + + #[cfg(all(feature = "3d", 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 + .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(); + + // Hierarchy: + // - parent + // - child1 + // - child2 + // - child3 + + let parent = app + .world + .spawn(ColliderConstructorHierarchy::new( + PRIMITIVE_COLLIDER.clone(), + )) + .id(); + let child1 = app.world.spawn(()).id(); + let child2 = app.world.spawn(()).id(); + let child3 = app.world.spawn(()).id(); + + app.world + .entity_mut(parent) + .push_children(&[child1, child2]); + app.world.entity_mut(child2).push_children(&[child3]); + + app.update(); + + // No entities should have ColliderConstructorHierarchy + 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(all(feature = "3d", 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(); + + // Hierarchy: + // - parent + // - child1 (no mesh) + // - child2 (no mesh) + // - child3 (mesh) + // - child4 (mesh) + // - child5 (no mesh) + // - child6 (mesh) + // - child7 (mesh) + // - child8 (mesh) + + let parent = app + .world + .spawn(ColliderConstructorHierarchy::new(COMPUTED_COLLIDER.clone())) + .id(); + let child1 = app.world.spawn(()).id(); + let child2 = app.world.spawn(()).id(); + let child3 = app.world.spawn(mesh_handle.clone()).id(); + let child4 = app.world.spawn(mesh_handle.clone()).id(); + let child5 = app.world.spawn(()).id(); + let child6 = app.world.spawn(mesh_handle.clone()).id(); + let child7 = app.world.spawn(mesh_handle.clone()).id(); + let child8 = app.world.spawn(mesh_handle.clone()).id(); + + app.world + .entity_mut(parent) + .push_children(&[child1, child2, child4, child6, child7]); + app.world.entity_mut(child2).push_children(&[child3]); + app.world.entity_mut(child4).push_children(&[child5]); + app.world.entity_mut(child7).push_children(&[child8]); + + app.update(); + + // No entities should have ColliderConstructorHierarchy + 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 = "3d", feature = "collider-from-mesh", feature = "bevy_scene"))] + #[test] + fn collider_constructor_hierarchy_inserts_computed_colliders_on_scene() { + let mut app = create_gltf_test_app(); + + let scene_handle = app + .world + .resource_mut::() + .load("ferris.glb#Scene0"); + + let hierarchy = app + .world + .spawn(( + SceneBundle { + scene: scene_handle, + ..default() + }, + ColliderConstructorHierarchy::new(ColliderConstructor::ConvexDecompositionFromMesh) + .with_density_for_name("armL_mesh", 2.0) + .with_density_for_name("armR_mesh", 3.0), + RigidBody::Dynamic, + )) + .id(); + + while app + .world + .resource::>() + .is_empty() + { + app.update(); + } + app.update(); + + assert!(app.query_err::<&ColliderConstructorHierarchy>(hierarchy)); + assert!(app.query_err::<&Collider>(hierarchy)); + + let densities: HashMap<_, _> = app + .world + .query::<(&Name, &ColliderDensity)>() + .iter(&app.world) + .map(|(name, density)| (name.to_string(), density.0)) + .collect(); + + assert_eq!( + densities["eyes_mesh"], + ColliderConstructorHierarchyConfig::DEFAULT_DENSITY.0 + ); + assert_eq!(densities["armL_mesh"], 2.0); + assert_eq!(densities["armR_mesh"], 3.0); + } + + const PRIMITIVE_COLLIDER: ColliderConstructor = ColliderConstructor::Capsule { + height: 1.0, + radius: 0.5, + }; + + #[cfg(all(feature = "3d", 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, + PhysicsPlugins::default(), + )) + .init_resource::>(); + + app + } + + #[cfg(all(feature = "3d", feature = "collider-from-mesh", feature = "bevy_scene"))] + fn create_gltf_test_app() -> App { + let mut app = App::new(); + app.add_plugins(( + DefaultPlugins.build().disable::(), + PhysicsPlugins::default(), + )); + app.finish(); + app.cleanup(); + app + } + + trait AppExt { + fn query_ok(&mut self, entity: Entity) -> bool; + fn query_err(&mut self, entity: Entity) -> bool { + !self.query_ok::(entity) + } + + #[cfg(all(feature = "3d", feature = "collider-from-mesh"))] + fn add_mesh(&mut self) -> Handle; + } + + impl AppExt for App { + fn query_ok(&mut self, entity: Entity) -> bool { + let mut query = self.world.query::(); + let component = query.get(&self.world, entity); + component.is_ok() + } + + #[cfg(all(feature = "3d", feature = "collider-from-mesh"))] + fn add_mesh(&mut self) -> Handle { + self.world + .get_resource_mut::>() + .unwrap() + .add(Mesh::from(Cuboid::default())) + } + } +} diff --git a/src/plugins/collision/collider/parry/mod.rs b/src/plugins/collision/collider/parry/mod.rs index 4b0713ab..fd7de1ce 100644 --- a/src/plugins/collision/collider/parry/mod.rs +++ b/src/plugins/collision/collider/parry/mod.rs @@ -22,13 +22,188 @@ impl> From for Collider { } } -/// Parameters controlling the VHACD convex decomposition algorithm. +/// Parameters controlling the VHACD convex decomposition. /// /// See for details. -pub type VHACDParameters = parry::transformation::vhacd::VHACDParameters; +#[derive(Clone, PartialEq, Debug, Reflect)] +#[reflect(PartialEq, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))] +pub struct VhacdParameters { + /// Maximum concavity. + /// + /// Default: 0.1 (in 2D), 0.01 (in 3D). + /// Valid range `[0.0, 1.0]`. + pub concavity: Scalar, + /// Controls the bias toward clipping along symmetry planes. + /// + /// Default: 0.05. + /// Valid Range: `[0.0, 1.0]`. + pub alpha: Scalar, + /// Controls the bias toward clipping along revolution planes. + /// + /// Default: 0.05. + /// Valid Range: `[0.0, 1.0]`. + pub beta: Scalar, + /// Resolution used during the voxelization stage. + /// + /// Default: 256 (in 2D), 64 (in 3D). + pub resolution: u32, + /// Controls the granularity of the search for the best + /// clipping plane during the decomposition. + /// + /// Default: 4 + pub plane_downsampling: u32, + /// Controls the precision of the convex-hull generation + /// process during the clipping plane selection stage. + /// + /// Default: 4 + pub convex_hull_downsampling: u32, + /// Controls the way the input mesh or polyline is being + /// voxelized. + /// + /// Default: `FillMode::FloodFill { detect_cavities: false, detect_self_intersections: false }` + pub fill_mode: FillMode, + /// Controls whether the convex-hull should be approximated during the decomposition stage. + /// Setting this to `true` increases performances with a slight degradation of the decomposition + /// quality. + /// + /// Default: true + pub convex_hull_approximation: bool, + /// Controls the max number of convex-hull generated by the convex decomposition. + /// + /// Default: 1024 + pub max_convex_hulls: u32, +} + +impl Default for VhacdParameters { + fn default() -> Self { + Self { + #[cfg(feature = "3d")] + resolution: 64, + #[cfg(feature = "3d")] + concavity: 0.01, + #[cfg(feature = "2d")] + resolution: 256, + #[cfg(feature = "2d")] + concavity: 0.1, + plane_downsampling: 4, + convex_hull_downsampling: 4, + alpha: 0.05, + beta: 0.05, + convex_hull_approximation: true, + max_convex_hulls: 1024, + fill_mode: FillMode::FloodFill { + detect_cavities: false, + #[cfg(feature = "2d")] + detect_self_intersections: false, + }, + } + } +} + +impl From for parry::transformation::vhacd::VHACDParameters { + fn from(value: VhacdParameters) -> Self { + Self { + concavity: value.concavity, + alpha: value.alpha, + beta: value.beta, + resolution: value.resolution, + plane_downsampling: value.plane_downsampling, + convex_hull_downsampling: value.convex_hull_downsampling, + fill_mode: value.fill_mode.into(), + convex_hull_approximation: value.convex_hull_approximation, + max_convex_hulls: value.max_convex_hulls, + } + } +} + +/// Controls how the voxelization determines which voxel needs +/// to be considered empty, and which ones will be considered full. +#[derive(Hash, Clone, Copy, PartialEq, Eq, Debug, Reflect)] +#[reflect(Hash, PartialEq, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))] +pub enum FillMode { + /// Only consider full the voxels intersecting the surface of the + /// shape being voxelized. + SurfaceOnly, + /// Use a flood-fill technique to consider fill the voxels intersecting + /// the surface of the shape being voxelized, as well as all the voxels + /// bounded of them. + FloodFill { + /// Detects holes inside of a solid contour. + detect_cavities: bool, + /// Attempts to properly handle self-intersections. + #[cfg(feature = "2d")] + detect_self_intersections: bool, + }, +} + +impl From for parry::transformation::voxelization::FillMode { + fn from(value: FillMode) -> Self { + match value { + FillMode::SurfaceOnly => Self::SurfaceOnly, + FillMode::FloodFill { + detect_cavities, + #[cfg(feature = "2d")] + detect_self_intersections, + } => Self::FloodFill { + detect_cavities, + #[cfg(feature = "2d")] + detect_self_intersections, + }, + } + } +} + +bitflags::bitflags! { + /// Flags used for the preprocessing of a triangle mesh collider. + #[repr(transparent)] + #[derive(Hash, Clone, Copy, PartialEq, Eq, Debug, Reflect)] + #[reflect_value(Hash, PartialEq, Debug)] + #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "serialize", reflect_value(Serialize, Deserialize))] + pub struct TrimeshFlags: u8 { + /// If set, the half-edge topology of the trimesh will be computed if possible. + const HALF_EDGE_TOPOLOGY = 0b0000_0001; + /// If set, the half-edge topology and connected components of the trimesh will be computed if possible. + /// + /// Because of the way it is currently implemented, connected components can only be computed on + /// a mesh where the half-edge topology computation succeeds. It will no longer be the case in the + /// future once we decouple the computations. + const CONNECTED_COMPONENTS = 0b0000_0010; + /// If set, any triangle that results in a failing half-hedge topology computation will be deleted. + const DELETE_BAD_TOPOLOGY_TRIANGLES = 0b0000_0100; + /// If set, the trimesh will be assumed to be oriented (with outward normals). + /// + /// The pseudo-normals of its vertices and edges will be computed. + const ORIENTED = 0b0000_1000; + /// If set, the duplicate vertices of the trimesh will be merged. + /// + /// Two vertices with the exact same coordinates will share the same entry on the + /// vertex buffer and the index buffer is adjusted accordingly. + const MERGE_DUPLICATE_VERTICES = 0b0001_0000; + /// If set, the triangles sharing two vertices with identical index values will be removed. + /// + /// Because of the way it is currently implemented, this methods implies that duplicate + /// vertices will be merged. It will no longer be the case in the future once we decouple + /// the computations. + const DELETE_DEGENERATE_TRIANGLES = 0b0010_0000; + /// If set, two triangles sharing three vertices with identical index values (in any order) will be removed. + /// + /// Because of the way it is currently implemented, this methods implies that duplicate + /// vertices will be merged. It will no longer be the case in the future once we decouple + /// the computations. + const DELETE_DUPLICATE_TRIANGLES = 0b0100_0000; + } +} -/// Flags used for the preprocessing of a triangle mesh collider. -pub type TriMeshFlags = parry::shape::TriMeshFlags; +impl From for parry::shape::TriMeshFlags { + fn from(value: TrimeshFlags) -> Self { + Self::from_bits(value.bits()).unwrap() + } +} /// A collider used for detecting collisions and generating contacts. /// @@ -90,9 +265,13 @@ pub type TriMeshFlags = parry::shape::TriMeshFlags; /// - [`ColliderDensity`] /// - [`ColliderMassProperties`] /// +/// If you need to specify the shape of the collider statically, use [`ColliderConstructor`] and build your collider +/// with the [`Collider::try_from_constructor`] method. +/// This can also be done automatically by simply placing the [`ColliderConstructor`] on an entity. +/// #[cfg_attr( feature = "3d", - doc = "Colliders can also be generated automatically from meshes and scenes. See [`AsyncCollider`] and [`AsyncSceneCollider`]." + doc = "Colliders can also be generated automatically from meshes and scenes. See [`ColliderConstructor`] and [`ColliderConstructorHierarchy`]." )] /// /// ### Multiple colliders @@ -162,7 +341,7 @@ pub type TriMeshFlags = parry::shape::TriMeshFlags; /// - [Sensors](Sensor) #[cfg_attr( feature = "3d", - doc = "- Creating colliders from meshes with [`AsyncCollider`] and [`AsyncSceneCollider`]" + doc = "- Creating colliders from meshes with [`ColliderConstructor`] and [`ColliderConstructorHierarchy`]" )] /// - [Get colliding entities](CollidingEntities) /// - [Collision events](ContactReportingPlugin#collision-events) @@ -176,6 +355,8 @@ pub type TriMeshFlags = parry::shape::TriMeshFlags; /// /// To get a reference to the internal [`SharedShape`], you can use the [`Collider::shape()`] /// or [`Collider::shape_scaled()`] methods. +/// +/// `Collider` is currently not `Reflect`. If you need to reflect it, you can use [`ColliderConstructor`] as a workaround. #[derive(Clone, Component)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct Collider { @@ -610,7 +791,7 @@ impl Collider { } /// Creates a collider with a [half-space](https://en.wikipedia.org/wiki/Half-space_(geometry)) shape defined by the outward normal of its planar boundary. - pub fn halfspace(outward_normal: Vector) -> Self { + pub fn half_space(outward_normal: Vector) -> Self { SharedShape::halfspace(nalgebra::Unit::new_normalize(outward_normal.into())).into() } @@ -651,10 +832,10 @@ impl Collider { pub fn trimesh_with_config( vertices: Vec, indices: Vec<[u32; 3]>, - flags: TriMeshFlags, + flags: TrimeshFlags, ) -> Self { let vertices = vertices.into_iter().map(|v| v.into()).collect(); - SharedShape::trimesh_with_flags(vertices, indices, flags).into() + SharedShape::trimesh_with_flags(vertices, indices, flags.into()).into() } /// Creates a collider shape with a compound shape obtained from the decomposition of a given polyline @@ -674,29 +855,31 @@ impl Collider { } /// Creates a collider shape with a compound shape obtained from the decomposition of a given polyline - /// defined by its vertex and index buffers. The given [`VHACDParameters`] are used for configuring + /// defined by its vertex and index buffers. The given [`VhacdParameters`] are used for configuring /// the decomposition process. #[cfg(feature = "2d")] pub fn convex_decomposition_with_config( vertices: Vec, indices: Vec<[u32; 2]>, - params: &VHACDParameters, + params: &VhacdParameters, ) -> Self { let vertices = vertices.iter().map(|v| (*v).into()).collect::>(); - SharedShape::convex_decomposition_with_params(&vertices, &indices, params).into() + SharedShape::convex_decomposition_with_params(&vertices, &indices, ¶ms.clone().into()) + .into() } /// Creates a collider shape with a compound shape obtained from the decomposition of a given trimesh - /// defined by its vertex and index buffers. The given [`VHACDParameters`] are used for configuring + /// defined by its vertex and index buffers. The given [`VhacdParameters`] are used for configuring /// the decomposition process. #[cfg(feature = "3d")] pub fn convex_decomposition_with_config( vertices: Vec, indices: Vec<[u32; 3]>, - params: &VHACDParameters, + params: VhacdParameters, ) -> Self { let vertices = vertices.iter().map(|v| (*v).into()).collect::>(); - SharedShape::convex_decomposition_with_params(&vertices, &indices, params).into() + SharedShape::convex_decomposition_with_params(&vertices, &indices, ¶ms.clone().into()) + .into() } /// Creates a collider with a [convex polygon](https://en.wikipedia.org/wiki/Convex_polygon) shape obtained after computing @@ -776,13 +959,13 @@ impl Collider { SharedShape::trimesh_with_flags( vertices, indices, - TriMeshFlags::MERGE_DUPLICATE_VERTICES, + TrimeshFlags::MERGE_DUPLICATE_VERTICES.into(), ) .into() }) } - /// Creates a collider with a triangle mesh shape from a `Mesh` using the given [`TriMeshFlags`] + /// Creates a collider with a triangle mesh shape from a `Mesh` using the given [`TrimeshFlags`] /// for controlling the preprocessing. /// /// ## Example @@ -794,7 +977,7 @@ impl Collider { /// fn setup(mut commands: Commands, mut meshes: ResMut>) { /// let mesh = Mesh::from(Cuboid::default()); /// commands.spawn(( - /// Collider::trimesh_from_mesh_with_config(&mesh, TriMeshFlags::all()).unwrap(), + /// Collider::trimesh_from_mesh_with_config(&mesh, TrimeshFlags::all()).unwrap(), /// PbrBundle { /// mesh: meshes.add(mesh), /// ..default() @@ -803,9 +986,9 @@ impl Collider { /// } /// ``` #[cfg(all(feature = "3d", feature = "collider-from-mesh"))] - pub fn trimesh_from_mesh_with_config(mesh: &Mesh, flags: TriMeshFlags) -> Option { + pub fn trimesh_from_mesh_with_config(mesh: &Mesh, flags: TrimeshFlags) -> Option { extract_mesh_vertices_indices(mesh).map(|(vertices, indices)| { - SharedShape::trimesh_with_flags(vertices, indices, flags).into() + SharedShape::trimesh_with_flags(vertices, indices, flags.into()).into() }) } @@ -861,7 +1044,7 @@ impl Collider { } /// Creates a compound shape obtained from the decomposition of a `Mesh` - /// with the given [`VHACDParameters`] passed to the decomposition algorithm. + /// with the given [`VhacdParameters`] passed to the decomposition algorithm. /// /// ## Example /// @@ -871,7 +1054,7 @@ impl Collider { /// /// fn setup(mut commands: Commands, mut meshes: ResMut>) { /// let mesh = Mesh::from(Cuboid::default()); - /// let config = VHACDParameters { + /// let config = VhacdParameters { /// convex_hull_approximation: false, /// ..default() /// }; @@ -887,12 +1070,160 @@ impl Collider { #[cfg(all(feature = "3d", feature = "collider-from-mesh"))] pub fn convex_decomposition_from_mesh_with_config( mesh: &Mesh, - parameters: &VHACDParameters, + parameters: &VhacdParameters, ) -> Option { extract_mesh_vertices_indices(mesh).map(|(vertices, indices)| { - SharedShape::convex_decomposition_with_params(&vertices, &indices, parameters).into() + SharedShape::convex_decomposition_with_params( + &vertices, + &indices, + ¶meters.clone().into(), + ) + .into() }) } + + /// Attempts to create a collider from an optional mesh with the given [`ColliderConstructor`]. + /// By using this, you can serialize and deserialize the collider's creation method + /// separately from the collider itself via the [`ColliderConstructor`] enum. + /// + /// Returns `None` in the following cases: + /// - The given [`ColliderConstructor`] requires a mesh, but none was provided. + /// - Creating the collider from the given [`ColliderConstructor`] failed. + pub fn try_from_constructor( + collider_constructor: ColliderConstructor, + #[allow(unused_variables)] mesh: Option<&Mesh>, + ) -> Option { + match collider_constructor { + #[cfg(feature = "2d")] + ColliderConstructor::Circle { radius } => Some(Self::circle(radius)), + #[cfg(feature = "3d")] + ColliderConstructor::Sphere { radius } => Some(Self::sphere(radius)), + #[cfg(feature = "2d")] + ColliderConstructor::Ellipse { + half_width, + half_height, + } => Some(Self::ellipse(half_width, half_height)), + #[cfg(feature = "2d")] + ColliderConstructor::Rectangle { x_length, y_length } => { + Some(Self::rectangle(x_length, y_length)) + } + #[cfg(feature = "3d")] + ColliderConstructor::Cuboid { + x_length, + y_length, + z_length, + } => Some(Self::cuboid(x_length, y_length, z_length)), + #[cfg(feature = "2d")] + ColliderConstructor::RoundRectangle { + x_length, + y_length, + border_radius, + } => Some(Self::round_rectangle(x_length, y_length, border_radius)), + #[cfg(feature = "3d")] + ColliderConstructor::RoundCuboid { + x_length, + y_length, + z_length, + border_radius, + } => Some(Self::round_cuboid( + x_length, + y_length, + z_length, + border_radius, + )), + #[cfg(feature = "3d")] + ColliderConstructor::Cylinder { height, radius } => { + Some(Self::cylinder(height, radius)) + } + #[cfg(feature = "3d")] + ColliderConstructor::Cone { height, radius } => Some(Self::cone(height, radius)), + ColliderConstructor::Capsule { height, radius } => Some(Self::capsule(height, radius)), + ColliderConstructor::CapsuleEndpoints { a, b, radius } => { + Some(Self::capsule_endpoints(a, b, radius)) + } + ColliderConstructor::HalfSpace { outward_normal } => { + Some(Self::half_space(outward_normal)) + } + ColliderConstructor::Segment { a, b } => Some(Self::segment(a, b)), + ColliderConstructor::Triangle { a, b, c } => Some(Self::triangle(a, b, c)), + #[cfg(feature = "2d")] + ColliderConstructor::RegularPolygon { + circumradius, + sides, + } => Some(Self::regular_polygon(circumradius, sides)), + ColliderConstructor::Polyline { vertices, indices } => { + Some(Self::polyline(vertices, indices)) + } + ColliderConstructor::Trimesh { vertices, indices } => { + Some(Self::trimesh(vertices, indices)) + } + ColliderConstructor::TrimeshWithConfig { + vertices, + indices, + flags, + } => Some(Self::trimesh_with_config(vertices, indices, flags)), + #[cfg(feature = "2d")] + ColliderConstructor::ConvexDecomposition { vertices, indices } => { + Some(Self::convex_decomposition(vertices, indices)) + } + #[cfg(feature = "3d")] + ColliderConstructor::ConvexDecomposition { vertices, indices } => { + Some(Self::convex_decomposition(vertices, indices)) + } + #[cfg(feature = "2d")] + ColliderConstructor::ConvexDecompositionWithConfig { + vertices, + indices, + params, + } => Some(Self::convex_decomposition_with_config( + vertices, indices, ¶ms, + )), + #[cfg(feature = "3d")] + ColliderConstructor::ConvexDecompositionWithConfig { + vertices, + indices, + params, + } => Some(Self::convex_decomposition_with_config( + vertices, indices, params, + )), + #[cfg(feature = "2d")] + ColliderConstructor::ConvexHull { points } => Self::convex_hull(points), + #[cfg(feature = "3d")] + ColliderConstructor::ConvexHull { points } => Self::convex_hull(points), + #[cfg(feature = "2d")] + ColliderConstructor::Heightfield { heights, scale } => { + Some(Self::heightfield(heights, scale)) + } + #[cfg(feature = "3d")] + ColliderConstructor::Heightfield { heights, scale } => { + Some(Self::heightfield(heights, scale)) + } + #[cfg(all(feature = "3d", feature = "collider-from-mesh"))] + ColliderConstructor::TrimeshFromMesh => Self::trimesh_from_mesh(mesh?), + #[cfg(all( + feature = "3d", + feature = "collider-from-mesh", + feature = "default-collider" + ))] + ColliderConstructor::TrimeshFromMeshWithConfig(flags) => { + Self::trimesh_from_mesh_with_config(mesh?, flags) + } + #[cfg(all(feature = "3d", feature = "collider-from-mesh"))] + ColliderConstructor::ConvexDecompositionFromMesh => { + Self::convex_decomposition_from_mesh(mesh?) + } + #[cfg(all( + feature = "3d", + feature = "collider-from-mesh", + feature = "default-collider" + ))] + ColliderConstructor::ConvexDecompositionFromMeshWithConfig(params) => { + Self::convex_decomposition_from_mesh_with_config(mesh?, ¶ms) + } + #[cfg(all(feature = "3d", feature = "collider-from-mesh"))] + ColliderConstructor::ConvexHullFromMesh => Self::convex_hull_from_mesh(mesh?), + } + } } #[cfg(all(feature = "3d", feature = "collider-from-mesh"))] diff --git a/src/plugins/collision/collider_backend.rs b/src/plugins/collision/collider_backend.rs index eddfa424..5b03edf7 100644 --- a/src/plugins/collision/collider_backend.rs +++ b/src/plugins/collision/collider_backend.rs @@ -9,11 +9,7 @@ use crate::{ prelude::*, prepare::{match_any, PrepareSet}, }; -#[cfg(all( - feature = "3d", - feature = "async-collider", - feature = "default-collider" -))] +#[cfg(feature = "bevy_scene")] use bevy::scene::SceneInstance; use bevy::{ prelude::*, @@ -171,12 +167,13 @@ impl Plugin for ColliderBackendPlugin { .ambiguous_with_all(), ); - #[cfg(all( - feature = "3d", - feature = "async-collider", - feature = "default-collider" - ))] - app.add_systems(Update, (init_async_colliders, init_async_scene_colliders)); + app.add_systems( + Update, + ( + init_collider_constructors, + init_collider_constructor_hierarchies, + ), + ); // Update child colliders before narrow phase in substepping loop let substep_schedule = app @@ -250,105 +247,158 @@ fn init_colliders( } } -/// Creates [`Collider`]s from [`AsyncCollider`]s if the meshes have become available. -#[cfg(all( - feature = "3d", - feature = "async-collider", - feature = "default-collider" -))] -pub fn init_async_colliders( +/// Generates [`Collider`]s based on [`ColliderConstructor`]s. +/// +/// If a [`ColliderConstructor`] requires a mesh, the system keeps running +/// until the mesh associated with the mesh handle is available. +/// +/// # Panics +/// +/// Panics if the [`ColliderConstructor`] requires a mesh but no mesh handle is found. +fn init_collider_constructors( mut commands: Commands, meshes: Res>, - async_colliders: Query<(Entity, &Handle, &AsyncCollider)>, + constructors: Query<( + Entity, + Option<&Handle>, + Option<&Collider>, + Option<&Name>, + &ColliderConstructor, + )>, ) { - for (entity, mesh_handle, async_collider) in async_colliders.iter() { - if let Some(mesh) = meshes.get(mesh_handle) { - let collider = match &async_collider.0 { - ComputedCollider::TriMesh => Collider::trimesh_from_mesh(mesh), - ComputedCollider::TriMeshWithFlags(flags) => { - Collider::trimesh_from_mesh_with_config(mesh, *flags) - } - ComputedCollider::ConvexHull => Collider::convex_hull_from_mesh(mesh), - ComputedCollider::ConvexDecomposition(params) => { - Collider::convex_decomposition_from_mesh_with_config(mesh, params) - } - }; - if let Some(collider) = collider { - commands - .entity(entity) - .insert(collider) - .remove::(); - } else { - error!("Unable to generate collider from mesh {:?}", mesh); + for (entity, mesh_handle, 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::(); + continue; + } + let mesh = if constructor.requires_mesh() { + let mesh_handle = mesh_handle.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 mesh = meshes.get(mesh_handle); + if mesh.is_none() { + // Mesh required, but not loaded yet + continue; } + mesh + } else { + None + }; + + let collider = Collider::try_from_constructor(constructor.clone(), mesh); + if let Some(collider) = collider { + commands.entity(entity).insert(collider); + } else { + error!( + "Tried to add a collider to entity {name} via {constructor:#?}, \ + but the collider could not be generated from mesh {mesh:#?}. Skipping.", + ); } + commands.entity(entity).remove::(); } } -/// Creates [`Collider`]s from [`AsyncSceneCollider`]s if the scenes have become available. -#[cfg(all( - feature = "3d", - feature = "async-collider", - feature = "default-collider" -))] -pub fn init_async_scene_colliders( +/// Generates [`Collider`]s for descendants of entities with the [`ColliderConstructorHierarchy`] component. +/// +/// If an entity has a `SceneInstance`, its collider hierarchy is only generated once the scene is ready. +fn init_collider_constructor_hierarchies( mut commands: Commands, meshes: Res>, - scene_spawner: Res, - async_colliders: Query<(Entity, &SceneInstance, &AsyncSceneCollider)>, + #[cfg(feature = "bevy_scene")] scene_spawner: Res, + #[cfg(feature = "bevy_scene")] scenes: Query<&Handle>, + #[cfg(feature = "bevy_scene")] scene_instances: Query<&SceneInstance>, + collider_constructors: Query<(Entity, &ColliderConstructorHierarchy)>, children: Query<&Children>, - mesh_handles: Query<(&Name, &Handle)>, + mesh_handles: Query<(Option<&Name>, Option<&Collider>, Option<&Handle>)>, ) { - for (scene_entity, scene_instance, async_scene_collider) in async_colliders.iter() { - if scene_spawner.instance_is_ready(**scene_instance) { - for child_entity in children.iter_descendants(scene_entity) { - if let Ok((name, handle)) = mesh_handles.get(child_entity) { - let Some(collider_data) = async_scene_collider - .meshes_by_name + 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) { + // Wait for the scene to be ready + continue; + } + } else { + // SceneInstance is added in the SpawnScene schedule, so it might not be available yet + continue; + } + } + } + + for child_entity in children.iter_descendants(scene_entity) { + if let Ok((name, existing_collider, handle)) = mesh_handles.get(child_entity) { + let pretty_name = pretty_name(name, child_entity); + + let default_collider = || { + collider_constructor_hierarchy + .default_constructor + .clone() + .map(ColliderConstructorHierarchyConfig::from_constructor) + }; + + let collider_data = if let Some(name) = name { + collider_constructor_hierarchy + .config .get(name.as_str()) .cloned() - .unwrap_or( - async_scene_collider - .default_shape - .clone() - .map(|shape| AsyncSceneColliderData { shape, ..default() }), - ) - else { - continue; - }; - - let mesh = meshes.get(handle).expect("mesh should already be loaded"); - - let collider = match collider_data.shape { - ComputedCollider::TriMesh => Collider::trimesh_from_mesh(mesh), - ComputedCollider::TriMeshWithFlags(flags) => { - Collider::trimesh_from_mesh_with_config(mesh, flags) - } - ComputedCollider::ConvexHull => Collider::convex_hull_from_mesh(mesh), - ComputedCollider::ConvexDecomposition(params) => { - Collider::convex_decomposition_from_mesh_with_config(mesh, ¶ms) - } - }; - if let Some(collider) = collider { - commands.entity(child_entity).insert(( - collider, - collider_data.layers, - ColliderDensity(collider_data.density), - )); + .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 mesh = if collider_data.constructor.requires_mesh() { + if let Some(handle) = handle { + meshes.get(handle) } else { - error!( - "unable to generate collider from mesh {:?} with name {}", - mesh, name - ); + continue; } + } else { + None + }; + + let collider = Collider::try_from_constructor(collider_data.constructor, mesh); + if let Some(collider) = collider { + commands.entity(child_entity).insert(( + collider, + collider_data.layers, + collider_data.density, + )); + } else { + error!( + "Tried to add a collider to entity {pretty_name} via {collider_constructor_hierarchy:#?}, \ + but the collider could not be generated from mesh {mesh:#?}. Skipping.", + ); } } - - commands.entity(scene_entity).remove::(); } + + commands + .entity(scene_entity) + .remove::(); } } +fn pretty_name(name: Option<&Name>, entity: Entity) -> String { + name.map(|n| n.to_string()) + .unwrap_or_else(|| format!("", entity.index())) +} + /// Updates the Axis-Aligned Bounding Boxes of all colliders. A safety margin will be added to account for sudden accelerations. #[allow(clippy::type_complexity)] fn update_aabb( diff --git a/src/plugins/setup/mod.rs b/src/plugins/setup/mod.rs index 2c6c7db9..49c706a7 100644 --- a/src/plugins/setup/mod.rs +++ b/src/plugins/setup/mod.rs @@ -114,7 +114,10 @@ impl Plugin for PhysicsSetupPlugin { .register_type::() .register_type::() .register_type::() - .register_type::(); + .register_type::() + .register_type::() + .register_type::() + .register_type::(); // Configure higher level system sets for the given schedule let schedule = self.schedule; diff --git a/src/tests.rs b/src/tests.rs index 344c6d95..ebf7ba13 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -29,16 +29,17 @@ macro_rules! setup_insta { fn create_app() -> App { let mut app = App::new(); - app.add_plugins((MinimalPlugins, TransformPlugin, PhysicsPlugins::default())); - #[cfg(feature = "async-collider")] - { - app.add_plugins(( - bevy::asset::AssetPlugin::default(), - bevy::scene::ScenePlugin, - )) - .init_resource::>(); - } - app.insert_resource(TimeUpdateStrategy::ManualInstant(Instant::now())); + app.add_plugins(( + MinimalPlugins, + TransformPlugin, + PhysicsPlugins::default(), + bevy::asset::AssetPlugin::default(), + #[cfg(feature = "bevy_scene")] + bevy::scene::ScenePlugin, + )) + .init_resource::>() + .insert_resource(TimeUpdateStrategy::ManualInstant(Instant::now())); + app } @@ -248,28 +249,23 @@ fn no_ambiguity_errors() { #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] struct DeterministicSchedule; - let mut app = App::new(); - - app.add_plugins((MinimalPlugins, PhysicsPlugins::new(DeterministicSchedule))); - - #[cfg(feature = "async-collider")] - { - app.add_plugins(( + App::new() + .add_plugins(( + MinimalPlugins, + PhysicsPlugins::new(DeterministicSchedule), bevy::asset::AssetPlugin::default(), + #[cfg(feature = "bevy_scene")] bevy::scene::ScenePlugin, )) - .init_resource::>(); - } - - app.edit_schedule(DeterministicSchedule, |s| { - s.set_build_settings(ScheduleBuildSettings { - ambiguity_detection: LogLevel::Error, - ..default() - }); - }) - .add_systems(Update, |w: &mut World| { - w.run_schedule(DeterministicSchedule); - }); - - app.update(); + .init_resource::>() + .edit_schedule(DeterministicSchedule, |s| { + s.set_build_settings(ScheduleBuildSettings { + ambiguity_detection: LogLevel::Error, + ..default() + }); + }) + .add_systems(Update, |w: &mut World| { + w.run_schedule(DeterministicSchedule); + }) + .update(); }