diff --git a/CHANGELOG.md b/CHANGELOG.md index 71576bec..88bef488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - The region key has been replaced by an i64 in the f64 version of rapier, increasing the range before panics occur. - Fix `BroadphaseMultiSap` not being able to serialize correctly with serde_json. +- Fix `KinematicCharacterController::move_shape` not respecting parameters `max_slope_climb_angle` and `min_slope_slide_angle`. ### Added diff --git a/crates/rapier_testbed2d-f64/Cargo.toml b/crates/rapier_testbed2d-f64/Cargo.toml index 04077ea3..9770080b 100644 --- a/crates/rapier_testbed2d-f64/Cargo.toml +++ b/crates/rapier_testbed2d-f64/Cargo.toml @@ -50,7 +50,6 @@ crossbeam = "0.8" bincode = "1" Inflector = "0.11" md5 = "0.7" - bevy_egui = "0.29" bevy_ecs = "0.14" bevy_core_pipeline = "0.14" diff --git a/crates/rapier_testbed2d/Cargo.toml b/crates/rapier_testbed2d/Cargo.toml index 34028ba4..77373bd4 100644 --- a/crates/rapier_testbed2d/Cargo.toml +++ b/crates/rapier_testbed2d/Cargo.toml @@ -50,7 +50,6 @@ crossbeam = "0.8" bincode = "1" Inflector = "0.11" md5 = "0.7" - bevy_egui = "0.29" bevy_ecs = "0.14" bevy_core_pipeline = "0.14" diff --git a/crates/rapier_testbed3d-f64/Cargo.toml b/crates/rapier_testbed3d-f64/Cargo.toml index f345f89b..3fa2511f 100644 --- a/crates/rapier_testbed3d-f64/Cargo.toml +++ b/crates/rapier_testbed3d-f64/Cargo.toml @@ -52,7 +52,6 @@ bincode = "1" md5 = "0.7" Inflector = "0.11" serde = { version = "1", features = ["derive"] } - bevy_egui = "0.29" bevy_ecs = "0.14" bevy_core_pipeline = "0.14" diff --git a/crates/rapier_testbed3d/Cargo.toml b/crates/rapier_testbed3d/Cargo.toml index da55229f..a7a3fe85 100644 --- a/crates/rapier_testbed3d/Cargo.toml +++ b/crates/rapier_testbed3d/Cargo.toml @@ -53,7 +53,6 @@ bincode = "1" md5 = "0.7" Inflector = "0.11" serde = { version = "1", features = ["derive"] } - bevy_egui = "0.29" bevy_ecs = "0.14" bevy_core_pipeline = "0.14" diff --git a/examples3d/character_controller3.rs b/examples3d/character_controller3.rs index f6684390..ce96bf8a 100644 --- a/examples3d/character_controller3.rs +++ b/examples3d/character_controller3.rs @@ -1,4 +1,4 @@ -use rapier3d::prelude::*; +use rapier3d::{control::KinematicCharacterController, prelude::*}; use rapier_testbed3d::Testbed; pub fn init_world(testbed: &mut Testbed) { @@ -41,7 +41,7 @@ pub fn init_world(testbed: &mut Testbed) { * Character we will control manually. */ let rigid_body = - RigidBodyBuilder::kinematic_position_based().translation(vector![-3.0, 5.0, 0.0] * scale); + RigidBodyBuilder::kinematic_position_based().translation(vector![0.0, 0.5, 0.0] * scale); let character_handle = bodies.insert(rigid_body); let collider = ColliderBuilder::capsule_y(0.3 * scale, 0.15 * scale); // 0.15, 0.3, 0.15); colliders.insert_with_parent(collider, character_handle, &mut bodies); @@ -95,19 +95,15 @@ pub fn init_world(testbed: &mut Testbed) { */ let slope_angle = 0.2; let slope_size = 2.0; - let collider = ColliderBuilder::cuboid( - slope_size * scale, - ground_height * scale, - slope_size * scale, - ) - .translation(vector![ground_size + slope_size, -ground_height + 0.4, 0.0] * scale) - .rotation(Vector::z() * slope_angle); + let collider = ColliderBuilder::cuboid(slope_size, ground_height, slope_size) + .translation(vector![0.1 + slope_size, -ground_height + 0.4, 0.0]) + .rotation(Vector::z() * slope_angle); colliders.insert(collider); /* * Create a slope we can’t climb. */ - let impossible_slope_angle = 0.9; + let impossible_slope_angle = 0.6; let impossible_slope_size = 2.0; let collider = ColliderBuilder::cuboid( slope_size * scale, @@ -116,8 +112,8 @@ pub fn init_world(testbed: &mut Testbed) { ) .translation( vector![ - ground_size + slope_size * 2.0 + impossible_slope_size - 0.9, - -ground_height + 2.3, + 0.1 + slope_size * 2.0 + impossible_slope_size - 0.9, + -ground_height + 1.7, 0.0 ] * scale, ) @@ -185,5 +181,11 @@ pub fn init_world(testbed: &mut Testbed) { */ testbed.set_world(bodies, colliders, impulse_joints, multibody_joints); testbed.set_character_body(character_handle); + testbed.set_character_controller(Some(KinematicCharacterController { + max_slope_climb_angle: impossible_slope_angle - 0.02, + min_slope_slide_angle: impossible_slope_angle - 0.02, + slide: true, + ..Default::default() + })); testbed.look_at(point!(10.0, 10.0, 10.0), Point::origin()); } diff --git a/examples3d/debug_internal_edges3.rs b/examples3d/debug_internal_edges3.rs index 86de1bdb..48f84927 100644 --- a/examples3d/debug_internal_edges3.rs +++ b/examples3d/debug_internal_edges3.rs @@ -37,7 +37,7 @@ pub fn init_world(testbed: &mut Testbed) { colliders.insert_with_parent(collider, handle, &mut bodies); let rigid_body = RigidBodyBuilder::dynamic() - .translation(vector![0.0, 0.5, 0.0]) + .translation(vector![-3.0, 5.0, 0.0]) .linvel(vector![0.0, -4.0, 20.0]) .can_sleep(false); let handle = bodies.insert(rigid_body); diff --git a/src/control/character_controller.rs b/src/control/character_controller.rs index e2b48004..c8a15c5f 100644 --- a/src/control/character_controller.rs +++ b/src/control/character_controller.rs @@ -169,6 +169,7 @@ impl Default for KinematicCharacterController { } /// The effective movement computed by the character controller. +#[derive(Debug)] pub struct EffectiveCharacterMovement { /// The movement to apply. pub translation: Vector, @@ -542,17 +543,17 @@ impl KinematicCharacterController { ) -> Vector { let [_vertical_input, horizontal_input] = self.split_into_components(movement_input); let horiz_input_decomp = self.decompose_hit(&horizontal_input, &hit.toi); - let input_decomp = self.decompose_hit(movement_input, &hit.toi); - let decomp = self.decompose_hit(translation_remaining, &hit.toi); // An object is trying to slip if the tangential movement induced by its vertical movement // points downward. let slipping_intent = self.up.dot(&horiz_input_decomp.vertical_tangent) < 0.0; + // An object is slipping if its vertical movement points downward. let slipping = self.up.dot(&decomp.vertical_tangent) < 0.0; - // An object is trying to climb if its indirect vertical motion points upward. - let climbing_intent = self.up.dot(&input_decomp.vertical_tangent) > 0.0; + // An object is trying to climb if its vertical input motion points upward. + let climbing_intent = self.up.dot(&_vertical_input) > 0.0; + // An object is climbing if the tangential movement induced by its vertical movement points upward. let climbing = self.up.dot(&decomp.vertical_tangent) > 0.0; let allowed_movement = if hit.is_wall && climbing && !climbing_intent { @@ -904,3 +905,151 @@ fn subtract_hit(translation: Vector, hit: &ShapeCastHit) -> Vector { let surface_correction = surface_correction * (1.0 + 1.0e-5); translation + *hit.normal1 * surface_correction } + +#[cfg(all(feature = "dim3", feature = "f32"))] +#[cfg(test)] +mod test { + use crate::{control::KinematicCharacterController, prelude::*}; + + #[test] + fn character_controller_climb_test() { + let mut colliders = ColliderSet::new(); + let mut impulse_joints = ImpulseJointSet::new(); + let mut multibody_joints = MultibodyJointSet::new(); + let mut pipeline = PhysicsPipeline::new(); + let mut bf = BroadPhaseMultiSap::new(); + let mut nf = NarrowPhase::new(); + let mut islands = IslandManager::new(); + let mut query_pipeline = QueryPipeline::new(); + + let mut bodies = RigidBodySet::new(); + + let gravity = Vector::y() * -9.81; + + let ground_size = 100.0; + let ground_height = 0.1; + /* + * Create a flat ground + */ + let rigid_body = RigidBodyBuilder::fixed().translation(vector![0.0, -ground_height, 0.0]); + let floor_handle = bodies.insert(rigid_body); + let collider = ColliderBuilder::cuboid(ground_size, ground_height, ground_size); + colliders.insert_with_parent(collider, floor_handle, &mut bodies); + + /* + * Create a slope we can climb. + */ + let slope_angle = 0.2; + let slope_size = 2.0; + let collider = ColliderBuilder::cuboid(slope_size, ground_height, slope_size) + .translation(vector![0.1 + slope_size, -ground_height + 0.4, 0.0]) + .rotation(Vector::z() * slope_angle); + colliders.insert(collider); + + /* + * Create a slope we can’t climb. + */ + let impossible_slope_angle = 0.6; + let impossible_slope_size = 2.0; + let collider = ColliderBuilder::cuboid(slope_size, ground_height, ground_size) + .translation(vector![ + 0.1 + slope_size * 2.0 + impossible_slope_size - 0.9, + -ground_height + 1.7, + 0.0 + ]) + .rotation(Vector::z() * impossible_slope_angle); + colliders.insert(collider); + + let integration_parameters = IntegrationParameters::default(); + + // Initialize character which can climb + let mut character_body_can_climb = RigidBodyBuilder::kinematic_position_based() + .additional_mass(1.0) + .build(); + character_body_can_climb.set_translation(Vector::new(0.6, 0.5, 0.0), false); + let character_handle_can_climb = bodies.insert(character_body_can_climb); + + let collider = ColliderBuilder::ball(0.5).build(); + colliders.insert_with_parent(collider.clone(), character_handle_can_climb, &mut bodies); + + // Initialize character which cannot climb + let mut character_body_cannot_climb = RigidBodyBuilder::kinematic_position_based() + .additional_mass(1.0) + .build(); + character_body_cannot_climb.set_translation(Vector::new(-0.6, 0.5, 0.0), false); + let character_handle_cannot_climb = bodies.insert(character_body_cannot_climb); + + let collider = ColliderBuilder::ball(0.5).build(); + let character_shape = collider.shape(); + colliders.insert_with_parent(collider.clone(), character_handle_cannot_climb, &mut bodies); + + query_pipeline.update(&colliders); + for i in 0..200 { + let mut update_character_controller = + |controller: KinematicCharacterController, handle: RigidBodyHandle| { + let character_body = bodies.get(handle).unwrap(); + // Use a closure to handle or collect the collisions while + // the character is being moved. + let mut collisions = vec![]; + let filter_character_controller = QueryFilter::new().exclude_rigid_body(handle); + let effective_movement = controller.move_shape( + integration_parameters.dt, + &bodies, + &colliders, + &query_pipeline, + character_shape, + character_body.position(), + Vector::new(0.1, -0.1, 0.0), + filter_character_controller, + |collision| collisions.push(collision), + ); + let character_body = bodies.get_mut(handle).unwrap(); + let translation = character_body.translation(); + assert_eq!( + effective_movement.grounded, true, + "movement should be grounded at all times for current setup (iter: {}), pos: {}.", + i, translation + effective_movement.translation + ); + character_body.set_next_kinematic_translation( + translation + effective_movement.translation, + ); + }; + + let character_controller_cannot_climb = KinematicCharacterController { + max_slope_climb_angle: impossible_slope_angle - 0.001, + ..Default::default() + }; + let character_controller_can_climb = KinematicCharacterController { + max_slope_climb_angle: impossible_slope_angle + 0.001, + ..Default::default() + }; + update_character_controller( + character_controller_cannot_climb, + character_handle_cannot_climb, + ); + update_character_controller(character_controller_can_climb, character_handle_can_climb); + // Step once + pipeline.step( + &gravity, + &integration_parameters, + &mut islands, + &mut bf, + &mut nf, + &mut bodies, + &mut colliders, + &mut impulse_joints, + &mut multibody_joints, + &mut CCDSolver::new(), + Some(&mut query_pipeline), + &(), + &(), + ); + } + let character_body = bodies.get(character_handle_can_climb).unwrap(); + assert!(character_body.translation().x > 6.0); + assert!(character_body.translation().y > 3.0); + let character_body = bodies.get(character_handle_cannot_climb).unwrap(); + assert!(character_body.translation().x < 4.0); + assert!(dbg!(character_body.translation().y) < 2.0); + } +} diff --git a/src_testbed/testbed.rs b/src_testbed/testbed.rs index 879eb848..7b2f0fdc 100644 --- a/src_testbed/testbed.rs +++ b/src_testbed/testbed.rs @@ -102,6 +102,7 @@ pub struct TestbedState { pub draw_colls: bool, pub highlighted_body: Option, pub character_body: Option, + pub character_controller: Option, #[cfg(feature = "dim3")] pub vehicle_controller: Option, // pub grabbed_object: Option, @@ -186,6 +187,7 @@ impl TestbedApp { draw_colls: false, highlighted_body: None, character_body: None, + character_controller: None, #[cfg(feature = "dim3")] vehicle_controller: None, // grabbed_object: None, @@ -530,6 +532,10 @@ impl<'a, 'b, 'c, 'd, 'e, 'f> Testbed<'a, 'b, 'c, 'd, 'e, 'f> { self.state.character_body = Some(handle); } + pub fn set_character_controller(&mut self, controller: Option) { + self.state.character_controller = controller; + } + #[cfg(feature = "dim3")] pub fn set_vehicle_controller(&mut self, controller: DynamicRayCastVehicleController) { self.state.vehicle_controller = Some(controller); @@ -827,7 +833,7 @@ impl<'a, 'b, 'c, 'd, 'e, 'f> Testbed<'a, 'b, 'c, 'd, 'e, 'f> { desired_movement *= speed; desired_movement -= Vector::y() * speed; - let controller = KinematicCharacterController::default(); + let controller = self.state.character_controller.unwrap_or_default(); let phx = &mut self.harness.physics; let character_body = &phx.bodies[character_handle]; let character_collider = &phx.colliders[character_body.colliders()[0]]; diff --git a/src_testbed/ui.rs b/src_testbed/ui.rs index aa9dc7a7..b27ea7a6 100644 --- a/src_testbed/ui.rs +++ b/src_testbed/ui.rs @@ -1,3 +1,4 @@ +use rapier::control::CharacterLength; use rapier::counters::Counters; use rapier::math::Real; use std::num::NonZeroUsize; @@ -240,7 +241,40 @@ pub fn update_ui( // .set(TestbedStateFlags::CONTACT_POINTS, contact_points); // state.flags.set(TestbedStateFlags::WIREFRAME, wireframe); ui.separator(); + if let Some(character_controller) = &mut state.character_controller { + ui.label("Character controller"); + ui.checkbox(&mut character_controller.slide, "slide").on_hover_text("Should the character try to slide against the floor if it hits it?"); + #[allow(clippy::useless_conversion)] + { + ui.add(Slider::new(&mut character_controller.max_slope_climb_angle, 0.0..=std::f32::consts::TAU.into()).text("max_slope_climb_angle")) + .on_hover_text("The maximum angle (radians) between the floor’s normal and the `up` vector that the character is able to climb."); + ui.add(Slider::new(&mut character_controller.min_slope_slide_angle, 0.0..=std::f32::consts::FRAC_PI_2.into()).text("min_slope_slide_angle")) + .on_hover_text("The minimum angle (radians) between the floor’s normal and the `up` vector before the character starts to slide down automatically."); + } + let mut is_snapped = character_controller.snap_to_ground.is_some(); + if ui.checkbox(&mut is_snapped, "snap_to_ground").changed { + match is_snapped { + true => { + character_controller.snap_to_ground = Some(CharacterLength::Relative(0.1)); + }, + false => { + character_controller.snap_to_ground = None; + }, + } + } + if let Some(snapped) = &mut character_controller.snap_to_ground { + match snapped { + CharacterLength::Relative(val) => { + ui.add(Slider::new(val, 0.0..=10.0).text("Snapped Relative Character Length")); + }, + CharacterLength::Absolute(val) => { + ui.add(Slider::new(val, 0.0..=10.0).text("Snapped Absolute Character Length")); + }, + } + } + ui.separator(); + } let label = if state.running == RunMode::Stop { "Start (T)" } else {