Skip to content

Commit 76357e3

Browse files
authored
Fix CharacterController max/min slope handling (#701)
1 parent e7e196d commit 76357e3

File tree

10 files changed

+210
-22
lines changed

10 files changed

+210
-22
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- The region key has been replaced by an i64 in the f64 version of rapier, increasing the range before panics occur.
66
- Fix `BroadphaseMultiSap` not being able to serialize correctly with serde_json.
7+
- Fix `KinematicCharacterController::move_shape` not respecting parameters `max_slope_climb_angle` and `min_slope_slide_angle`.
78

89
### Added
910

crates/rapier_testbed2d-f64/Cargo.toml

-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ crossbeam = "0.8"
5050
bincode = "1"
5151
Inflector = "0.11"
5252
md5 = "0.7"
53-
5453
bevy_egui = "0.29"
5554
bevy_ecs = "0.14"
5655
bevy_core_pipeline = "0.14"

crates/rapier_testbed2d/Cargo.toml

-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ crossbeam = "0.8"
5050
bincode = "1"
5151
Inflector = "0.11"
5252
md5 = "0.7"
53-
5453
bevy_egui = "0.29"
5554
bevy_ecs = "0.14"
5655
bevy_core_pipeline = "0.14"

crates/rapier_testbed3d-f64/Cargo.toml

-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ bincode = "1"
5252
md5 = "0.7"
5353
Inflector = "0.11"
5454
serde = { version = "1", features = ["derive"] }
55-
5655
bevy_egui = "0.29"
5756
bevy_ecs = "0.14"
5857
bevy_core_pipeline = "0.14"

crates/rapier_testbed3d/Cargo.toml

-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ bincode = "1"
5353
md5 = "0.7"
5454
Inflector = "0.11"
5555
serde = { version = "1", features = ["derive"] }
56-
5756
bevy_egui = "0.29"
5857
bevy_ecs = "0.14"
5958
bevy_core_pipeline = "0.14"

examples3d/character_controller3.rs

+14-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use rapier3d::prelude::*;
1+
use rapier3d::{control::KinematicCharacterController, prelude::*};
22
use rapier_testbed3d::Testbed;
33

44
pub fn init_world(testbed: &mut Testbed) {
@@ -41,7 +41,7 @@ pub fn init_world(testbed: &mut Testbed) {
4141
* Character we will control manually.
4242
*/
4343
let rigid_body =
44-
RigidBodyBuilder::kinematic_position_based().translation(vector![-3.0, 5.0, 0.0] * scale);
44+
RigidBodyBuilder::kinematic_position_based().translation(vector![0.0, 0.5, 0.0] * scale);
4545
let character_handle = bodies.insert(rigid_body);
4646
let collider = ColliderBuilder::capsule_y(0.3 * scale, 0.15 * scale); // 0.15, 0.3, 0.15);
4747
colliders.insert_with_parent(collider, character_handle, &mut bodies);
@@ -95,19 +95,15 @@ pub fn init_world(testbed: &mut Testbed) {
9595
*/
9696
let slope_angle = 0.2;
9797
let slope_size = 2.0;
98-
let collider = ColliderBuilder::cuboid(
99-
slope_size * scale,
100-
ground_height * scale,
101-
slope_size * scale,
102-
)
103-
.translation(vector![ground_size + slope_size, -ground_height + 0.4, 0.0] * scale)
104-
.rotation(Vector::z() * slope_angle);
98+
let collider = ColliderBuilder::cuboid(slope_size, ground_height, slope_size)
99+
.translation(vector![0.1 + slope_size, -ground_height + 0.4, 0.0])
100+
.rotation(Vector::z() * slope_angle);
105101
colliders.insert(collider);
106102

107103
/*
108104
* Create a slope we can’t climb.
109105
*/
110-
let impossible_slope_angle = 0.9;
106+
let impossible_slope_angle = 0.6;
111107
let impossible_slope_size = 2.0;
112108
let collider = ColliderBuilder::cuboid(
113109
slope_size * scale,
@@ -116,8 +112,8 @@ pub fn init_world(testbed: &mut Testbed) {
116112
)
117113
.translation(
118114
vector![
119-
ground_size + slope_size * 2.0 + impossible_slope_size - 0.9,
120-
-ground_height + 2.3,
115+
0.1 + slope_size * 2.0 + impossible_slope_size - 0.9,
116+
-ground_height + 1.7,
121117
0.0
122118
] * scale,
123119
)
@@ -185,5 +181,11 @@ pub fn init_world(testbed: &mut Testbed) {
185181
*/
186182
testbed.set_world(bodies, colliders, impulse_joints, multibody_joints);
187183
testbed.set_character_body(character_handle);
184+
testbed.set_character_controller(Some(KinematicCharacterController {
185+
max_slope_climb_angle: impossible_slope_angle - 0.02,
186+
min_slope_slide_angle: impossible_slope_angle - 0.02,
187+
slide: true,
188+
..Default::default()
189+
}));
188190
testbed.look_at(point!(10.0, 10.0, 10.0), Point::origin());
189191
}

examples3d/debug_internal_edges3.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ pub fn init_world(testbed: &mut Testbed) {
3737
colliders.insert_with_parent(collider, handle, &mut bodies);
3838

3939
let rigid_body = RigidBodyBuilder::dynamic()
40-
.translation(vector![0.0, 0.5, 0.0])
40+
.translation(vector![-3.0, 5.0, 0.0])
4141
.linvel(vector![0.0, -4.0, 20.0])
4242
.can_sleep(false);
4343
let handle = bodies.insert(rigid_body);

src/control/character_controller.rs

+153-4
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ impl Default for KinematicCharacterController {
169169
}
170170

171171
/// The effective movement computed by the character controller.
172+
#[derive(Debug)]
172173
pub struct EffectiveCharacterMovement {
173174
/// The movement to apply.
174175
pub translation: Vector<Real>,
@@ -542,17 +543,17 @@ impl KinematicCharacterController {
542543
) -> Vector<Real> {
543544
let [_vertical_input, horizontal_input] = self.split_into_components(movement_input);
544545
let horiz_input_decomp = self.decompose_hit(&horizontal_input, &hit.toi);
545-
let input_decomp = self.decompose_hit(movement_input, &hit.toi);
546-
547546
let decomp = self.decompose_hit(translation_remaining, &hit.toi);
548547

549548
// An object is trying to slip if the tangential movement induced by its vertical movement
550549
// points downward.
551550
let slipping_intent = self.up.dot(&horiz_input_decomp.vertical_tangent) < 0.0;
551+
// An object is slipping if its vertical movement points downward.
552552
let slipping = self.up.dot(&decomp.vertical_tangent) < 0.0;
553553

554-
// An object is trying to climb if its indirect vertical motion points upward.
555-
let climbing_intent = self.up.dot(&input_decomp.vertical_tangent) > 0.0;
554+
// An object is trying to climb if its vertical input motion points upward.
555+
let climbing_intent = self.up.dot(&_vertical_input) > 0.0;
556+
// An object is climbing if the tangential movement induced by its vertical movement points upward.
556557
let climbing = self.up.dot(&decomp.vertical_tangent) > 0.0;
557558

558559
let allowed_movement = if hit.is_wall && climbing && !climbing_intent {
@@ -904,3 +905,151 @@ fn subtract_hit(translation: Vector<Real>, hit: &ShapeCastHit) -> Vector<Real> {
904905
let surface_correction = surface_correction * (1.0 + 1.0e-5);
905906
translation + *hit.normal1 * surface_correction
906907
}
908+
909+
#[cfg(all(feature = "dim3", feature = "f32"))]
910+
#[cfg(test)]
911+
mod test {
912+
use crate::{control::KinematicCharacterController, prelude::*};
913+
914+
#[test]
915+
fn character_controller_climb_test() {
916+
let mut colliders = ColliderSet::new();
917+
let mut impulse_joints = ImpulseJointSet::new();
918+
let mut multibody_joints = MultibodyJointSet::new();
919+
let mut pipeline = PhysicsPipeline::new();
920+
let mut bf = BroadPhaseMultiSap::new();
921+
let mut nf = NarrowPhase::new();
922+
let mut islands = IslandManager::new();
923+
let mut query_pipeline = QueryPipeline::new();
924+
925+
let mut bodies = RigidBodySet::new();
926+
927+
let gravity = Vector::y() * -9.81;
928+
929+
let ground_size = 100.0;
930+
let ground_height = 0.1;
931+
/*
932+
* Create a flat ground
933+
*/
934+
let rigid_body = RigidBodyBuilder::fixed().translation(vector![0.0, -ground_height, 0.0]);
935+
let floor_handle = bodies.insert(rigid_body);
936+
let collider = ColliderBuilder::cuboid(ground_size, ground_height, ground_size);
937+
colliders.insert_with_parent(collider, floor_handle, &mut bodies);
938+
939+
/*
940+
* Create a slope we can climb.
941+
*/
942+
let slope_angle = 0.2;
943+
let slope_size = 2.0;
944+
let collider = ColliderBuilder::cuboid(slope_size, ground_height, slope_size)
945+
.translation(vector![0.1 + slope_size, -ground_height + 0.4, 0.0])
946+
.rotation(Vector::z() * slope_angle);
947+
colliders.insert(collider);
948+
949+
/*
950+
* Create a slope we can’t climb.
951+
*/
952+
let impossible_slope_angle = 0.6;
953+
let impossible_slope_size = 2.0;
954+
let collider = ColliderBuilder::cuboid(slope_size, ground_height, ground_size)
955+
.translation(vector![
956+
0.1 + slope_size * 2.0 + impossible_slope_size - 0.9,
957+
-ground_height + 1.7,
958+
0.0
959+
])
960+
.rotation(Vector::z() * impossible_slope_angle);
961+
colliders.insert(collider);
962+
963+
let integration_parameters = IntegrationParameters::default();
964+
965+
// Initialize character which can climb
966+
let mut character_body_can_climb = RigidBodyBuilder::kinematic_position_based()
967+
.additional_mass(1.0)
968+
.build();
969+
character_body_can_climb.set_translation(Vector::new(0.6, 0.5, 0.0), false);
970+
let character_handle_can_climb = bodies.insert(character_body_can_climb);
971+
972+
let collider = ColliderBuilder::ball(0.5).build();
973+
colliders.insert_with_parent(collider.clone(), character_handle_can_climb, &mut bodies);
974+
975+
// Initialize character which cannot climb
976+
let mut character_body_cannot_climb = RigidBodyBuilder::kinematic_position_based()
977+
.additional_mass(1.0)
978+
.build();
979+
character_body_cannot_climb.set_translation(Vector::new(-0.6, 0.5, 0.0), false);
980+
let character_handle_cannot_climb = bodies.insert(character_body_cannot_climb);
981+
982+
let collider = ColliderBuilder::ball(0.5).build();
983+
let character_shape = collider.shape();
984+
colliders.insert_with_parent(collider.clone(), character_handle_cannot_climb, &mut bodies);
985+
986+
query_pipeline.update(&colliders);
987+
for i in 0..200 {
988+
let mut update_character_controller =
989+
|controller: KinematicCharacterController, handle: RigidBodyHandle| {
990+
let character_body = bodies.get(handle).unwrap();
991+
// Use a closure to handle or collect the collisions while
992+
// the character is being moved.
993+
let mut collisions = vec![];
994+
let filter_character_controller = QueryFilter::new().exclude_rigid_body(handle);
995+
let effective_movement = controller.move_shape(
996+
integration_parameters.dt,
997+
&bodies,
998+
&colliders,
999+
&query_pipeline,
1000+
character_shape,
1001+
character_body.position(),
1002+
Vector::new(0.1, -0.1, 0.0),
1003+
filter_character_controller,
1004+
|collision| collisions.push(collision),
1005+
);
1006+
let character_body = bodies.get_mut(handle).unwrap();
1007+
let translation = character_body.translation();
1008+
assert_eq!(
1009+
effective_movement.grounded, true,
1010+
"movement should be grounded at all times for current setup (iter: {}), pos: {}.",
1011+
i, translation + effective_movement.translation
1012+
);
1013+
character_body.set_next_kinematic_translation(
1014+
translation + effective_movement.translation,
1015+
);
1016+
};
1017+
1018+
let character_controller_cannot_climb = KinematicCharacterController {
1019+
max_slope_climb_angle: impossible_slope_angle - 0.001,
1020+
..Default::default()
1021+
};
1022+
let character_controller_can_climb = KinematicCharacterController {
1023+
max_slope_climb_angle: impossible_slope_angle + 0.001,
1024+
..Default::default()
1025+
};
1026+
update_character_controller(
1027+
character_controller_cannot_climb,
1028+
character_handle_cannot_climb,
1029+
);
1030+
update_character_controller(character_controller_can_climb, character_handle_can_climb);
1031+
// Step once
1032+
pipeline.step(
1033+
&gravity,
1034+
&integration_parameters,
1035+
&mut islands,
1036+
&mut bf,
1037+
&mut nf,
1038+
&mut bodies,
1039+
&mut colliders,
1040+
&mut impulse_joints,
1041+
&mut multibody_joints,
1042+
&mut CCDSolver::new(),
1043+
Some(&mut query_pipeline),
1044+
&(),
1045+
&(),
1046+
);
1047+
}
1048+
let character_body = bodies.get(character_handle_can_climb).unwrap();
1049+
assert!(character_body.translation().x > 6.0);
1050+
assert!(character_body.translation().y > 3.0);
1051+
let character_body = bodies.get(character_handle_cannot_climb).unwrap();
1052+
assert!(character_body.translation().x < 4.0);
1053+
assert!(dbg!(character_body.translation().y) < 2.0);
1054+
}
1055+
}

src_testbed/testbed.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ pub struct TestbedState {
102102
pub draw_colls: bool,
103103
pub highlighted_body: Option<RigidBodyHandle>,
104104
pub character_body: Option<RigidBodyHandle>,
105+
pub character_controller: Option<KinematicCharacterController>,
105106
#[cfg(feature = "dim3")]
106107
pub vehicle_controller: Option<DynamicRayCastVehicleController>,
107108
// pub grabbed_object: Option<DefaultBodyPartHandle>,
@@ -186,6 +187,7 @@ impl TestbedApp {
186187
draw_colls: false,
187188
highlighted_body: None,
188189
character_body: None,
190+
character_controller: None,
189191
#[cfg(feature = "dim3")]
190192
vehicle_controller: None,
191193
// grabbed_object: None,
@@ -530,6 +532,10 @@ impl<'a, 'b, 'c, 'd, 'e, 'f> Testbed<'a, 'b, 'c, 'd, 'e, 'f> {
530532
self.state.character_body = Some(handle);
531533
}
532534

535+
pub fn set_character_controller(&mut self, controller: Option<KinematicCharacterController>) {
536+
self.state.character_controller = controller;
537+
}
538+
533539
#[cfg(feature = "dim3")]
534540
pub fn set_vehicle_controller(&mut self, controller: DynamicRayCastVehicleController) {
535541
self.state.vehicle_controller = Some(controller);
@@ -827,7 +833,7 @@ impl<'a, 'b, 'c, 'd, 'e, 'f> Testbed<'a, 'b, 'c, 'd, 'e, 'f> {
827833
desired_movement *= speed;
828834
desired_movement -= Vector::y() * speed;
829835

830-
let controller = KinematicCharacterController::default();
836+
let controller = self.state.character_controller.unwrap_or_default();
831837
let phx = &mut self.harness.physics;
832838
let character_body = &phx.bodies[character_handle];
833839
let character_collider = &phx.colliders[character_body.colliders()[0]];

src_testbed/ui.rs

+34
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use rapier::control::CharacterLength;
12
use rapier::counters::Counters;
23
use rapier::math::Real;
34
use std::num::NonZeroUsize;
@@ -240,7 +241,40 @@ pub fn update_ui(
240241
// .set(TestbedStateFlags::CONTACT_POINTS, contact_points);
241242
// state.flags.set(TestbedStateFlags::WIREFRAME, wireframe);
242243
ui.separator();
244+
if let Some(character_controller) = &mut state.character_controller {
245+
ui.label("Character controller");
246+
ui.checkbox(&mut character_controller.slide, "slide").on_hover_text("Should the character try to slide against the floor if it hits it?");
247+
#[allow(clippy::useless_conversion)]
248+
{
243249

250+
ui.add(Slider::new(&mut character_controller.max_slope_climb_angle, 0.0..=std::f32::consts::TAU.into()).text("max_slope_climb_angle"))
251+
.on_hover_text("The maximum angle (radians) between the floor’s normal and the `up` vector that the character is able to climb.");
252+
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"))
253+
.on_hover_text("The minimum angle (radians) between the floor’s normal and the `up` vector before the character starts to slide down automatically.");
254+
}
255+
let mut is_snapped = character_controller.snap_to_ground.is_some();
256+
if ui.checkbox(&mut is_snapped, "snap_to_ground").changed {
257+
match is_snapped {
258+
true => {
259+
character_controller.snap_to_ground = Some(CharacterLength::Relative(0.1));
260+
},
261+
false => {
262+
character_controller.snap_to_ground = None;
263+
},
264+
}
265+
}
266+
if let Some(snapped) = &mut character_controller.snap_to_ground {
267+
match snapped {
268+
CharacterLength::Relative(val) => {
269+
ui.add(Slider::new(val, 0.0..=10.0).text("Snapped Relative Character Length"));
270+
},
271+
CharacterLength::Absolute(val) => {
272+
ui.add(Slider::new(val, 0.0..=10.0).text("Snapped Absolute Character Length"));
273+
},
274+
}
275+
}
276+
ui.separator();
277+
}
244278
let label = if state.running == RunMode::Stop {
245279
"Start (T)"
246280
} else {

0 commit comments

Comments
 (0)