1use crate::geometry::{ColliderHandle, ContactManifold, Shape, ShapeCastHit};
2use crate::math::{Isometry, Point, Real, UnitVector, Vector};
3use crate::pipeline::{QueryFilterFlags, QueryPipeline, QueryPipelineMut};
4use crate::utils;
5use na::{RealField, Vector2};
6use parry::bounding_volume::BoundingVolume;
7use parry::math::Translation;
8use parry::query::details::ShapeCastOptions;
9use parry::query::{DefaultQueryDispatcher, PersistentQueryDispatcher};
10
11#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
12#[derive(Copy, Clone, Debug, PartialEq)]
13pub enum CharacterLength {
15 Relative(Real),
21 Absolute(Real),
27}
28
29impl CharacterLength {
30 pub fn map_absolute(self, f: impl FnOnce(Real) -> Real) -> Self {
33 if let Self::Absolute(value) = self {
34 Self::Absolute(f(value))
35 } else {
36 self
37 }
38 }
39
40 pub fn map_relative(self, f: impl FnOnce(Real) -> Real) -> Self {
43 if let Self::Relative(value) = self {
44 Self::Relative(f(value))
45 } else {
46 self
47 }
48 }
49
50 fn eval(self, value: Real) -> Real {
51 match self {
52 Self::Relative(x) => value * x,
53 Self::Absolute(x) => x,
54 }
55 }
56}
57
58#[derive(Debug)]
59struct HitInfo {
60 toi: ShapeCastHit,
61 is_wall: bool,
62 is_nonslip_slope: bool,
63}
64
65#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
67#[derive(Copy, Clone, Debug, PartialEq)]
68pub struct CharacterAutostep {
69 pub max_height: CharacterLength,
71 pub min_width: CharacterLength,
73 pub include_dynamic_bodies: bool,
75}
76
77impl Default for CharacterAutostep {
78 fn default() -> Self {
79 Self {
80 max_height: CharacterLength::Relative(0.25),
81 min_width: CharacterLength::Relative(0.5),
82 include_dynamic_bodies: true,
83 }
84 }
85}
86
87#[derive(Debug)]
88struct HitDecomposition {
89 normal_part: Vector<Real>,
90 horizontal_tangent: Vector<Real>,
91 vertical_tangent: Vector<Real>,
92 }
95
96impl HitDecomposition {
97 pub fn unconstrained_slide_part(&self) -> Vector<Real> {
98 self.normal_part + self.horizontal_tangent + self.vertical_tangent
99 }
100}
101
102#[derive(Copy, Clone, Debug)]
104pub struct CharacterCollision {
105 pub handle: ColliderHandle,
107 pub character_pos: Isometry<Real>,
109 pub translation_applied: Vector<Real>,
111 pub translation_remaining: Vector<Real>,
113 pub hit: ShapeCastHit,
115}
116
117#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
173#[derive(Copy, Clone, Debug)]
174pub struct KinematicCharacterController {
175 pub up: UnitVector<Real>,
177 pub offset: CharacterLength,
182 pub slide: bool,
184 pub autostep: Option<CharacterAutostep>,
189 pub max_slope_climb_angle: Real,
192 pub min_slope_slide_angle: Real,
195 pub snap_to_ground: Option<CharacterLength>,
198 pub normal_nudge_factor: Real,
207}
208
209impl Default for KinematicCharacterController {
210 fn default() -> Self {
211 Self {
212 up: Vector::y_axis(),
213 offset: CharacterLength::Relative(0.01),
214 slide: true,
215 autostep: None,
216 max_slope_climb_angle: Real::frac_pi_4(),
217 min_slope_slide_angle: Real::frac_pi_4(),
218 snap_to_ground: Some(CharacterLength::Relative(0.2)),
219 normal_nudge_factor: 1.0e-4,
220 }
221 }
222}
223
224#[derive(Debug)]
226pub struct EffectiveCharacterMovement {
227 pub translation: Vector<Real>,
229 pub grounded: bool,
231 pub is_sliding_down_slope: bool,
233}
234
235impl KinematicCharacterController {
236 fn check_and_fix_penetrations(&self) {
237 }
265
266 #[profiling::function]
268 pub fn move_shape(
269 &self,
270 dt: Real,
271 queries: &QueryPipeline,
272 character_shape: &dyn Shape,
273 character_pos: &Isometry<Real>,
274 desired_translation: Vector<Real>,
275 mut events: impl FnMut(CharacterCollision),
276 ) -> EffectiveCharacterMovement {
277 let mut result = EffectiveCharacterMovement {
278 translation: Vector::zeros(),
279 grounded: false,
280 is_sliding_down_slope: false,
281 };
282 let dims = self.compute_dims(character_shape);
283
284 self.check_and_fix_penetrations();
286
287 let mut translation_remaining = desired_translation;
288
289 let grounded_at_starting_pos = self.detect_grounded_status_and_apply_friction(
290 dt,
291 queries,
292 character_shape,
293 character_pos,
294 &dims,
295 None,
296 None,
297 );
298
299 let mut max_iters = 20;
300 let mut kinematic_friction_translation = Vector::zeros();
301 let offset = self.offset.eval(dims.y);
302 let mut is_moving = false;
303
304 while let Some((translation_dir, translation_dist)) =
305 UnitVector::try_new_and_get(translation_remaining, 1.0e-5)
306 {
307 if max_iters == 0 {
308 break;
309 } else {
310 max_iters -= 1;
311 }
312 is_moving = true;
313
314 if let Some((handle, hit)) = queries.cast_shape(
316 &(Translation::from(result.translation) * character_pos),
317 &translation_dir,
318 character_shape,
319 ShapeCastOptions {
320 target_distance: offset,
321 stop_at_penetration: false,
322 max_time_of_impact: translation_dist,
323 compute_impact_geometry_on_penetration: true,
324 },
325 ) {
326 let allowed_dist = hit.time_of_impact;
328 let allowed_translation = *translation_dir * allowed_dist;
329 result.translation += allowed_translation;
330 translation_remaining -= allowed_translation;
331
332 events(CharacterCollision {
333 handle,
334 character_pos: Translation::from(result.translation) * character_pos,
335 translation_applied: result.translation,
336 translation_remaining,
337 hit,
338 });
339
340 let hit_info = self.compute_hit_info(hit);
341
342 if !self.handle_stairs(
344 *queries,
345 character_shape,
346 &(Translation::from(result.translation) * character_pos),
347 &dims,
348 handle,
349 &hit_info,
350 &mut translation_remaining,
351 &mut result,
352 ) {
353 translation_remaining = self.handle_slopes(
355 &hit_info,
356 &desired_translation,
357 &translation_remaining,
358 self.normal_nudge_factor,
359 &mut result,
360 );
361 }
362 } else {
363 result.translation += translation_remaining;
365 translation_remaining.fill(0.0);
366 result.grounded = self.detect_grounded_status_and_apply_friction(
367 dt,
368 queries,
369 character_shape,
370 &(Translation::from(result.translation) * character_pos),
371 &dims,
372 None,
373 None,
374 );
375 break;
376 }
377 result.grounded = self.detect_grounded_status_and_apply_friction(
378 dt,
379 queries,
380 character_shape,
381 &(Translation::from(result.translation) * character_pos),
382 &dims,
383 Some(&mut kinematic_friction_translation),
384 Some(&mut translation_remaining),
385 );
386
387 if !self.slide {
388 break;
389 }
390 }
391 if !is_moving {
394 result.grounded = self.detect_grounded_status_and_apply_friction(
395 dt,
396 queries,
397 character_shape,
398 &(Translation::from(result.translation) * character_pos),
399 &dims,
400 None,
401 None,
402 );
403 }
404 if grounded_at_starting_pos {
406 self.snap_to_ground(
407 queries,
408 character_shape,
409 &(Translation::from(result.translation) * character_pos),
410 &dims,
411 &mut result,
412 );
413 }
414
415 result
417 }
418
419 fn snap_to_ground(
420 &self,
421 queries: &QueryPipeline,
422 character_shape: &dyn Shape,
423 character_pos: &Isometry<Real>,
424 dims: &Vector2<Real>,
425 result: &mut EffectiveCharacterMovement,
426 ) -> Option<(ColliderHandle, ShapeCastHit)> {
427 if let Some(snap_distance) = self.snap_to_ground {
428 if result.translation.dot(&self.up) < -1.0e-5 {
429 let snap_distance = snap_distance.eval(dims.y);
430 let offset = self.offset.eval(dims.y);
431 if let Some((hit_handle, hit)) = queries.cast_shape(
432 character_pos,
433 &-self.up,
434 character_shape,
435 ShapeCastOptions {
436 target_distance: offset,
437 stop_at_penetration: false,
438 max_time_of_impact: snap_distance,
439 compute_impact_geometry_on_penetration: true,
440 },
441 ) {
442 result.translation -= *self.up * hit.time_of_impact;
444 result.grounded = true;
445 return Some((hit_handle, hit));
446 }
447 }
448 }
449
450 None
451 }
452
453 fn predict_ground(&self, up_extends: Real) -> Real {
454 self.offset.eval(up_extends) + 0.05
455 }
456
457 #[profiling::function]
458 fn detect_grounded_status_and_apply_friction(
459 &self,
460 dt: Real,
461 queries: &QueryPipeline,
462 character_shape: &dyn Shape,
463 character_pos: &Isometry<Real>,
464 dims: &Vector2<Real>,
465 mut kinematic_friction_translation: Option<&mut Vector<Real>>,
466 mut translation_remaining: Option<&mut Vector<Real>>,
467 ) -> bool {
468 let prediction = self.predict_ground(dims.y);
469
470 let dispatcher = DefaultQueryDispatcher;
472
473 let mut manifolds: Vec<ContactManifold> = vec![];
474 let character_aabb = character_shape
475 .compute_aabb(character_pos)
476 .loosened(prediction);
477
478 let mut grounded = false;
479
480 'outer: for (_, collider) in queries.intersect_aabb_conservative(character_aabb) {
481 manifolds.clear();
482 let pos12 = character_pos.inv_mul(collider.position());
483 let _ = dispatcher.contact_manifolds(
484 &pos12,
485 character_shape,
486 collider.shape(),
487 prediction,
488 &mut manifolds,
489 &mut None,
490 );
491
492 if let (Some(kinematic_friction_translation), Some(translation_remaining)) = (
493 kinematic_friction_translation.as_deref_mut(),
494 translation_remaining.as_deref_mut(),
495 ) {
496 let init_kinematic_friction_translation = *kinematic_friction_translation;
497 let kinematic_parent = collider
498 .parent
499 .and_then(|p| queries.bodies.get(p.handle))
500 .filter(|rb| rb.is_kinematic());
501
502 for m in &manifolds {
503 if self.is_grounded_at_contact_manifold(m, character_pos, dims) {
504 grounded = true;
505 }
506
507 if let Some(kinematic_parent) = kinematic_parent {
508 let mut num_active_contacts = 0;
509 let mut manifold_center = Point::origin();
510 let normal = -(character_pos * m.local_n1);
511
512 for contact in &m.points {
513 if contact.dist <= prediction {
514 num_active_contacts += 1;
515 let contact_point = collider.position() * contact.local_p2;
516 let target_vel = kinematic_parent.velocity_at_point(&contact_point);
517
518 let normal_target_mvt = target_vel.dot(&normal) * dt;
519 let normal_current_mvt = translation_remaining.dot(&normal);
520
521 manifold_center += contact_point.coords;
522 *translation_remaining +=
523 normal * (normal_target_mvt - normal_current_mvt);
524 }
525 }
526
527 if num_active_contacts > 0 {
528 let target_vel = kinematic_parent.velocity_at_point(
529 &(manifold_center / num_active_contacts as Real),
530 );
531 let tangent_platform_mvt =
532 (target_vel - normal * target_vel.dot(&normal)) * dt;
533 kinematic_friction_translation.zip_apply(
534 &tangent_platform_mvt,
535 |y, x| {
536 if x.abs() > (*y).abs() {
537 *y = x;
538 }
539 },
540 );
541 }
542 }
543 }
544
545 *translation_remaining +=
546 *kinematic_friction_translation - init_kinematic_friction_translation;
547 } else {
548 for m in &manifolds {
549 if self.is_grounded_at_contact_manifold(m, character_pos, dims) {
550 grounded = true;
551 break 'outer; }
553 }
554 }
555 }
556 grounded
557 }
558
559 fn is_grounded_at_contact_manifold(
560 &self,
561 manifold: &ContactManifold,
562 character_pos: &Isometry<Real>,
563 dims: &Vector2<Real>,
564 ) -> bool {
565 let normal = -(character_pos * manifold.local_n1);
566
567 if normal.dot(&self.up) >= 1.0e-3 {
570 let prediction = self.predict_ground(dims.y);
571 for contact in &manifold.points {
572 if contact.dist <= prediction {
573 return true;
574 }
575 }
576 }
577 false
578 }
579
580 fn handle_slopes(
581 &self,
582 hit: &HitInfo,
583 movement_input: &Vector<Real>,
584 translation_remaining: &Vector<Real>,
585 normal_nudge_factor: Real,
586 result: &mut EffectiveCharacterMovement,
587 ) -> Vector<Real> {
588 let [_vertical_input, horizontal_input] = self.split_into_components(movement_input);
589 let horiz_input_decomp = self.decompose_hit(&horizontal_input, &hit.toi);
590 let decomp = self.decompose_hit(translation_remaining, &hit.toi);
591
592 let slipping_intent = self.up.dot(&horiz_input_decomp.vertical_tangent) < 0.0;
595 let slipping = self.up.dot(&decomp.vertical_tangent) < 0.0;
597
598 let climbing_intent = self.up.dot(&_vertical_input) > 0.0;
600 let climbing = self.up.dot(&decomp.vertical_tangent) > 0.0;
602
603 let allowed_movement = if hit.is_wall && climbing && !climbing_intent {
604 decomp.horizontal_tangent + decomp.normal_part
606 } else if hit.is_nonslip_slope && slipping && !slipping_intent {
607 decomp.horizontal_tangent + decomp.normal_part
609 } else {
610 result.is_sliding_down_slope = true;
612 decomp.unconstrained_slide_part()
613 };
614
615 allowed_movement + *hit.toi.normal1 * normal_nudge_factor
616 }
617
618 fn split_into_components(&self, translation: &Vector<Real>) -> [Vector<Real>; 2] {
619 let vertical_translation = *self.up * (self.up.dot(translation));
620 let horizontal_translation = *translation - vertical_translation;
621 [vertical_translation, horizontal_translation]
622 }
623
624 fn compute_hit_info(&self, toi: ShapeCastHit) -> HitInfo {
625 let angle_with_floor = self.up.angle(&toi.normal1);
626 let is_ceiling = self.up.dot(&toi.normal1) < 0.0;
627 let is_wall = angle_with_floor >= self.max_slope_climb_angle && !is_ceiling;
628 let is_nonslip_slope = angle_with_floor <= self.min_slope_slide_angle;
629
630 HitInfo {
631 toi,
632 is_wall,
633 is_nonslip_slope,
634 }
635 }
636
637 fn decompose_hit(&self, translation: &Vector<Real>, hit: &ShapeCastHit) -> HitDecomposition {
638 let dist_to_surface = translation.dot(&hit.normal1);
639 let normal_part;
640 let penetration_part;
641
642 if dist_to_surface < 0.0 {
643 normal_part = Vector::zeros();
644 penetration_part = dist_to_surface * *hit.normal1;
645 } else {
646 penetration_part = Vector::zeros();
647 normal_part = dist_to_surface * *hit.normal1;
648 }
649
650 let tangent = translation - normal_part - penetration_part;
651 #[cfg(feature = "dim3")]
652 let horizontal_tangent_dir = hit.normal1.cross(&self.up);
653 #[cfg(feature = "dim2")]
654 let horizontal_tangent_dir = Vector::zeros();
655
656 let horizontal_tangent_dir = horizontal_tangent_dir
657 .try_normalize(1.0e-5)
658 .unwrap_or_default();
659 let horizontal_tangent = tangent.dot(&horizontal_tangent_dir) * horizontal_tangent_dir;
660 let vertical_tangent = tangent - horizontal_tangent;
661
662 HitDecomposition {
663 normal_part,
664 horizontal_tangent,
665 vertical_tangent,
666 }
667 }
668
669 fn compute_dims(&self, character_shape: &dyn Shape) -> Vector2<Real> {
670 let extents = character_shape.compute_local_aabb().extents();
671 let up_extent = extents.dot(&self.up.abs());
672 let side_extent = (extents - (*self.up).abs() * up_extent).norm();
673 Vector2::new(side_extent, up_extent)
674 }
675
676 #[profiling::function]
677 fn handle_stairs(
678 &self,
679 mut queries: QueryPipeline,
680 character_shape: &dyn Shape,
681 character_pos: &Isometry<Real>,
682 dims: &Vector2<Real>,
683 stair_handle: ColliderHandle,
684 hit: &HitInfo,
685 translation_remaining: &mut Vector<Real>,
686 result: &mut EffectiveCharacterMovement,
687 ) -> bool {
688 let Some(autostep) = self.autostep else {
689 return false;
690 };
691
692 if !hit.is_wall {
694 return false;
695 }
696
697 let offset = self.offset.eval(dims.y);
698 let min_width = autostep.min_width.eval(dims.x) + offset;
699 let max_height = autostep.max_height.eval(dims.y) + offset;
700
701 if !autostep.include_dynamic_bodies {
702 if queries
703 .colliders
704 .get(stair_handle)
705 .and_then(|co| co.parent)
706 .and_then(|p| queries.bodies.get(p.handle))
707 .map(|b| b.is_dynamic())
708 == Some(true)
709 {
710 return false;
712 }
713
714 queries.filter.flags |= QueryFilterFlags::EXCLUDE_DYNAMIC;
715 }
716
717 let shifted_character_pos = Translation::from(*self.up * max_height) * character_pos;
718
719 let Some(horizontal_dir) = (*translation_remaining
720 - *self.up * translation_remaining.dot(&self.up))
721 .try_normalize(1.0e-5) else {
722 return false;
723 };
724
725 if queries
726 .cast_shape(
727 character_pos,
728 &self.up,
729 character_shape,
730 ShapeCastOptions {
731 target_distance: offset,
732 stop_at_penetration: false,
733 max_time_of_impact: max_height,
734 compute_impact_geometry_on_penetration: true,
735 },
736 )
737 .is_some()
738 {
739 return false;
741 }
742
743 if queries
744 .cast_shape(
745 &shifted_character_pos,
746 &horizontal_dir,
747 character_shape,
748 ShapeCastOptions {
749 target_distance: offset,
750 stop_at_penetration: false,
751 max_time_of_impact: min_width,
752 compute_impact_geometry_on_penetration: true,
753 },
754 )
755 .is_some()
756 {
757 return false;
759 }
760
761 if let Some((_, hit)) = queries.cast_shape(
764 &(Translation::from(horizontal_dir * min_width) * shifted_character_pos),
765 &-self.up,
766 character_shape,
767 ShapeCastOptions {
768 target_distance: offset,
769 stop_at_penetration: false,
770 max_time_of_impact: max_height,
771 compute_impact_geometry_on_penetration: true,
772 },
773 ) {
774 let [vertical_slope_translation, horizontal_slope_translation] = self
775 .split_into_components(translation_remaining)
776 .map(|remaining| subtract_hit(remaining, &hit));
777
778 let slope_translation = horizontal_slope_translation + vertical_slope_translation;
779
780 let angle_with_floor = self.up.angle(&hit.normal1);
781 let climbing = self.up.dot(&slope_translation) >= 0.0;
782
783 if climbing && angle_with_floor > self.max_slope_climb_angle {
784 return false; }
786 }
787
788 let step_height = max_height
790 - queries
791 .cast_shape(
792 &(Translation::from(horizontal_dir * min_width) * shifted_character_pos),
793 &-self.up,
794 character_shape,
795 ShapeCastOptions {
796 target_distance: offset,
797 stop_at_penetration: false,
798 max_time_of_impact: max_height,
799 compute_impact_geometry_on_penetration: true,
800 },
801 )
802 .map(|hit| hit.1.time_of_impact)
803 .unwrap_or(max_height);
804
805 let step = *self.up * step_height;
807 *translation_remaining -= step;
808
809 let horizontal_nudge =
812 horizontal_dir * horizontal_dir.dot(translation_remaining).min(min_width);
813 *translation_remaining -= horizontal_nudge;
814
815 result.translation += step + horizontal_nudge;
816 true
817 }
818
819 #[profiling::function]
824 pub fn solve_character_collision_impulses<'a>(
825 &self,
826 dt: Real,
827 queries: &mut QueryPipelineMut,
828 character_shape: &dyn Shape,
829 character_mass: Real,
830 collisions: impl IntoIterator<Item = &'a CharacterCollision>,
831 ) {
832 for collision in collisions {
833 self.solve_single_character_collision_impulse(
834 dt,
835 queries,
836 character_shape,
837 character_mass,
838 collision,
839 );
840 }
841 }
842
843 #[profiling::function]
848 fn solve_single_character_collision_impulse(
849 &self,
850 dt: Real,
851 queries: &mut QueryPipelineMut,
852 character_shape: &dyn Shape,
853 character_mass: Real,
854 collision: &CharacterCollision,
855 ) {
856 let extents = character_shape.compute_local_aabb().extents();
857 let up_extent = extents.dot(&self.up.abs());
858 let movement_to_transfer =
859 *collision.hit.normal1 * collision.translation_remaining.dot(&collision.hit.normal1);
860 let prediction = self.predict_ground(up_extent);
861
862 let dispatcher = DefaultQueryDispatcher;
864
865 let mut manifolds: Vec<ContactManifold> = vec![];
866 let character_aabb = character_shape
867 .compute_aabb(&collision.character_pos)
868 .loosened(prediction);
869
870 for (_, collider) in queries.as_ref().intersect_aabb_conservative(character_aabb) {
871 if let Some(parent) = collider.parent {
872 if let Some(body) = queries.bodies.get(parent.handle) {
873 if body.is_dynamic() {
874 manifolds.clear();
875 let pos12 = collision.character_pos.inv_mul(collider.position());
876 let prev_manifolds_len = manifolds.len();
877 let _ = dispatcher.contact_manifolds(
878 &pos12,
879 character_shape,
880 collider.shape(),
881 prediction,
882 &mut manifolds,
883 &mut None,
884 );
885
886 for m in &mut manifolds[prev_manifolds_len..] {
887 m.data.rigid_body2 = Some(parent.handle);
888 m.data.normal = collision.character_pos * m.local_n1;
889 }
890 }
891 }
892 }
893 }
894
895 let velocity_to_transfer = movement_to_transfer * utils::inv(dt);
896
897 for manifold in &manifolds {
898 let body_handle = manifold.data.rigid_body2.unwrap();
899 let body = &mut queries.bodies[body_handle];
900
901 for pt in &manifold.points {
902 if pt.dist <= prediction {
903 let body_mass = body.mass();
904 let contact_point = body.position() * pt.local_p2;
905 let delta_vel_per_contact = (velocity_to_transfer
906 - body.velocity_at_point(&contact_point))
907 .dot(&manifold.data.normal);
908 let mass_ratio = body_mass * character_mass / (body_mass + character_mass);
909
910 body.apply_impulse_at_point(
911 manifold.data.normal * delta_vel_per_contact.max(0.0) * mass_ratio,
912 contact_point,
913 true,
914 );
915 }
916 }
917 }
918 }
919}
920
921fn subtract_hit(translation: Vector<Real>, hit: &ShapeCastHit) -> Vector<Real> {
922 let surface_correction = (-translation).dot(&hit.normal1).max(0.0);
923 let surface_correction = surface_correction * (1.0 + 1.0e-5);
925 translation + *hit.normal1 * surface_correction
926}
927
928#[cfg(all(feature = "dim3", feature = "f32"))]
929#[cfg(test)]
930mod test {
931 use crate::{
932 control::{CharacterLength, KinematicCharacterController},
933 prelude::*,
934 };
935
936 #[test]
937 fn character_controller_climb_test() {
938 let mut colliders = ColliderSet::new();
939 let mut impulse_joints = ImpulseJointSet::new();
940 let mut multibody_joints = MultibodyJointSet::new();
941 let mut pipeline = PhysicsPipeline::new();
942 let mut bf = BroadPhaseBvh::new();
943 let mut nf = NarrowPhase::new();
944 let mut islands = IslandManager::new();
945
946 let mut bodies = RigidBodySet::new();
947
948 let gravity = Vector::y() * -9.81;
949
950 let ground_size = 100.0;
951 let ground_height = 0.1;
952 let rigid_body = RigidBodyBuilder::fixed().translation(vector![0.0, -ground_height, 0.0]);
956 let floor_handle = bodies.insert(rigid_body);
957 let collider = ColliderBuilder::cuboid(ground_size, ground_height, ground_size);
958 colliders.insert_with_parent(collider, floor_handle, &mut bodies);
959
960 let slope_angle = 0.2;
964 let slope_size = 2.0;
965 let collider = ColliderBuilder::cuboid(slope_size, ground_height, slope_size)
966 .translation(vector![0.1 + slope_size, -ground_height + 0.4, 0.0])
967 .rotation(Vector::z() * slope_angle);
968 colliders.insert(collider);
969
970 let impossible_slope_angle = 0.6;
974 let impossible_slope_size = 2.0;
975 let collider = ColliderBuilder::cuboid(slope_size, ground_height, ground_size)
976 .translation(vector![
977 0.1 + slope_size * 2.0 + impossible_slope_size - 0.9,
978 -ground_height + 1.7,
979 0.0
980 ])
981 .rotation(Vector::z() * impossible_slope_angle);
982 colliders.insert(collider);
983
984 let integration_parameters = IntegrationParameters::default();
985
986 let mut character_body_can_climb = RigidBodyBuilder::kinematic_position_based()
988 .additional_mass(1.0)
989 .build();
990 character_body_can_climb.set_translation(Vector::new(0.6, 0.5, 0.0), false);
991 let character_handle_can_climb = bodies.insert(character_body_can_climb);
992
993 let collider = ColliderBuilder::ball(0.5).build();
994 colliders.insert_with_parent(collider.clone(), character_handle_can_climb, &mut bodies);
995
996 let mut character_body_cannot_climb = RigidBodyBuilder::kinematic_position_based()
998 .additional_mass(1.0)
999 .build();
1000 character_body_cannot_climb.set_translation(Vector::new(-0.6, 0.5, 0.0), false);
1001 let character_handle_cannot_climb = bodies.insert(character_body_cannot_climb);
1002
1003 let collider = ColliderBuilder::ball(0.5).build();
1004 let character_shape = collider.shape();
1005 colliders.insert_with_parent(collider.clone(), character_handle_cannot_climb, &mut bodies);
1006
1007 for i in 0..200 {
1008 pipeline.step(
1010 &gravity,
1011 &integration_parameters,
1012 &mut islands,
1013 &mut bf,
1014 &mut nf,
1015 &mut bodies,
1016 &mut colliders,
1017 &mut impulse_joints,
1018 &mut multibody_joints,
1019 &mut CCDSolver::new(),
1020 &(),
1021 &(),
1022 );
1023
1024 let mut update_character_controller =
1025 |controller: KinematicCharacterController, handle: RigidBodyHandle| {
1026 let character_body = bodies.get(handle).unwrap();
1027 let mut collisions = vec![];
1030 let filter_character_controller = QueryFilter::new().exclude_rigid_body(handle);
1031 let query_pipeline = bf.as_query_pipeline(
1032 nf.query_dispatcher(),
1033 &bodies,
1034 &colliders,
1035 filter_character_controller,
1036 );
1037 let effective_movement = controller.move_shape(
1038 integration_parameters.dt,
1039 &query_pipeline,
1040 character_shape,
1041 character_body.position(),
1042 Vector::new(0.1, -0.1, 0.0),
1043 |collision| collisions.push(collision),
1044 );
1045 let character_body = bodies.get_mut(handle).unwrap();
1046 let translation = character_body.translation();
1047 assert!(
1048 effective_movement.grounded,
1049 "movement should be grounded at all times for current setup (iter: {}), pos: {}.",
1050 i,
1051 translation + effective_movement.translation
1052 );
1053 character_body.set_next_kinematic_translation(
1054 translation + effective_movement.translation,
1055 );
1056 };
1057
1058 let character_controller_cannot_climb = KinematicCharacterController {
1059 max_slope_climb_angle: impossible_slope_angle - 0.001,
1060 ..Default::default()
1061 };
1062 let character_controller_can_climb = KinematicCharacterController {
1063 max_slope_climb_angle: impossible_slope_angle + 0.001,
1064 ..Default::default()
1065 };
1066 update_character_controller(
1067 character_controller_cannot_climb,
1068 character_handle_cannot_climb,
1069 );
1070 update_character_controller(character_controller_can_climb, character_handle_can_climb);
1071 }
1072 let character_body = bodies.get(character_handle_can_climb).unwrap();
1073 assert!(character_body.translation().x > 6.0);
1074 assert!(character_body.translation().y > 3.0);
1075 let character_body = bodies.get(character_handle_cannot_climb).unwrap();
1076 assert!(character_body.translation().x < 4.0);
1077 assert!(dbg!(character_body.translation().y) < 2.0);
1078 }
1079
1080 #[test]
1081 fn character_controller_ground_detection() {
1082 let mut colliders = ColliderSet::new();
1083 let mut impulse_joints = ImpulseJointSet::new();
1084 let mut multibody_joints = MultibodyJointSet::new();
1085 let mut pipeline = PhysicsPipeline::new();
1086 let mut bf = BroadPhaseBvh::new();
1087 let mut nf = NarrowPhase::new();
1088 let mut islands = IslandManager::new();
1089
1090 let mut bodies = RigidBodySet::new();
1091
1092 let gravity = Vector::y() * -9.81;
1093
1094 let ground_size = 1001.0;
1095 let ground_height = 1.0;
1096 let rigid_body =
1100 RigidBodyBuilder::fixed().translation(vector![0.0, -ground_height / 2f32, 0.0]);
1101 let floor_handle = bodies.insert(rigid_body);
1102 let collider = ColliderBuilder::cuboid(ground_size, ground_height, ground_size);
1103 colliders.insert_with_parent(collider, floor_handle, &mut bodies);
1104
1105 let integration_parameters = IntegrationParameters::default();
1106
1107 let character_controller_snap = KinematicCharacterController {
1109 snap_to_ground: Some(CharacterLength::Relative(0.2)),
1110 ..Default::default()
1111 };
1112 let mut character_body_snap = RigidBodyBuilder::kinematic_position_based()
1113 .additional_mass(1.0)
1114 .build();
1115 character_body_snap.set_translation(Vector::new(0.6, 0.5, 0.0), false);
1116 let character_handle_snap = bodies.insert(character_body_snap);
1117
1118 let collider = ColliderBuilder::ball(0.5).build();
1119 colliders.insert_with_parent(collider.clone(), character_handle_snap, &mut bodies);
1120
1121 let character_controller_no_snap = KinematicCharacterController {
1123 snap_to_ground: None,
1124 ..Default::default()
1125 };
1126 let mut character_body_no_snap = RigidBodyBuilder::kinematic_position_based()
1127 .additional_mass(1.0)
1128 .build();
1129 character_body_no_snap.set_translation(Vector::new(-0.6, 0.5, 0.0), false);
1130 let character_handle_no_snap = bodies.insert(character_body_no_snap);
1131
1132 let collider = ColliderBuilder::ball(0.5).build();
1133 let character_shape = collider.shape();
1134 colliders.insert_with_parent(collider.clone(), character_handle_no_snap, &mut bodies);
1135
1136 for i in 0..10000 {
1137 pipeline.step(
1139 &gravity,
1140 &integration_parameters,
1141 &mut islands,
1142 &mut bf,
1143 &mut nf,
1144 &mut bodies,
1145 &mut colliders,
1146 &mut impulse_joints,
1147 &mut multibody_joints,
1148 &mut CCDSolver::new(),
1149 &(),
1150 &(),
1151 );
1152
1153 let mut update_character_controller =
1154 |controller: KinematicCharacterController, handle: RigidBodyHandle| {
1155 let character_body = bodies.get(handle).unwrap();
1156 let mut collisions = vec![];
1159 let filter_character_controller = QueryFilter::new().exclude_rigid_body(handle);
1160 let query_pipeline = bf.as_query_pipeline(
1161 nf.query_dispatcher(),
1162 &bodies,
1163 &colliders,
1164 filter_character_controller,
1165 );
1166 let effective_movement = controller.move_shape(
1167 integration_parameters.dt,
1168 &query_pipeline,
1169 character_shape,
1170 character_body.position(),
1171 Vector::new(0.1, -0.1, 0.1),
1172 |collision| collisions.push(collision),
1173 );
1174 let character_body = bodies.get_mut(handle).unwrap();
1175 let translation = character_body.translation();
1176 assert!(
1177 effective_movement.grounded,
1178 "movement should be grounded at all times for current setup (iter: {}), pos: {}.",
1179 i,
1180 translation + effective_movement.translation
1181 );
1182 character_body.set_next_kinematic_translation(
1183 translation + effective_movement.translation,
1184 );
1185 };
1186
1187 update_character_controller(character_controller_no_snap, character_handle_no_snap);
1188 update_character_controller(character_controller_snap, character_handle_snap);
1189 }
1190
1191 let character_body = bodies.get_mut(character_handle_no_snap).unwrap();
1192 let translation = character_body.translation();
1193
1194 assert!(
1197 translation.x >= 997.0,
1198 "actual translation.x:{}",
1199 translation.x
1200 );
1201 assert!(
1202 translation.z >= 997.0,
1203 "actual translation.z:{}",
1204 translation.z
1205 );
1206
1207 let character_body = bodies.get_mut(character_handle_snap).unwrap();
1208 let translation = character_body.translation();
1209 assert!(
1210 translation.x >= 997.0,
1211 "actual translation.x:{}",
1212 translation.x
1213 );
1214 assert!(
1215 translation.z >= 997.0,
1216 "actual translation.z:{}",
1217 translation.z
1218 );
1219 }
1220}