Skip to content

Commit 684f3a3

Browse files
Fix character controller ground detection (#715)
1 parent 513ab3d commit 684f3a3

File tree

2 files changed

+174
-4
lines changed

2 files changed

+174
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
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.
77
- Fix `KinematicCharacterController::move_shape` not respecting parameters `max_slope_climb_angle` and `min_slope_slide_angle`.
8+
- Improve ground detection reliability for `KinematicCharacterController`. (#715)
89
- Fix wasm32 default values for physics hooks filter to be consistent with native: `COMPUTE_IMPULSES`.
910

1011
### Added

src/control/character_controller.rs

Lines changed: 173 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ impl KinematicCharacterController {
251251
let mut max_iters = 20;
252252
let mut kinematic_friction_translation = Vector::zeros();
253253
let offset = self.offset.eval(dims.y);
254+
let mut is_moving = false;
254255

255256
while let Some((translation_dir, translation_dist)) =
256257
UnitVector::try_new_and_get(translation_remaining, 1.0e-5)
@@ -260,6 +261,7 @@ impl KinematicCharacterController {
260261
} else {
261262
max_iters -= 1;
262263
}
264+
is_moving = true;
263265

264266
// 2. Cast towards the movement direction.
265267
if let Some((handle, hit)) = queries.cast_shape(
@@ -319,9 +321,20 @@ impl KinematicCharacterController {
319321
// No interference along the path.
320322
result.translation += translation_remaining;
321323
translation_remaining.fill(0.0);
324+
result.grounded = self.detect_grounded_status_and_apply_friction(
325+
dt,
326+
bodies,
327+
colliders,
328+
queries,
329+
character_shape,
330+
&(Translation::from(result.translation) * character_pos),
331+
&dims,
332+
filter,
333+
None,
334+
None,
335+
);
322336
break;
323337
}
324-
325338
result.grounded = self.detect_grounded_status_and_apply_friction(
326339
dt,
327340
bodies,
@@ -339,6 +352,22 @@ impl KinematicCharacterController {
339352
break;
340353
}
341354
}
355+
// When not moving, `detect_grounded_status_and_apply_friction` is not reached
356+
// so we call it explicitly here.
357+
if !is_moving {
358+
result.grounded = self.detect_grounded_status_and_apply_friction(
359+
dt,
360+
bodies,
361+
colliders,
362+
queries,
363+
character_shape,
364+
&(Translation::from(result.translation) * character_pos),
365+
&dims,
366+
filter,
367+
None,
368+
None,
369+
);
370+
}
342371
// If needed, and if we are not already grounded, snap to the ground.
343372
if grounded_at_starting_pos {
344373
self.snap_to_ground(
@@ -398,7 +427,7 @@ impl KinematicCharacterController {
398427
}
399428

400429
fn predict_ground(&self, up_extends: Real) -> Real {
401-
self.offset.eval(up_extends) * 1.2
430+
self.offset.eval(up_extends) + 0.05
402431
}
403432

404433
fn detect_grounded_status_and_apply_friction(
@@ -508,7 +537,6 @@ impl KinematicCharacterController {
508537
}
509538
true
510539
});
511-
512540
grounded
513541
}
514542

@@ -909,7 +937,10 @@ fn subtract_hit(translation: Vector<Real>, hit: &ShapeCastHit) -> Vector<Real> {
909937
#[cfg(all(feature = "dim3", feature = "f32"))]
910938
#[cfg(test)]
911939
mod test {
912-
use crate::{control::KinematicCharacterController, prelude::*};
940+
use crate::{
941+
control::{CharacterLength, KinematicCharacterController},
942+
prelude::*,
943+
};
913944

914945
#[test]
915946
fn character_controller_climb_test() {
@@ -1052,4 +1083,142 @@ mod test {
10521083
assert!(character_body.translation().x < 4.0);
10531084
assert!(dbg!(character_body.translation().y) < 2.0);
10541085
}
1086+
1087+
#[test]
1088+
fn character_controller_ground_detection() {
1089+
let mut colliders = ColliderSet::new();
1090+
let mut impulse_joints = ImpulseJointSet::new();
1091+
let mut multibody_joints = MultibodyJointSet::new();
1092+
let mut pipeline = PhysicsPipeline::new();
1093+
let mut bf = BroadPhaseMultiSap::new();
1094+
let mut nf = NarrowPhase::new();
1095+
let mut islands = IslandManager::new();
1096+
let mut query_pipeline = QueryPipeline::new();
1097+
1098+
let mut bodies = RigidBodySet::new();
1099+
1100+
let gravity = Vector::y() * -9.81;
1101+
1102+
let ground_size = 1001.0;
1103+
let ground_height = 1.0;
1104+
/*
1105+
* Create a flat ground
1106+
*/
1107+
let rigid_body =
1108+
RigidBodyBuilder::fixed().translation(vector![0.0, -ground_height / 2f32, 0.0]);
1109+
let floor_handle = bodies.insert(rigid_body);
1110+
let collider = ColliderBuilder::cuboid(ground_size, ground_height, ground_size);
1111+
colliders.insert_with_parent(collider, floor_handle, &mut bodies);
1112+
1113+
let integration_parameters = IntegrationParameters::default();
1114+
1115+
// Initialize character with snap to ground
1116+
let character_controller_snap = KinematicCharacterController {
1117+
snap_to_ground: Some(CharacterLength::Relative(0.2)),
1118+
..Default::default()
1119+
};
1120+
let mut character_body_snap = RigidBodyBuilder::kinematic_position_based()
1121+
.additional_mass(1.0)
1122+
.build();
1123+
character_body_snap.set_translation(Vector::new(0.6, 0.5, 0.0), false);
1124+
let character_handle_snap = bodies.insert(character_body_snap);
1125+
1126+
let collider = ColliderBuilder::ball(0.5).build();
1127+
colliders.insert_with_parent(collider.clone(), character_handle_snap, &mut bodies);
1128+
1129+
// Initialize character without snap to ground
1130+
let character_controller_no_snap = KinematicCharacterController {
1131+
snap_to_ground: None,
1132+
..Default::default()
1133+
};
1134+
let mut character_body_no_snap = RigidBodyBuilder::kinematic_position_based()
1135+
.additional_mass(1.0)
1136+
.build();
1137+
character_body_no_snap.set_translation(Vector::new(-0.6, 0.5, 0.0), false);
1138+
let character_handle_no_snap = bodies.insert(character_body_no_snap);
1139+
1140+
let collider = ColliderBuilder::ball(0.5).build();
1141+
let character_shape = collider.shape();
1142+
colliders.insert_with_parent(collider.clone(), character_handle_no_snap, &mut bodies);
1143+
1144+
query_pipeline.update(&colliders);
1145+
for i in 0..10000 {
1146+
let mut update_character_controller =
1147+
|controller: KinematicCharacterController, handle: RigidBodyHandle| {
1148+
let character_body = bodies.get(handle).unwrap();
1149+
// Use a closure to handle or collect the collisions while
1150+
// the character is being moved.
1151+
let mut collisions = vec![];
1152+
let filter_character_controller = QueryFilter::new().exclude_rigid_body(handle);
1153+
let effective_movement = controller.move_shape(
1154+
integration_parameters.dt,
1155+
&bodies,
1156+
&colliders,
1157+
&query_pipeline,
1158+
character_shape,
1159+
character_body.position(),
1160+
Vector::new(0.1, -0.1, 0.1),
1161+
filter_character_controller,
1162+
|collision| collisions.push(collision),
1163+
);
1164+
let character_body = bodies.get_mut(handle).unwrap();
1165+
let translation = character_body.translation();
1166+
assert_eq!(
1167+
effective_movement.grounded, true,
1168+
"movement should be grounded at all times for current setup (iter: {}), pos: {}.",
1169+
i, translation + effective_movement.translation
1170+
);
1171+
character_body.set_next_kinematic_translation(
1172+
translation + effective_movement.translation,
1173+
);
1174+
};
1175+
1176+
update_character_controller(character_controller_no_snap, character_handle_no_snap);
1177+
update_character_controller(character_controller_snap, character_handle_snap);
1178+
// Step once
1179+
pipeline.step(
1180+
&gravity,
1181+
&integration_parameters,
1182+
&mut islands,
1183+
&mut bf,
1184+
&mut nf,
1185+
&mut bodies,
1186+
&mut colliders,
1187+
&mut impulse_joints,
1188+
&mut multibody_joints,
1189+
&mut CCDSolver::new(),
1190+
Some(&mut query_pipeline),
1191+
&(),
1192+
&(),
1193+
);
1194+
}
1195+
let character_body = bodies.get_mut(character_handle_no_snap).unwrap();
1196+
let translation = character_body.translation();
1197+
1198+
// accumulated numerical errors make the test go less far than it should,
1199+
// but it's expected.
1200+
assert!(
1201+
translation.x >= 997.0,
1202+
"actual translation.x:{}",
1203+
translation.x
1204+
);
1205+
assert!(
1206+
translation.z >= 997.0,
1207+
"actual translation.z:{}",
1208+
translation.z
1209+
);
1210+
1211+
let character_body = bodies.get_mut(character_handle_snap).unwrap();
1212+
let translation = character_body.translation();
1213+
assert!(
1214+
translation.x >= 997.0,
1215+
"actual translation.x:{}",
1216+
translation.x
1217+
);
1218+
assert!(
1219+
translation.z >= 997.0,
1220+
"actual translation.z:{}",
1221+
translation.z
1222+
);
1223+
}
10551224
}

0 commit comments

Comments
 (0)