diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f966b503..e7fd33f1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,7 +32,7 @@ jobs: with: prefix-key: ${{ env.RUST_CACHE_KEY }} - name: Cargo doc - run: cargo doc + run: cargo doc --no-deps test: runs-on: ubuntu-latest env: @@ -51,9 +51,9 @@ jobs: - name: Clippy for bevy_rapier3d run: cargo clippy --verbose -p bevy_rapier3d - name: Clippy for bevy_rapier2d (debug-render, simd, serde) - run: cargo clippy --verbose -p bevy_rapier2d --features debug-render-2d,simd-stable,serde-serialize + run: cargo clippy --verbose -p bevy_rapier2d --features debug-render-2d,simd-stable,serde-serialize,picking-backend - name: Clippy for bevy_rapier3d (debug-render, simd, serde) - run: cargo clippy --verbose -p bevy_rapier3d --features debug-render-3d,simd-stable,serde-serialize + run: cargo clippy --verbose -p bevy_rapier3d --features debug-render-3d,simd-stable,serde-serialize,picking-backend - name: Test for bevy_rapier2d run: cargo test --verbose -p bevy_rapier2d - name: Test for bevy_rapier3d @@ -72,6 +72,6 @@ jobs: with: prefix-key: ${{ env.RUST_CACHE_KEY }} - name: Clippy bevy_rapier2d - run: cd bevy_rapier2d && cargo clippy --verbose --features wasm-bindgen,bevy/webgl2 --target wasm32-unknown-unknown + run: cd bevy_rapier2d && cargo clippy --verbose --features bevy/webgl2 --target wasm32-unknown-unknown - name: Clippy bevy_rapier3d - run: cd bevy_rapier3d && cargo clippy --verbose --features wasm-bindgen,bevy/webgl2 --target wasm32-unknown-unknown + run: cd bevy_rapier3d && cargo clippy --verbose --features bevy/webgl2 --target wasm32-unknown-unknown diff --git a/CHANGELOG.md b/CHANGELOG.md index b6c9efdb..cf210899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### Added + +- Added optional feature `picking-backend` to support bevy_picking. + - See `picking_backend` module documentation for more details. + ### Modified - `RapierContext` has been split in multiple `Component`s: diff --git a/bevy_rapier2d/Cargo.toml b/bevy_rapier2d/Cargo.toml index 4a626709..4006535e 100644 --- a/bevy_rapier2d/Cargo.toml +++ b/bevy_rapier2d/Cargo.toml @@ -24,7 +24,7 @@ rust.unexpected_cfgs = { level = "warn", check-cfg = [ clippy = { needless_lifetimes = "allow" } [features] -default = ["dim2", "async-collider", "debug-render-2d"] +default = ["dim2", "async-collider", "debug-render-2d", "picking-backend"] dim2 = [] debug-render-2d = [ "bevy/bevy_core_pipeline", @@ -45,16 +45,21 @@ rapier-debug-render = ["rapier2d/debug-render"] parallel = ["rapier2d/parallel"] simd-stable = ["rapier2d/simd-stable"] simd-nightly = ["rapier2d/simd-nightly"] -wasm-bindgen = ["rapier2d/wasm-bindgen"] serde-serialize = ["rapier2d/serde-serialize", "bevy/serialize", "serde"] enhanced-determinism = ["rapier2d/enhanced-determinism"] headless = [] -async-collider = ["bevy/bevy_asset", "bevy/bevy_scene", "bevy/bevy_render"] +picking-backend = ["bevy/bevy_picking"] +async-collider = [ + "bevy/bevy_asset", + "bevy/bevy_scene", + "bevy/bevy_render", + "bevy/bevy_image", +] [dependencies] bevy = { version = "0.15", default-features = false } nalgebra = { version = "0.33", features = ["convert-glam029"] } -rapier2d = "0.22" +rapier2d = "0.23" bitflags = "2.4" log = "0.4" serde = { version = "1", features = ["derive"], optional = true } diff --git a/bevy_rapier3d/Cargo.toml b/bevy_rapier3d/Cargo.toml index b5d18c47..649bfc94 100644 --- a/bevy_rapier3d/Cargo.toml +++ b/bevy_rapier3d/Cargo.toml @@ -24,7 +24,7 @@ rust.unexpected_cfgs = { level = "warn", check-cfg = [ clippy = { needless_lifetimes = "allow" } [features] -default = ["dim3", "async-collider", "debug-render-3d"] +default = ["dim3", "async-collider", "debug-render-3d", "picking-backend"] dim3 = [] debug-render = ["debug-render-3d"] debug-render-2d = [ @@ -46,16 +46,21 @@ rapier-debug-render = ["rapier3d/debug-render"] parallel = ["rapier3d/parallel"] simd-stable = ["rapier3d/simd-stable"] simd-nightly = ["rapier3d/simd-nightly"] -wasm-bindgen = ["rapier3d/wasm-bindgen"] serde-serialize = ["rapier3d/serde-serialize", "bevy/serialize", "serde"] enhanced-determinism = ["rapier3d/enhanced-determinism"] headless = [] -async-collider = ["bevy/bevy_asset", "bevy/bevy_scene", "bevy/bevy_render"] +picking-backend = ["bevy/bevy_picking"] +async-collider = [ + "bevy/bevy_asset", + "bevy/bevy_scene", + "bevy/bevy_render", + "bevy/bevy_image", +] [dependencies] bevy = { version = "0.15", default-features = false } nalgebra = { version = "0.33", features = ["convert-glam029"] } -rapier3d = "0.22" +rapier3d = "0.23" bitflags = "2.4" log = "0.4" serde = { version = "1", features = ["derive"], optional = true } @@ -67,6 +72,9 @@ bevy = { version = "0.15", default-features = false, features = [ "tonemapping_luts", "bevy_state", "bevy_debug_stepping", + "bevy_text", + "bevy_ui", + "default_font", ] } approx = "0.5.1" glam = { version = "0.29", features = ["approx"] } @@ -75,4 +83,11 @@ bevy_egui = "0.31" [package.metadata.docs.rs] # Enable all the features when building the docs on docs.rs -features = ["debug-render-3d", "serde-serialize"] + +[[example]] +name = "picking3" +required-features = ["picking-backend"] + +[[example]] +name = "testbed3" +required-features = ["picking-backend"] diff --git a/bevy_rapier3d/examples/picking3.rs b/bevy_rapier3d/examples/picking3.rs new file mode 100644 index 00000000..397eda28 --- /dev/null +++ b/bevy_rapier3d/examples/picking3.rs @@ -0,0 +1,100 @@ +//! A simple scene to demonstrate picking events for rapier [`Collider`] entities. + +use bevy::prelude::*; +use bevy_rapier3d::prelude::*; + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins, + RapierPhysicsPlugin::::default(), + RapierDebugRenderPlugin::default(), + RapierPickingPlugin::default(), + )) + .insert_resource(RapierPickingSettings { + // Optional: only needed when you want fine-grained control + // over which cameras and entities should be used with the rapier picking backend. + // This is disabled by default, and no marker components are required on cameras or colliders. + // This resource is inserted by default, + // you only need to add it if you want to override the default settings. + require_markers: true, + ..Default::default() + }) + .add_systems(Startup, (setup_graphics, setup_physics)) + .run(); +} + +pub fn setup_graphics(mut commands: Commands) { + // Camera + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), + RapierPickable, + )); +} + +pub fn setup_physics(mut commands: Commands) { + commands + .spawn(( + Text::new("Click Me to get a box\nDrag cubes to rotate"), + Node { + position_type: PositionType::Absolute, + top: Val::Percent(12.0), + left: Val::Percent(12.0), + ..default() + }, + )) + .observe(on_click_spawn_cube) + .observe( + |out: Trigger>, mut texts: Query<&mut TextColor>| { + let mut text_color = texts.get_mut(out.entity()).unwrap(); + text_color.0 = Color::WHITE; + }, + ) + .observe( + |over: Trigger>, mut texts: Query<&mut TextColor>| { + let mut color = texts.get_mut(over.entity()).unwrap(); + color.0 = bevy::color::palettes::tailwind::CYAN_400.into(); + }, + ); + // Base + let ground_size = 3.1; + let ground_height = 0.1; + commands.spawn(( + Transform::from_xyz(0.0, -ground_height / 2.0, 0.0), + Collider::cuboid(ground_size, ground_height, ground_size), + ColliderDebugColor(Hsla::BLACK), + )); +} + +fn on_click_spawn_cube( + _click: Trigger>, + mut commands: Commands, + mut num: Local, +) { + let rad = 0.25; + let colors = [ + Hsla::hsl(220.0, 1.0, 0.3), + Hsla::hsl(180.0, 1.0, 0.3), + Hsla::hsl(260.0, 1.0, 0.7), + ]; + commands + .spawn(( + Transform::from_xyz(0.0, 0.25 + 0.55 * *num as f32, 0.0), + Visibility::default(), + RigidBody::Dynamic, + Collider::cuboid(rad, rad, rad), + ColliderDebugColor(colors[*num % 3]), + RapierPickable, + )) + // With the RapierPickingPlugin added, you can add pointer event observers to colliders: + .observe(on_drag_rotate); + *num += 1; +} + +fn on_drag_rotate(drag: Trigger>, mut transforms: Query<&mut Transform>) { + if let Ok(mut transform) = transforms.get_mut(drag.entity()) { + transform.rotate_y(drag.delta.x * 0.02); + transform.rotate_x(drag.delta.y * 0.02); + } +} diff --git a/bevy_rapier3d/examples/static_trimesh3.rs b/bevy_rapier3d/examples/static_trimesh3.rs index 27fae6b5..5bc41382 100644 --- a/bevy_rapier3d/examples/static_trimesh3.rs +++ b/bevy_rapier3d/examples/static_trimesh3.rs @@ -55,7 +55,7 @@ pub fn setup_physics(mut commands: Commands, mut ball_state: ResMut) indices.push([2 * i + 2, 2 * i + 1, 2 * i + 3]); } - commands.spawn(Collider::trimesh(vertices, indices)); + commands.spawn(Collider::trimesh(vertices, indices).unwrap()); // Create a bowl with a cosine cross-section, // so that we can join the end of the ramp smoothly @@ -97,7 +97,7 @@ pub fn setup_physics(mut commands: Commands, mut ball_state: ResMut) -bowl_size.y / 2.0, bowl_size.z / 2.0 - ramp_size.z / 2.0, ), - Collider::trimesh(vertices, indices), + Collider::trimesh(vertices, indices).unwrap(), )); } diff --git a/bevy_rapier3d/examples/testbed3.rs b/bevy_rapier3d/examples/testbed3.rs index 316d713e..fc779d72 100644 --- a/bevy_rapier3d/examples/testbed3.rs +++ b/bevy_rapier3d/examples/testbed3.rs @@ -8,6 +8,7 @@ mod joints3; mod joints_despawn3; mod locked_rotations3; mod multiple_colliders3; +mod picking3; mod ray_casting3; mod static_trimesh3; @@ -28,6 +29,7 @@ pub enum Examples { JointsDespawn3, LockedRotations3, MultipleColliders3, + Picking3, Raycasting3, StaticTrimesh3, } @@ -64,6 +66,7 @@ fn main() { RapierPhysicsPlugin::::default(), RapierDebugRenderPlugin::default(), WorldInspectorPlugin::new(), + RapierPickingPlugin::default(), )) .register_type::() .register_type::() @@ -78,6 +81,7 @@ fn main() { (Examples::JointsDespawn3, "JointsDespawn3").into(), (Examples::LockedRotations3, "LockedRotations3").into(), (Examples::MultipleColliders3, "MultipleColliders3").into(), + (Examples::Picking3, "Picking3").into(), (Examples::Raycasting3, "Raycasting3").into(), (Examples::StaticTrimesh3, "StaticTrimesh3").into(), ])) @@ -173,6 +177,13 @@ fn main() { ) .add_systems(OnExit(Examples::MultipleColliders3), cleanup) // + // picking + .add_systems( + OnEnter(Examples::Picking3), + (picking3::setup_graphics, picking3::setup_physics), + ) + .add_systems(OnExit(Examples::Picking3), cleanup) + // // raycasting .add_systems( OnEnter(Examples::Raycasting3), diff --git a/bevy_rapier_benches3d/Cargo.toml b/bevy_rapier_benches3d/Cargo.toml index 80c325df..6b4689b4 100644 --- a/bevy_rapier_benches3d/Cargo.toml +++ b/bevy_rapier_benches3d/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rapier3d = { features = ["profiler"], version = "0.22" } +rapier3d = { features = ["profiler"], version = "0.23" } bevy_rapier3d = { version = "0.28", path = "../bevy_rapier3d" } bevy = { version = "0.15", default-features = false } diff --git a/bevy_rapier_benches3d/src/lib.rs b/bevy_rapier_benches3d/src/lib.rs index bd6b16b8..ba24b105 100644 --- a/bevy_rapier_benches3d/src/lib.rs +++ b/bevy_rapier_benches3d/src/lib.rs @@ -25,21 +25,25 @@ pub fn custom_bencher(steps: usize, setup: impl Fn(&mut App)) { timer_full_update.start(); app.update(); timer_full_update.pause(); - let elapsed_time = timer_full_update.time() as f32; + let elapsed_time = timer_full_update.time().as_millis(); let rc = app .world_mut() .query::<&RapierContextSimulation>() .single(app.world()); - rapier_step_times.push(rc.pipeline.counters.step_time.time() as f32); + rapier_step_times.push(rc.pipeline.counters.step_time.time().as_millis() as f32); total_update_times.push(elapsed_time); } timer_total.pause(); - let average_total = total_update_times.iter().sum::() / total_update_times.len() as f32; + let average_total = total_update_times + .iter() + .map(|time| *time as f32) + .sum::() + / total_update_times.len() as f32; println!("average total time: {} ms", average_total); let average_rapier_step = rapier_step_times.iter().sum::() / rapier_step_times.len() as f32; println!("average rapier step time: {} ms", average_rapier_step); let average_rapier_overhead = average_total - average_rapier_step; println!("average bevy overhead: {} ms", average_rapier_overhead); - println!("total time: {} ms", timer_total.time()); + println!("total time: {} ms", timer_total.time().as_millis()); } diff --git a/src/geometry/collider_impl.rs b/src/geometry/collider_impl.rs index 88ead96e..c8892249 100644 --- a/src/geometry/collider_impl.rs +++ b/src/geometry/collider_impl.rs @@ -151,9 +151,12 @@ impl Collider { } /// Initializes a collider with a triangle mesh shape defined by its vertex and index buffers. - pub fn trimesh(vertices: Vec, indices: Vec<[u32; 3]>) -> Self { + pub fn trimesh( + vertices: Vec, + indices: Vec<[u32; 3]>, + ) -> Result { let vertices = vertices.into_iter().map(|v| v.into()).collect(); - SharedShape::trimesh(vertices, indices).into() + Ok(SharedShape::trimesh(vertices, indices)?.into()) } /// Initializes a collider with a triangle mesh shape defined by its vertex and index buffers, and flags @@ -162,9 +165,9 @@ impl Collider { vertices: Vec, indices: Vec<[u32; 3]>, flags: TriMeshFlags, - ) -> Self { + ) -> Result { let vertices = vertices.into_iter().map(|v| v.into()).collect(); - SharedShape::trimesh_with_flags(vertices, indices, flags).into() + Ok(SharedShape::trimesh_with_flags(vertices, indices, flags)?.into()) } /// Initializes a collider with a Bevy Mesh. @@ -175,9 +178,11 @@ impl Collider { let (vtx, idx) = extract_mesh_vertices_indices(mesh)?; match collider_shape { - ComputedColliderShape::TriMesh(flags) => { - Some(SharedShape::trimesh_with_flags(vtx, idx, *flags).into()) - } + ComputedColliderShape::TriMesh(flags) => Some( + SharedShape::trimesh_with_flags(vtx, idx, *flags) + .ok()? + .into(), + ), ComputedColliderShape::ConvexHull => { SharedShape::convex_hull(&vtx).map(|shape| shape.into()) } diff --git a/src/lib.rs b/src/lib.rs index 427c278b..8832c580 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ //! Rapier is a set of two Rust crates `rapier2d` and `rapier3d` for efficient cross-platform //! physics simulation. Its target application include video games, animation, robotics, etc. //! -//! The `bevy_rapier` projects implements two other crates `bevy_rapier2d` and `bevy_rapier3d` which +//! The `bevy_rapier` project implements two crates `bevy_rapier2d` and `bevy_rapier3d` which //! define physics plugins for the Bevy game engine. //! //! User documentation for `bevy_rapier` is on [the official Rapier site](https://rapier.rs/docs/). @@ -57,6 +57,9 @@ pub mod pipeline; /// The physics plugin and systems. pub mod plugin; +#[cfg(feature = "picking-backend")] +pub mod picking_backend; + /// Components related to character control. pub mod control; /// The debug-renderer. @@ -71,6 +74,8 @@ pub mod prelude { pub use crate::dynamics::*; pub use crate::geometry::*; pub use crate::math::*; + #[cfg(feature = "picking-backend")] + pub use crate::picking_backend::*; pub use crate::pipeline::*; pub use crate::plugin::context::systemparams::*; pub use crate::plugin::context::*; diff --git a/src/picking_backend/mod.rs b/src/picking_backend/mod.rs new file mode 100644 index 00000000..04f384cf --- /dev/null +++ b/src/picking_backend/mod.rs @@ -0,0 +1,174 @@ +//! A picking backend for Rapier physics entities. +//! +//! By default, all colliders are pickable. Picking can be disabled for individual entities +//! by adding [`PickingBehavior::IGNORE`](bevy::picking::PickingBehavior::IGNORE). +//! +//! To make rapier picking entirely opt-in, set [`RapierPickingSettings::require_markers`] +//! to `true` and add a [`RapierPickable`] component to the desired camera and target entities. + +use bevy::app::prelude::*; +use bevy::ecs::prelude::*; +use bevy::picking::{ + backend::{ray::RayMap, HitData, PointerHits}, + PickSet, +}; +use bevy::reflect::prelude::*; +use bevy::render::{prelude::*, view::RenderLayers}; + +/// How a ray cast should handle [`Visibility`]. +#[derive(Clone, Copy, Reflect)] +pub enum RapierCastVisibility { + /// Completely ignore visibility checks. Hidden items can still be ray casted against. + Any, + /// Only cast rays against entities that are visible in the hierarchy. See [`Visibility`]. + Visible, +} +/// Runtime settings for the [`RapierPickingPlugin`]. +#[derive(Resource, Reflect)] +#[reflect(Resource, Default)] +pub struct RapierPickingSettings { + /// When set to `true` ray casting will only happen between cameras and entities marked with + /// [`RapierPickable`]. `false` by default. + /// + /// This setting is provided to give you fine-grained control over which cameras and entities + /// should be used by the rapier picking backend at runtime. + pub require_markers: bool, + + /// Determines how rapier picking should consider [`Visibility`]. When set to [`RapierCastVisibility::Any`], + /// ray casts can be performed against both visible and hidden entities. + /// + /// Defaults to [`RapierCastVisibility::Visible`], only performing picking against entities with [`InheritedVisibility`] set to `true`. + pub ray_cast_visibility: RapierCastVisibility, +} + +impl Default for RapierPickingSettings { + fn default() -> Self { + Self { + require_markers: false, + ray_cast_visibility: RapierCastVisibility::Visible, + } + } +} + +/// An optional component that marks cameras and target entities that should be used in the [`RapierPickingPlugin`]. +/// +/// Only needed if [`RapierPickingSettings::require_markers`] is set to `true`, and ignored otherwise. +#[derive(Debug, Clone, Default, Component, Reflect)] +#[reflect(Component, Default)] +pub struct RapierPickable; + +/// Adds the rapier picking backend to your app. +#[derive(Clone, Default)] +pub struct RapierPickingPlugin; + +impl Plugin for RapierPickingPlugin { + fn build(&self, app: &mut App) { + app.register_type::<(RapierPickable, RapierPickingSettings)>() + .init_resource::() + .add_systems(PreUpdate, update_hits.in_set(PickSet::Backend)); + } +} + +/// System which casts rays into the scene using [`RapierPickingSettings`] and sends [`PointerHits`] events. +#[allow(clippy::too_many_arguments)] +pub fn update_hits( + backend_settings: Res, + ray_map: Res, + picking_cameras: Query<(&Camera, Option<&RapierPickable>, Option<&RenderLayers>)>, + marked_targets: Query<&RapierPickable>, + culling_query: Query<(Option<&InheritedVisibility>, Option<&ViewVisibility>)>, + layers: Query<&RenderLayers>, + rapier_context: Query<( + &crate::prelude::RapierContextColliders, + &crate::prelude::RapierRigidBodySet, + &crate::prelude::RapierQueryPipeline, + )>, + mut output: EventWriter, +) { + for (&ray_id, &ray) in ray_map.map().iter() { + let Ok((camera, cam_pickable, cam_layers)) = picking_cameras.get(ray_id.camera) else { + continue; + }; + if backend_settings.require_markers && cam_pickable.is_none() { + continue; + } + let order = camera.order as f32; + for (colliders, bodies, query_pipeline) in rapier_context.iter() { + let predicate = |entity| { + let marker_requirement = + !backend_settings.require_markers || marked_targets.get(entity).is_ok(); + if !marker_requirement { + return false; + } + + let visibility_requirement = match backend_settings.ray_cast_visibility { + RapierCastVisibility::Any => true, + RapierCastVisibility::Visible => { + let is_visible = culling_query + .get(entity) + .map(|(inherited_visibility, _)| { + inherited_visibility.map(|v| v.get()).unwrap_or(false) + }) + .unwrap_or(false); + is_visible + } + }; + if !visibility_requirement { + return false; + } + // Other entities missing render layers are on the default layer 0 + let entity_layers = layers.get(entity).to_owned().unwrap_or_default(); + let cam_layers = cam_layers.to_owned().unwrap_or_default(); + let render_layers_match = cam_layers.intersects(entity_layers); + if !render_layers_match { + return false; + } + + true + }; + + let mut picks = Vec::new(); + #[cfg(feature = "dim2")] + query_pipeline.intersections_with_point( + colliders, + bodies, + bevy::math::Vec2::new(ray.origin.x, ray.origin.y), + crate::prelude::QueryFilter::default().predicate(&predicate), + |entity| { + let hit_data = HitData { + camera: ray_id.camera, + position: Some(bevy::math::Vec3::new(ray.origin.x, ray.origin.y, 0.0)), + normal: None, + depth: 0.0, + }; + picks.push((entity, hit_data)); + true + }, + ); + #[cfg(feature = "dim3")] + query_pipeline.intersections_with_ray( + colliders, + bodies, + ray.origin, + ray.direction.into(), + f32::MAX, + true, + crate::prelude::QueryFilter::default().predicate(&predicate), + |entity, intersection| { + let hit_data = HitData { + camera: ray_id.camera, + position: Some(intersection.point), + normal: Some(intersection.normal), + depth: intersection.time_of_impact, + }; + picks.push((entity, hit_data)); + true + }, + ); + + if !picks.is_empty() { + output.send(PointerHits::new(ray_id.pointer, picks, order)); + } + } + } +} diff --git a/src/pipeline/events.rs b/src/pipeline/events.rs index ff3ccdb0..38df07c5 100644 --- a/src/pipeline/events.rs +++ b/src/pipeline/events.rs @@ -132,7 +132,6 @@ mod test { transform::{components::Transform, TransformPlugin}, MinimalPlugins, }; - use systems::tests::HeadlessRenderPlugin; use crate::{plugin::*, prelude::*}; @@ -180,6 +179,7 @@ mod test { app.insert_resource(TimeUpdateStrategy::ManualDuration( std::time::Duration::from_secs_f32(1f32 / 60f32), )); + app.finish(); // 2 seconds should be plenty of time for the cube to fall on the // lowest collider. for _ in 0..120 { @@ -201,7 +201,6 @@ mod test { fn main() { let mut app = App::new(); app.add_plugins(( - HeadlessRenderPlugin, TransformPlugin, TimePlugin, RapierPhysicsPlugin::::default(), @@ -236,7 +235,6 @@ mod test { pub fn spam_remove_rapier_entity_interpolated() { let mut app = App::new(); app.add_plugins(( - HeadlessRenderPlugin, MinimalPlugins, TransformPlugin, RapierPhysicsPlugin::::default(), @@ -253,6 +251,8 @@ mod test { std::time::Duration::from_secs_f32(1f32 / 60f32), )); + app.finish(); + for _ in 0..100 { app.update(); } diff --git a/src/plugin/plugin.rs b/src/plugin/plugin.rs index f16ef7bf..8ea0a18f 100644 --- a/src/plugin/plugin.rs +++ b/src/plugin/plugin.rs @@ -304,6 +304,22 @@ where } } } + + fn finish(&self, _app: &mut App) { + #[cfg(feature = "async-collider")] + { + use bevy::{asset::AssetPlugin, render::mesh::MeshPlugin, scene::ScenePlugin}; + if !_app.is_plugin_added::() { + _app.add_plugins(AssetPlugin::default()); + } + if !_app.is_plugin_added::() { + _app.add_plugins(MeshPlugin); + } + if !_app.is_plugin_added::() { + _app.add_plugins(ScenePlugin); + } + } + } } /// Specifies a default configuration for the default [`RapierContext`] @@ -374,7 +390,6 @@ mod test { time::{TimePlugin, TimeUpdateStrategy}, }; use rapier::{data::Index, dynamics::RigidBodyHandle}; - use systems::tests::HeadlessRenderPlugin; use crate::{plugin::context::*, plugin::*, prelude::*}; @@ -403,6 +418,8 @@ mod test { app.add_systems(Update, setup_physics); + app.finish(); + let mut stepping = Stepping::new(); app.update(); @@ -478,7 +495,6 @@ mod test { fn main() { let mut app = App::new(); app.add_plugins(( - HeadlessRenderPlugin, TransformPlugin, TimePlugin, RapierPhysicsPlugin::::default(), @@ -526,6 +542,7 @@ mod test { app.add_systems(Update, remove_rapier_entity); app.add_systems(FixedUpdate, || println!("Fixed Update")); app.add_systems(Update, || println!("Update")); + app.finish(); // startup app.update(); // normal updates starting @@ -559,7 +576,6 @@ mod test { fn main() { let mut app = App::new(); app.add_plugins(( - HeadlessRenderPlugin, TransformPlugin, TimePlugin, RapierPhysicsPlugin::::default().in_fixed_schedule(), diff --git a/src/plugin/systems/collider.rs b/src/plugin/systems/collider.rs index c371b556..c43fe5f6 100644 --- a/src/plugin/systems/collider.rs +++ b/src/plugin/systems/collider.rs @@ -589,11 +589,13 @@ pub mod test { #[cfg(all(feature = "dim3", feature = "async-collider"))] fn async_collider_initializes() { use super::*; - use crate::plugin::systems::tests::HeadlessRenderPlugin; + use bevy::{render::mesh::MeshPlugin, scene::ScenePlugin}; let mut app = App::new(); - app.add_plugins(HeadlessRenderPlugin) - .add_systems(Update, init_async_colliders); + app.add_plugins((AssetPlugin::default(), MeshPlugin, ScenePlugin)); + app.add_systems(Update, init_async_colliders); + + app.finish(); let mut meshes = app.world_mut().resource_mut::>(); let cube = meshes.add(Cuboid::default()); @@ -620,11 +622,11 @@ pub mod test { #[cfg(all(feature = "dim3", feature = "async-collider"))] fn async_scene_collider_initializes() { use super::*; - use crate::plugin::systems::tests::HeadlessRenderPlugin; + use bevy::{render::mesh::MeshPlugin, scene::ScenePlugin}; let mut app = App::new(); - app.add_plugins(HeadlessRenderPlugin) - .add_systems(PostUpdate, init_async_scene_colliders); + app.add_plugins((AssetPlugin::default(), MeshPlugin, ScenePlugin)); + app.add_systems(PostUpdate, init_async_scene_colliders); let mut meshes = app.world_mut().resource_mut::>(); let cube_handle = meshes.add(Cuboid::default()); diff --git a/src/plugin/systems/joint.rs b/src/plugin/systems/joint.rs index c62b7774..9bd364b5 100644 --- a/src/plugin/systems/joint.rs +++ b/src/plugin/systems/joint.rs @@ -138,7 +138,7 @@ pub fn apply_joint_user_changes( // Re-parenting the joint isn’t supported yet. for (link, handle, changed_joint) in changed_impulse_joints.iter() { let mut context = context.get_mut(link.0).expect(RAPIER_CONTEXT_EXPECT_ERROR); - if let Some(joint) = context.impulse_joints.get_mut(handle.0) { + if let Some(joint) = context.impulse_joints.get_mut(handle.0, false) { joint.data = changed_joint.data.as_ref().into_rapier(); } } diff --git a/src/plugin/systems/mod.rs b/src/plugin/systems/mod.rs index 420876f4..ef2d03bf 100644 --- a/src/plugin/systems/mod.rs +++ b/src/plugin/systems/mod.rs @@ -93,16 +93,7 @@ pub fn step_simulation( #[cfg(test)] #[allow(missing_docs)] pub mod tests { - use bevy::{ - asset::AssetPlugin, - ecs::event::Events, - render::{ - settings::{RenderCreation, WgpuSettings}, - RenderPlugin, - }, - scene::ScenePlugin, - time::TimePlugin, - }; + use bevy::{ecs::event::Events, time::TimePlugin}; use rapier::geometry::CollisionEventFlags; use std::f32::consts::PI; @@ -203,11 +194,11 @@ pub mod tests { fn transform_propagation() { let mut app = App::new(); app.add_plugins(( - HeadlessRenderPlugin, TransformPlugin, TimePlugin, RapierPhysicsPlugin::::default(), )); + app.finish(); let zero = (Transform::default(), Transform::default()); @@ -260,11 +251,11 @@ pub mod tests { fn transform_propagation2() { let mut app = App::new(); app.add_plugins(( - HeadlessRenderPlugin, TransformPlugin, TimePlugin, RapierPhysicsPlugin::::default(), )); + app.finish(); let zero = (Transform::default(), Transform::default()); @@ -342,24 +333,4 @@ pub mod tests { approx::assert_relative_eq!(body_transform.scale, child_transform.scale,); } } - - // Allows run tests for systems containing rendering related things without GPU - pub struct HeadlessRenderPlugin; - - impl Plugin for HeadlessRenderPlugin { - fn build(&self, app: &mut App) { - app.add_plugins(( - AssetPlugin::default(), - ScenePlugin, - RenderPlugin { - render_creation: RenderCreation::Automatic(WgpuSettings { - backends: None, - ..Default::default() - }), - ..Default::default() - }, - ImagePlugin::default(), - )); - } - } } diff --git a/src/plugin/systems/multiple_rapier_contexts.rs b/src/plugin/systems/multiple_rapier_contexts.rs index b24bccb9..a266425d 100644 --- a/src/plugin/systems/multiple_rapier_contexts.rs +++ b/src/plugin/systems/multiple_rapier_contexts.rs @@ -130,7 +130,6 @@ fn bubble_down_context_change( #[cfg(test)] mod test { - use crate::plugin::systems::tests::HeadlessRenderPlugin; use crate::plugin::{ context::{RapierContextEntityLink, RapierContextSimulation}, NoUserData, PhysicsSet, RapierPhysicsPlugin, @@ -144,7 +143,6 @@ mod test { pub fn multi_context_hierarchy_update() { let mut app = App::new(); app.add_plugins(( - HeadlessRenderPlugin, TransformPlugin, TimePlugin, RapierPhysicsPlugin::::default(), @@ -159,6 +157,7 @@ mod test { app.insert_resource(TimeUpdateStrategy::ManualDuration( std::time::Duration::from_secs_f32(1f32 / 60f32), )); + app.finish(); app.update(); // Verify all rapier entities have a `RapierContextEntityLink`. let world = app.world_mut();