rapier3d/control/
character_controller.rs

1use crate::geometry::{ColliderHandle, ContactManifold, Shape, ShapeCastHit};
2use crate::math::{Pose, Real, Vector};
3use crate::pipeline::{QueryFilterFlags, QueryPipeline, QueryPipelineMut};
4use crate::utils;
5use na::{RealField, Vector2};
6use parry::bounding_volume::BoundingVolume;
7use parry::query::details::ShapeCastOptions;
8use parry::query::{DefaultQueryDispatcher, PersistentQueryDispatcher};
9
10#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
11#[derive(Copy, Clone, Debug, PartialEq)]
12/// A length measure used for various options of a character controller.
13pub enum CharacterLength {
14    /// The length is specified relative to some of the character shape’s size.
15    ///
16    /// For example setting `CharacterAutostep::max_height` to `CharacterLength::Relative(0.1)`
17    /// for a shape with a height equal to 20.0 will result in a maximum step height
18    /// of `0.1 * 20.0 = 2.0`.
19    Relative(Real),
20    /// The length is specified as an absolute value, independent from the character shape’s size.
21    ///
22    /// For example setting `CharacterAutostep::max_height` to `CharacterLength::Relative(0.1)`
23    /// for a shape with a height equal to 20.0 will result in a maximum step height
24    /// of `0.1` (the shape height is ignored in for this value).
25    Absolute(Real),
26}
27
28impl CharacterLength {
29    /// Returns `self` with its value changed by the closure `f` if `self` is the `Self::Absolute`
30    /// variant.
31    pub fn map_absolute(self, f: impl FnOnce(Real) -> Real) -> Self {
32        if let Self::Absolute(value) = self {
33            Self::Absolute(f(value))
34        } else {
35            self
36        }
37    }
38
39    /// Returns `self` with its value changed by the closure `f` if `self` is the `Self::Relative`
40    /// variant.
41    pub fn map_relative(self, f: impl FnOnce(Real) -> Real) -> Self {
42        if let Self::Relative(value) = self {
43            Self::Relative(f(value))
44        } else {
45            self
46        }
47    }
48
49    fn eval(self, value: Real) -> Real {
50        match self {
51            Self::Relative(x) => value * x,
52            Self::Absolute(x) => x,
53        }
54    }
55}
56
57#[derive(Debug)]
58struct HitInfo {
59    toi: ShapeCastHit,
60    is_wall: bool,
61    is_nonslip_slope: bool,
62}
63
64/// Configuration for the auto-stepping character controller feature.
65#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
66#[derive(Copy, Clone, Debug, PartialEq)]
67pub struct CharacterAutostep {
68    /// The maximum step height a character can automatically step over.
69    pub max_height: CharacterLength,
70    /// The minimum width of free space that must be available after stepping on a stair.
71    pub min_width: CharacterLength,
72    /// Can the character automatically step over dynamic bodies too?
73    pub include_dynamic_bodies: bool,
74}
75
76impl Default for CharacterAutostep {
77    fn default() -> Self {
78        Self {
79            max_height: CharacterLength::Relative(0.25),
80            min_width: CharacterLength::Relative(0.5),
81            include_dynamic_bodies: true,
82        }
83    }
84}
85
86#[derive(Debug)]
87struct HitDecomposition {
88    normal_part: Vector,
89    horizontal_tangent: Vector,
90    vertical_tangent: Vector,
91    // NOTE: we don’t store the penetration part since we don’t really need it
92    //       for anything.
93}
94
95impl HitDecomposition {
96    pub fn unconstrained_slide_part(&self) -> Vector {
97        self.normal_part + self.horizontal_tangent + self.vertical_tangent
98    }
99}
100
101/// A collision between the character and its environment during its movement.
102#[derive(Copy, Clone, Debug)]
103pub struct CharacterCollision {
104    /// The collider hit by the character.
105    pub handle: ColliderHandle,
106    /// The position of the character when the collider was hit.
107    pub character_pos: Pose,
108    /// The translation that was already applied to the character when the hit happens.
109    pub translation_applied: Vector,
110    /// The translations that was still waiting to be applied to the character when the hit happens.
111    pub translation_remaining: Vector,
112    /// Geometric information about the hit.
113    pub hit: ShapeCastHit,
114}
115
116/// A kinematic character controller for player/NPC movement (walking, climbing, sliding).
117///
118/// This provides classic game character movement: walking on floors, sliding on slopes,
119/// climbing stairs, and snapping to ground. It's kinematic (not physics-based), meaning
120/// you control movement directly rather than applying forces.
121///
122/// **Not suitable for:** Ragdolls, vehicles, or physics-driven movement (use dynamic bodies instead).
123///
124/// # How it works
125///
126/// 1. You provide desired movement (e.g., "move forward 5 units")
127/// 2. Controller casts the character shape through the world
128/// 3. It handles collisions: sliding along walls, stepping up stairs, snapping to ground
129/// 4. Returns the final movement to apply
130///
131/// # Example
132///
133/// ```
134/// # use rapier3d::prelude::*;
135/// # use rapier3d::control::{CharacterAutostep, KinematicCharacterController};
136/// # let mut bodies = RigidBodySet::new();
137/// # let mut colliders = ColliderSet::new();
138/// # let broad_phase = BroadPhaseBvh::new();
139/// # let narrow_phase = NarrowPhase::new();
140/// # let dt = 1.0 / 60.0;
141/// # let speed = 5.0;
142/// # let (input_x, input_z) = (1.0, 0.0);
143/// # let character_shape = Ball::new(0.5);
144/// # let mut character_pos = Pose::IDENTITY;
145/// # let query_pipeline = broad_phase.as_query_pipeline(
146/// #     narrow_phase.query_dispatcher(),
147/// #     &bodies,
148/// #     &colliders,
149/// #     QueryFilter::default(),
150/// # );
151/// let controller = KinematicCharacterController {
152///     slide: true,  // Slide along walls instead of stopping
153///     autostep: Some(CharacterAutostep::default()),  // Auto-climb stairs
154///     max_slope_climb_angle: 45.0_f32.to_radians(),  // Max climbable slope
155///     ..Default::default()
156/// };
157///
158/// // In your game loop:
159/// let desired_movement = Vector::new(input_x, 0.0, input_z) * speed * dt;
160/// let movement = controller.move_shape(
161///     dt,
162///     &query_pipeline,
163///     &character_shape,
164///     &character_pos,
165///     desired_movement,
166///     |_| {}  // Collision event callback
167/// );
168/// character_pos.translation += movement.translation;
169/// ```
170#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
171#[derive(Copy, Clone, Debug)]
172pub struct KinematicCharacterController {
173    /// The direction that goes "up". Used to determine where the floor is, and the floor’s angle.
174    pub up: Vector,
175    /// A small gap to preserve between the character and its surroundings.
176    ///
177    /// This value should not be too large to avoid visual artifacts, but shouldn’t be too small
178    /// (must not be zero) to improve numerical stability of the character controller.
179    pub offset: CharacterLength,
180    /// Should the character try to slide against the floor if it hits it?
181    pub slide: bool,
182    /// Should the character automatically step over small obstacles? (disabled by default)
183    ///
184    /// Note that autostepping is currently a very computationally expensive feature, so it
185    /// is disabled by default.
186    pub autostep: Option<CharacterAutostep>,
187    /// The maximum angle (radians) between the floor’s normal and the `up` vector that the
188    /// character is able to climb.
189    pub max_slope_climb_angle: Real,
190    /// The minimum angle (radians) between the floor’s normal and the `up` vector before the
191    /// character starts to slide down automatically.
192    pub min_slope_slide_angle: Real,
193    /// Should the character be automatically snapped to the ground if the distance between
194    /// the ground and its feed are smaller than the specified threshold?
195    pub snap_to_ground: Option<CharacterLength>,
196    /// Increase this number if your character appears to get stuck when sliding against surfaces.
197    ///
198    /// This is a small distance applied to the movement toward the contact normals of shapes hit
199    /// by the character controller. This helps shape-casting not getting stuck in an always-penetrating
200    /// state during the sliding calculation.
201    ///
202    /// This value should remain fairly small since it can introduce artificial "bumps" when sliding
203    /// along a flat surface.
204    pub normal_nudge_factor: Real,
205}
206
207impl Default for KinematicCharacterController {
208    fn default() -> Self {
209        Self {
210            up: Vector::Y,
211            offset: CharacterLength::Relative(0.01),
212            slide: true,
213            autostep: None,
214            max_slope_climb_angle: Real::frac_pi_4(),
215            min_slope_slide_angle: Real::frac_pi_4(),
216            snap_to_ground: Some(CharacterLength::Relative(0.2)),
217            normal_nudge_factor: 1.0e-4,
218        }
219    }
220}
221
222/// The effective movement computed by the character controller.
223#[derive(Debug)]
224pub struct EffectiveCharacterMovement {
225    /// The movement to apply.
226    pub translation: Vector,
227    /// Is the character touching the ground after applying `EffectiveKineamticMovement::translation`?
228    pub grounded: bool,
229    /// Is the character sliding down a slope due to slope angle being larger than `min_slope_slide_angle`?
230    pub is_sliding_down_slope: bool,
231}
232
233impl KinematicCharacterController {
234    fn check_and_fix_penetrations(&self) {
235        /*
236        // 1/ Check if the body is grounded and if there are penetrations.
237        let mut grounded = false;
238        let mut penetrating = false;
239
240        let mut contacts = vec![];
241
242        let aabb = shape
243            .compute_aabb(shape_pos)
244            .loosened(self.offset);
245        queries.colliders_with_aabb_intersecting_aabb(&aabb, |handle| {
246            // TODO: apply the filter.
247            if let Some(collider) = colliders.get(*handle) {
248                if let Ok(Some(contact)) = parry::query::contact(
249                    &shape_pos,
250                    shape,
251                    collider.position(),
252                    collider.shape(),
253                    self.offset,
254                ) {
255                    contacts.push((contact, collider));
256                }
257            }
258
259            true
260        });
261         */
262    }
263
264    /// Computes the possible movement for a shape.
265    #[profiling::function]
266    pub fn move_shape(
267        &self,
268        dt: Real,
269        queries: &QueryPipeline,
270        character_shape: &dyn Shape,
271        character_pos: &Pose,
272        desired_translation: Vector,
273        mut events: impl FnMut(CharacterCollision),
274    ) -> EffectiveCharacterMovement {
275        let mut result = EffectiveCharacterMovement {
276            translation: Vector::ZERO,
277            grounded: false,
278            is_sliding_down_slope: false,
279        };
280        let dims = self.compute_dims(character_shape);
281
282        // 1. Check and fix penetrations.
283        self.check_and_fix_penetrations();
284
285        let mut translation_remaining = desired_translation;
286
287        let grounded_at_starting_pos = self.detect_grounded_status_and_apply_friction(
288            dt,
289            queries,
290            character_shape,
291            character_pos,
292            dims,
293            None,
294            None,
295        );
296
297        let mut max_iters = 20;
298        let mut kinematic_friction_translation = Vector::ZERO;
299        let offset = self.offset.eval(dims.y);
300        let mut is_moving = false;
301
302        while let Some((translation_dir, translation_dist)) =
303            utils::try_normalize_and_get_length(translation_remaining, 1.0e-5)
304        {
305            if max_iters == 0 {
306                break;
307            } else {
308                max_iters -= 1;
309            }
310            is_moving = true;
311
312            // 2. Cast towards the movement direction.
313            if let Some((handle, hit)) = queries.cast_shape(
314                &(Pose::from_translation(result.translation) * *character_pos),
315                translation_dir,
316                character_shape,
317                ShapeCastOptions {
318                    target_distance: offset,
319                    stop_at_penetration: false,
320                    max_time_of_impact: translation_dist,
321                    compute_impact_geometry_on_penetration: true,
322                },
323            ) {
324                // We hit something, compute and apply the allowed interference-free translation.
325                let allowed_dist = hit.time_of_impact;
326                let allowed_translation = translation_dir * allowed_dist;
327                result.translation += allowed_translation;
328                translation_remaining -= allowed_translation;
329
330                events(CharacterCollision {
331                    handle,
332                    character_pos: Pose::from_translation(result.translation) * *character_pos,
333                    translation_applied: result.translation,
334                    translation_remaining,
335                    hit,
336                });
337
338                let hit_info = self.compute_hit_info(hit);
339
340                // Try to go upstairs.
341                if !self.handle_stairs(
342                    *queries,
343                    character_shape,
344                    &(Pose::from_translation(result.translation) * *character_pos),
345                    dims,
346                    handle,
347                    &hit_info,
348                    &mut translation_remaining,
349                    &mut result,
350                ) {
351                    // No stairs, try to move along slopes.
352                    translation_remaining = self.handle_slopes(
353                        &hit_info,
354                        desired_translation,
355                        translation_remaining,
356                        self.normal_nudge_factor,
357                        &mut result,
358                    );
359                }
360            } else {
361                // No interference along the path.
362                result.translation += translation_remaining;
363                result.grounded = self.detect_grounded_status_and_apply_friction(
364                    dt,
365                    queries,
366                    character_shape,
367                    &(Pose::from_translation(result.translation) * *character_pos),
368                    dims,
369                    None,
370                    None,
371                );
372                break;
373            }
374            result.grounded = self.detect_grounded_status_and_apply_friction(
375                dt,
376                queries,
377                character_shape,
378                &(Pose::from_translation(result.translation) * *character_pos),
379                dims,
380                Some(&mut kinematic_friction_translation),
381                Some(&mut translation_remaining),
382            );
383
384            if !self.slide {
385                break;
386            }
387        }
388        // When not moving, `detect_grounded_status_and_apply_friction` is not reached
389        // so we call it explicitly here.
390        if !is_moving {
391            result.grounded = self.detect_grounded_status_and_apply_friction(
392                dt,
393                queries,
394                character_shape,
395                &(Pose::from_translation(result.translation) * *character_pos),
396                dims,
397                None,
398                None,
399            );
400        }
401        // If needed, and if we are not already grounded, snap to the ground.
402        if grounded_at_starting_pos {
403            self.snap_to_ground(
404                queries,
405                character_shape,
406                &(Pose::from_translation(result.translation) * *character_pos),
407                dims,
408                &mut result,
409            );
410        }
411
412        // Return the result.
413        result
414    }
415
416    fn snap_to_ground(
417        &self,
418        queries: &QueryPipeline,
419        character_shape: &dyn Shape,
420        character_pos: &Pose,
421        dims: Vector2<Real>,
422        result: &mut EffectiveCharacterMovement,
423    ) -> Option<(ColliderHandle, ShapeCastHit)> {
424        if let Some(snap_distance) = self.snap_to_ground {
425            if result.translation.dot(self.up) < -1.0e-5 {
426                let snap_distance = snap_distance.eval(dims.y);
427                let offset = self.offset.eval(dims.y);
428                if let Some((hit_handle, hit)) = queries.cast_shape(
429                    character_pos,
430                    -self.up,
431                    character_shape,
432                    ShapeCastOptions {
433                        target_distance: offset,
434                        stop_at_penetration: false,
435                        max_time_of_impact: snap_distance,
436                        compute_impact_geometry_on_penetration: true,
437                    },
438                ) {
439                    // Apply the snap.
440                    result.translation -= self.up * hit.time_of_impact;
441                    result.grounded = true;
442                    return Some((hit_handle, hit));
443                }
444            }
445        }
446
447        None
448    }
449
450    fn predict_ground(&self, up_extends: Real) -> Real {
451        self.offset.eval(up_extends) + 0.05
452    }
453
454    #[profiling::function]
455    fn detect_grounded_status_and_apply_friction(
456        &self,
457        dt: Real,
458        queries: &QueryPipeline,
459        character_shape: &dyn Shape,
460        character_pos: &Pose,
461        dims: Vector2<Real>,
462        mut kinematic_friction_translation: Option<&mut Vector>,
463        mut translation_remaining: Option<&mut Vector>,
464    ) -> bool {
465        let prediction = self.predict_ground(dims.y);
466
467        // TODO: allow custom dispatchers.
468        let dispatcher = DefaultQueryDispatcher;
469
470        let mut manifolds: Vec<ContactManifold> = vec![];
471        let character_aabb = character_shape
472            .compute_aabb(character_pos)
473            .loosened(prediction);
474
475        let mut grounded = false;
476
477        'outer: for (_, collider) in queries.intersect_aabb_conservative(character_aabb) {
478            manifolds.clear();
479            let pos12 = character_pos.inv_mul(collider.position());
480            let _ = dispatcher.contact_manifolds(
481                &pos12,
482                character_shape,
483                collider.shape(),
484                prediction,
485                &mut manifolds,
486                &mut None,
487            );
488
489            if let (Some(kinematic_friction_translation), Some(translation_remaining)) = (
490                kinematic_friction_translation.as_deref_mut(),
491                translation_remaining.as_deref_mut(),
492            ) {
493                let init_kinematic_friction_translation = *kinematic_friction_translation;
494                let kinematic_parent = collider
495                    .parent
496                    .and_then(|p| queries.bodies.get(p.handle))
497                    .filter(|rb| rb.is_kinematic());
498
499                for m in &manifolds {
500                    if self.is_grounded_at_contact_manifold(m, character_pos, dims) {
501                        grounded = true;
502                    }
503
504                    if let Some(kinematic_parent) = kinematic_parent {
505                        let mut num_active_contacts = 0;
506                        let mut manifold_center = Vector::ZERO;
507                        let normal = -(character_pos.rotation * m.local_n1);
508
509                        for contact in &m.points {
510                            if contact.dist <= prediction {
511                                num_active_contacts += 1;
512                                let contact_point = collider.position() * contact.local_p2;
513                                let target_vel = kinematic_parent.velocity_at_point(contact_point);
514
515                                let normal_target_mvt = target_vel.dot(normal) * dt;
516                                let normal_current_mvt = translation_remaining.dot(normal);
517
518                                manifold_center += contact_point;
519                                *translation_remaining +=
520                                    normal * (normal_target_mvt - normal_current_mvt);
521                            }
522                        }
523
524                        if num_active_contacts > 0 {
525                            let target_vel = kinematic_parent
526                                .velocity_at_point(manifold_center / num_active_contacts as Real);
527                            let tangent_platform_mvt =
528                                (target_vel - normal * target_vel.dot(normal)) * dt;
529                            // Apply larger-absolute-value-wins component-wise
530                            if tangent_platform_mvt.x.abs() > kinematic_friction_translation.x.abs()
531                            {
532                                kinematic_friction_translation.x = tangent_platform_mvt.x;
533                            }
534                            if tangent_platform_mvt.y.abs() > kinematic_friction_translation.y.abs()
535                            {
536                                kinematic_friction_translation.y = tangent_platform_mvt.y;
537                            }
538                            #[cfg(feature = "dim3")]
539                            if tangent_platform_mvt.z.abs() > kinematic_friction_translation.z.abs()
540                            {
541                                kinematic_friction_translation.z = tangent_platform_mvt.z;
542                            }
543                        }
544                    }
545                }
546
547                *translation_remaining +=
548                    *kinematic_friction_translation - init_kinematic_friction_translation;
549            } else {
550                for m in &manifolds {
551                    if self.is_grounded_at_contact_manifold(m, character_pos, dims) {
552                        grounded = true;
553                        break 'outer; // We can stop the search early.
554                    }
555                }
556            }
557        }
558        grounded
559    }
560
561    fn is_grounded_at_contact_manifold(
562        &self,
563        manifold: &ContactManifold,
564        character_pos: &Pose,
565        dims: Vector2<Real>,
566    ) -> bool {
567        let normal = -(character_pos.rotation * manifold.local_n1);
568
569        // For the controller to be grounded, the angle between the contact normal and the up vector
570        // has to be smaller than acos(1.0e-3) = 89.94 degrees.
571        if normal.dot(self.up) >= 1.0e-3 {
572            let prediction = self.predict_ground(dims.y);
573            for contact in &manifold.points {
574                if contact.dist <= prediction {
575                    return true;
576                }
577            }
578        }
579        false
580    }
581
582    fn handle_slopes(
583        &self,
584        hit: &HitInfo,
585        movement_input: Vector,
586        translation_remaining: Vector,
587        normal_nudge_factor: Real,
588        result: &mut EffectiveCharacterMovement,
589    ) -> Vector {
590        let [_vertical_input, horizontal_input] = self.split_into_components(movement_input);
591        let horiz_input_decomp = self.decompose_hit(horizontal_input, &hit.toi);
592        let decomp = self.decompose_hit(translation_remaining, &hit.toi);
593
594        // An object is trying to slip if the tangential movement induced by its vertical movement
595        // points downward.
596        let slipping_intent = self.up.dot(horiz_input_decomp.vertical_tangent) < 0.0;
597        // An object is slipping if its vertical movement points downward.
598        let slipping = self.up.dot(decomp.vertical_tangent) < 0.0;
599
600        // An object is trying to climb if its vertical input motion points upward.
601        let climbing_intent = self.up.dot(_vertical_input) > 0.0;
602        // An object is climbing if the tangential movement induced by its vertical movement points upward.
603        let climbing = self.up.dot(decomp.vertical_tangent) > 0.0;
604
605        let allowed_movement = if hit.is_wall && climbing && !climbing_intent {
606            // Can’t climb the slope, remove the vertical tangent motion induced by the forward motion.
607            decomp.horizontal_tangent + decomp.normal_part
608        } else if hit.is_nonslip_slope && slipping && !slipping_intent {
609            // Prevent the vertical movement from sliding down.
610            decomp.horizontal_tangent + decomp.normal_part
611        } else {
612            // Let it slide (including climbing the slope).
613            result.is_sliding_down_slope = true;
614            decomp.unconstrained_slide_part()
615        };
616
617        allowed_movement + hit.toi.normal1 * normal_nudge_factor
618    }
619
620    fn split_into_components(&self, translation: Vector) -> [Vector; 2] {
621        let vertical_translation = self.up * (self.up.dot(translation));
622        let horizontal_translation = translation - vertical_translation;
623        [vertical_translation, horizontal_translation]
624    }
625
626    fn compute_hit_info(&self, toi: ShapeCastHit) -> HitInfo {
627        #[cfg(feature = "dim2")]
628        let angle_with_floor = self.up.angle_to(toi.normal1);
629        #[cfg(feature = "dim3")]
630        let angle_with_floor = self.up.angle_between(toi.normal1);
631        let is_ceiling = self.up.dot(toi.normal1) < 0.0;
632        let is_wall = angle_with_floor >= self.max_slope_climb_angle && !is_ceiling;
633        let is_nonslip_slope = angle_with_floor <= self.min_slope_slide_angle;
634
635        HitInfo {
636            toi,
637            is_wall,
638            is_nonslip_slope,
639        }
640    }
641
642    fn decompose_hit(&self, translation: Vector, hit: &ShapeCastHit) -> HitDecomposition {
643        let dist_to_surface = translation.dot(hit.normal1);
644        let normal_part;
645        let penetration_part;
646
647        if dist_to_surface < 0.0 {
648            normal_part = Vector::ZERO;
649            penetration_part = dist_to_surface * hit.normal1;
650        } else {
651            penetration_part = Vector::ZERO;
652            normal_part = dist_to_surface * hit.normal1;
653        }
654
655        let tangent = translation - normal_part - penetration_part;
656        #[cfg(feature = "dim3")]
657        let horizontal_tangent_dir = hit.normal1.cross(self.up);
658        #[cfg(feature = "dim2")]
659        let horizontal_tangent_dir = Vector::ZERO;
660
661        let horizontal_tangent_dir = horizontal_tangent_dir.try_normalize().unwrap_or_default();
662        let horizontal_tangent = tangent.dot(horizontal_tangent_dir) * horizontal_tangent_dir;
663        let vertical_tangent = tangent - horizontal_tangent;
664
665        HitDecomposition {
666            normal_part,
667            horizontal_tangent,
668            vertical_tangent,
669        }
670    }
671
672    fn compute_dims(&self, character_shape: &dyn Shape) -> Vector2<Real> {
673        let extents = character_shape.compute_local_aabb().extents();
674        let up_extent = extents.dot(self.up.abs());
675        let side_extent = (extents - (self.up).abs() * up_extent).length();
676        Vector2::new(side_extent, up_extent)
677    }
678
679    #[profiling::function]
680    fn handle_stairs(
681        &self,
682        mut queries: QueryPipeline,
683        character_shape: &dyn Shape,
684        character_pos: &Pose,
685        dims: Vector2<Real>,
686        stair_handle: ColliderHandle,
687        hit: &HitInfo,
688        translation_remaining: &mut Vector,
689        result: &mut EffectiveCharacterMovement,
690    ) -> bool {
691        let Some(autostep) = self.autostep else {
692            return false;
693        };
694
695        // Only try to autostep on walls.
696        if !hit.is_wall {
697            return false;
698        }
699
700        let offset = self.offset.eval(dims.y);
701        let min_width = autostep.min_width.eval(dims.x) + offset;
702        let max_height = autostep.max_height.eval(dims.y) + offset;
703
704        if !autostep.include_dynamic_bodies {
705            if queries
706                .colliders
707                .get(stair_handle)
708                .and_then(|co| co.parent)
709                .and_then(|p| queries.bodies.get(p.handle))
710                .map(|b| b.is_dynamic())
711                == Some(true)
712            {
713                // The "stair" is a dynamic body, which the user wants to ignore.
714                return false;
715            }
716
717            queries.filter.flags |= QueryFilterFlags::EXCLUDE_DYNAMIC;
718        }
719
720        let shifted_character_pos = Pose::from_parts(
721            character_pos.translation + self.up * max_height,
722            character_pos.rotation,
723        );
724
725        let Some(horizontal_dir) =
726            (*translation_remaining - self.up * translation_remaining.dot(self.up)).try_normalize()
727        else {
728            return false;
729        };
730
731        if queries
732            .cast_shape(
733                character_pos,
734                self.up,
735                character_shape,
736                ShapeCastOptions {
737                    target_distance: offset,
738                    stop_at_penetration: false,
739                    max_time_of_impact: max_height,
740                    compute_impact_geometry_on_penetration: true,
741                },
742            )
743            .is_some()
744        {
745            // We can’t go up.
746            return false;
747        }
748
749        if queries
750            .cast_shape(
751                &shifted_character_pos,
752                horizontal_dir,
753                character_shape,
754                ShapeCastOptions {
755                    target_distance: offset,
756                    stop_at_penetration: false,
757                    max_time_of_impact: min_width,
758                    compute_impact_geometry_on_penetration: true,
759                },
760            )
761            .is_some()
762        {
763            // We don’t have enough room on the stair to stay on it.
764            return false;
765        }
766
767        // Check that we are not getting into a ramp that is too steep
768        // after stepping.
769        if let Some((_, hit)) = queries.cast_shape(
770            &Pose::from_parts(
771                shifted_character_pos.translation + horizontal_dir * min_width,
772                shifted_character_pos.rotation,
773            ),
774            -self.up,
775            character_shape,
776            ShapeCastOptions {
777                target_distance: offset,
778                stop_at_penetration: false,
779                max_time_of_impact: max_height,
780                compute_impact_geometry_on_penetration: true,
781            },
782        ) {
783            let [vertical_slope_translation, horizontal_slope_translation] = self
784                .split_into_components(*translation_remaining)
785                .map(|remaining| subtract_hit(remaining, &hit));
786
787            let slope_translation = horizontal_slope_translation + vertical_slope_translation;
788
789            #[cfg(feature = "dim2")]
790            let angle_with_floor = self.up.angle_to(hit.normal1);
791            #[cfg(feature = "dim3")]
792            let angle_with_floor = self.up.angle_between(hit.normal1);
793            let climbing = self.up.dot(slope_translation) >= 0.0;
794
795            if climbing && angle_with_floor > self.max_slope_climb_angle {
796                return false; // The target ramp is too steep.
797            }
798        }
799
800        // We can step, we need to find the actual step height.
801        let step_height = max_height
802            - queries
803                .cast_shape(
804                    &Pose::from_parts(
805                        shifted_character_pos.translation + horizontal_dir * min_width,
806                        shifted_character_pos.rotation,
807                    ),
808                    -self.up,
809                    character_shape,
810                    ShapeCastOptions {
811                        target_distance: offset,
812                        stop_at_penetration: false,
813                        max_time_of_impact: max_height,
814                        compute_impact_geometry_on_penetration: true,
815                    },
816                )
817                .map(|hit| hit.1.time_of_impact)
818                .unwrap_or(max_height);
819
820        // Remove the step height from the vertical part of the self.
821        let step = self.up * step_height;
822        *translation_remaining -= step;
823
824        // Advance the collider on the step horizontally, to make sure further
825        // movement won’t just get stuck on its edge.
826        let horizontal_nudge =
827            horizontal_dir * horizontal_dir.dot(*translation_remaining).min(min_width);
828        *translation_remaining -= horizontal_nudge;
829
830        result.translation += step + horizontal_nudge;
831        true
832    }
833
834    /// For the given collisions between a character and its environment, this method will apply
835    /// impulses to the rigid-bodies surrounding the character shape at the time of the collisions.
836    /// Note that the impulse calculation is only approximate as it is not based on a global
837    /// constraints resolution scheme.
838    #[profiling::function]
839    pub fn solve_character_collision_impulses<'a>(
840        &self,
841        dt: Real,
842        queries: &mut QueryPipelineMut,
843        character_shape: &dyn Shape,
844        character_mass: Real,
845        collisions: impl IntoIterator<Item = &'a CharacterCollision>,
846    ) {
847        for collision in collisions {
848            self.solve_single_character_collision_impulse(
849                dt,
850                queries,
851                character_shape,
852                character_mass,
853                collision,
854            );
855        }
856    }
857
858    /// For the given collision between a character and its environment, this method will apply
859    /// impulses to the rigid-bodies surrounding the character shape at the time of the collision.
860    /// Note that the impulse calculation is only approximate as it is not based on a global
861    /// constraints resolution scheme.
862    #[profiling::function]
863    fn solve_single_character_collision_impulse(
864        &self,
865        dt: Real,
866        queries: &mut QueryPipelineMut,
867        character_shape: &dyn Shape,
868        character_mass: Real,
869        collision: &CharacterCollision,
870    ) {
871        let extents = character_shape.compute_local_aabb().extents();
872        let up_extent = extents.dot(self.up.abs());
873        let movement_to_transfer =
874            collision.hit.normal1 * collision.translation_remaining.dot(collision.hit.normal1);
875        let prediction = self.predict_ground(up_extent);
876
877        // TODO: allow custom dispatchers.
878        let dispatcher = DefaultQueryDispatcher;
879
880        let mut manifolds: Vec<ContactManifold> = vec![];
881        let character_aabb = character_shape
882            .compute_aabb(&collision.character_pos)
883            .loosened(prediction);
884
885        for (_, collider) in queries.as_ref().intersect_aabb_conservative(character_aabb) {
886            if let Some(parent) = collider.parent {
887                if let Some(body) = queries.bodies.get(parent.handle) {
888                    if body.is_dynamic() {
889                        manifolds.clear();
890                        let pos12 = collision.character_pos.inv_mul(collider.position());
891                        let prev_manifolds_len = manifolds.len();
892                        let _ = dispatcher.contact_manifolds(
893                            &pos12,
894                            character_shape,
895                            collider.shape(),
896                            prediction,
897                            &mut manifolds,
898                            &mut None,
899                        );
900
901                        for m in &mut manifolds[prev_manifolds_len..] {
902                            m.data.rigid_body2 = Some(parent.handle);
903                            m.data.normal = collision.character_pos.rotation * m.local_n1;
904                        }
905                    }
906                }
907            }
908        }
909
910        let velocity_to_transfer = movement_to_transfer * utils::inv(dt);
911
912        for manifold in &manifolds {
913            let body_handle = manifold.data.rigid_body2.unwrap();
914            let body = &mut queries.bodies[body_handle];
915
916            for pt in &manifold.points {
917                if pt.dist <= prediction {
918                    let body_mass = body.mass();
919                    let contact_point = body.position() * pt.local_p2;
920                    let delta_vel_per_contact = (velocity_to_transfer
921                        - body.velocity_at_point(contact_point))
922                    .dot(manifold.data.normal);
923                    let mass_ratio = body_mass * character_mass / (body_mass + character_mass);
924
925                    body.apply_impulse_at_point(
926                        manifold.data.normal * delta_vel_per_contact.max(0.0) * mass_ratio,
927                        contact_point,
928                        true,
929                    );
930                }
931            }
932        }
933    }
934}
935
936fn subtract_hit(translation: Vector, hit: &ShapeCastHit) -> Vector {
937    let surface_correction = (-translation).dot(hit.normal1).max(0.0);
938    // This fixes some instances of moving through walls
939    let surface_correction = surface_correction * (1.0 + 1.0e-5);
940    translation + hit.normal1 * surface_correction
941}
942
943#[cfg(all(feature = "dim3", feature = "f32"))]
944#[cfg(test)]
945mod test {
946    use crate::{
947        control::{CharacterLength, KinematicCharacterController},
948        prelude::*,
949    };
950
951    #[test]
952    fn character_controller_climb_test() {
953        let mut colliders = ColliderSet::new();
954        let mut impulse_joints = ImpulseJointSet::new();
955        let mut multibody_joints = MultibodyJointSet::new();
956        let mut pipeline = PhysicsPipeline::new();
957        let mut bf = BroadPhaseBvh::new();
958        let mut nf = NarrowPhase::new();
959        let mut islands = IslandManager::new();
960
961        let mut bodies = RigidBodySet::new();
962
963        let gravity = Vector::Y * -9.81;
964
965        let ground_size = 100.0;
966        let ground_height = 0.1;
967        /*
968         * Create a flat ground
969         */
970        let rigid_body =
971            RigidBodyBuilder::fixed().translation(Vector::new(0.0, -ground_height, 0.0));
972        let floor_handle = bodies.insert(rigid_body);
973        let collider = ColliderBuilder::cuboid(ground_size, ground_height, ground_size);
974        colliders.insert_with_parent(collider, floor_handle, &mut bodies);
975
976        /*
977         * Create a slope we can climb.
978         */
979        let slope_angle = 0.2;
980        let slope_size = 2.0;
981        let collider = ColliderBuilder::cuboid(slope_size, ground_height, slope_size)
982            .translation(Vector::new(0.1 + slope_size, -ground_height + 0.4, 0.0))
983            .rotation(Vector::Z * slope_angle);
984        colliders.insert(collider);
985
986        /*
987         * Create a slope we can't climb.
988         */
989        let impossible_slope_angle = 0.6;
990        let impossible_slope_size = 2.0;
991        let collider = ColliderBuilder::cuboid(slope_size, ground_height, ground_size)
992            .translation(Vector::new(
993                0.1 + slope_size * 2.0 + impossible_slope_size - 0.9,
994                -ground_height + 1.7,
995                0.0,
996            ))
997            .rotation(Vector::Z * impossible_slope_angle);
998        colliders.insert(collider);
999
1000        let integration_parameters = IntegrationParameters::default();
1001
1002        // Initialize character which can climb
1003        let mut character_body_can_climb = RigidBodyBuilder::kinematic_position_based()
1004            .additional_mass(1.0)
1005            .build();
1006        character_body_can_climb.set_translation(Vector::new(0.6, 0.5, 0.0), false);
1007        let character_handle_can_climb = bodies.insert(character_body_can_climb);
1008
1009        let collider = ColliderBuilder::ball(0.5).build();
1010        colliders.insert_with_parent(collider.clone(), character_handle_can_climb, &mut bodies);
1011
1012        // Initialize character which cannot climb
1013        let mut character_body_cannot_climb = RigidBodyBuilder::kinematic_position_based()
1014            .additional_mass(1.0)
1015            .build();
1016        character_body_cannot_climb.set_translation(Vector::new(-0.6, 0.5, 0.0), false);
1017        let character_handle_cannot_climb = bodies.insert(character_body_cannot_climb);
1018
1019        let collider = ColliderBuilder::ball(0.5).build();
1020        let character_shape = collider.shape();
1021        colliders.insert_with_parent(collider.clone(), character_handle_cannot_climb, &mut bodies);
1022
1023        for i in 0..200 {
1024            // Step once
1025            pipeline.step(
1026                gravity,
1027                &integration_parameters,
1028                &mut islands,
1029                &mut bf,
1030                &mut nf,
1031                &mut bodies,
1032                &mut colliders,
1033                &mut impulse_joints,
1034                &mut multibody_joints,
1035                &mut CCDSolver::new(),
1036                &(),
1037                &(),
1038            );
1039
1040            let mut update_character_controller =
1041                |controller: KinematicCharacterController, handle: RigidBodyHandle| {
1042                    let character_body = bodies.get(handle).unwrap();
1043                    // Use a closure to handle or collect the collisions while
1044                    // the character is being moved.
1045                    let mut collisions = vec![];
1046                    let filter_character_controller = QueryFilter::new().exclude_rigid_body(handle);
1047                    let query_pipeline = bf.as_query_pipeline(
1048                        nf.query_dispatcher(),
1049                        &bodies,
1050                        &colliders,
1051                        filter_character_controller,
1052                    );
1053                    let effective_movement = controller.move_shape(
1054                        integration_parameters.dt,
1055                        &query_pipeline,
1056                        character_shape,
1057                        character_body.position(),
1058                        Vector::new(0.1, -0.1, 0.0),
1059                        |collision| collisions.push(collision),
1060                    );
1061                    let character_body = bodies.get_mut(handle).unwrap();
1062                    let translation = character_body.translation();
1063                    assert!(
1064                        effective_movement.grounded,
1065                        "movement should be grounded at all times for current setup (iter: {}), pos: {}.",
1066                        i,
1067                        translation + effective_movement.translation
1068                    );
1069                    character_body.set_next_kinematic_translation(
1070                        translation + effective_movement.translation,
1071                    );
1072                };
1073
1074            let character_controller_cannot_climb = KinematicCharacterController {
1075                max_slope_climb_angle: impossible_slope_angle - 0.001,
1076                ..Default::default()
1077            };
1078            let character_controller_can_climb = KinematicCharacterController {
1079                max_slope_climb_angle: impossible_slope_angle + 0.001,
1080                ..Default::default()
1081            };
1082            update_character_controller(
1083                character_controller_cannot_climb,
1084                character_handle_cannot_climb,
1085            );
1086            update_character_controller(character_controller_can_climb, character_handle_can_climb);
1087        }
1088        let character_body = bodies.get(character_handle_can_climb).unwrap();
1089        assert!(character_body.translation().x > 6.0);
1090        assert!(character_body.translation().y > 3.0);
1091        let character_body = bodies.get(character_handle_cannot_climb).unwrap();
1092        assert!(character_body.translation().x < 4.0);
1093        assert!(dbg!(character_body.translation().y) < 2.0);
1094    }
1095
1096    #[test]
1097    fn character_controller_ground_detection() {
1098        let mut colliders = ColliderSet::new();
1099        let mut impulse_joints = ImpulseJointSet::new();
1100        let mut multibody_joints = MultibodyJointSet::new();
1101        let mut pipeline = PhysicsPipeline::new();
1102        let mut bf = BroadPhaseBvh::new();
1103        let mut nf = NarrowPhase::new();
1104        let mut islands = IslandManager::new();
1105
1106        let mut bodies = RigidBodySet::new();
1107
1108        let gravity = Vector::Y * -9.81;
1109
1110        let ground_size = 1001.0;
1111        let ground_height = 1.0;
1112        /*
1113         * Create a flat ground
1114         */
1115        let rigid_body =
1116            RigidBodyBuilder::fixed().translation(Vector::new(0.0, -ground_height / 2.0, 0.0));
1117        let floor_handle = bodies.insert(rigid_body);
1118        let collider = ColliderBuilder::cuboid(ground_size, ground_height, ground_size);
1119        colliders.insert_with_parent(collider, floor_handle, &mut bodies);
1120
1121        let integration_parameters = IntegrationParameters::default();
1122
1123        // Initialize character with snap to ground
1124        let character_controller_snap = KinematicCharacterController {
1125            snap_to_ground: Some(CharacterLength::Relative(0.2)),
1126            ..Default::default()
1127        };
1128        let mut character_body_snap = RigidBodyBuilder::kinematic_position_based()
1129            .additional_mass(1.0)
1130            .build();
1131        character_body_snap.set_translation(Vector::new(0.6, 0.5, 0.0), false);
1132        let character_handle_snap = bodies.insert(character_body_snap);
1133
1134        let collider = ColliderBuilder::ball(0.5).build();
1135        colliders.insert_with_parent(collider.clone(), character_handle_snap, &mut bodies);
1136
1137        // Initialize character without snap to ground
1138        let character_controller_no_snap = KinematicCharacterController {
1139            snap_to_ground: None,
1140            ..Default::default()
1141        };
1142        let mut character_body_no_snap = RigidBodyBuilder::kinematic_position_based()
1143            .additional_mass(1.0)
1144            .build();
1145        character_body_no_snap.set_translation(Vector::new(-0.6, 0.5, 0.0), false);
1146        let character_handle_no_snap = bodies.insert(character_body_no_snap);
1147
1148        let collider = ColliderBuilder::ball(0.5).build();
1149        let character_shape = collider.shape();
1150        colliders.insert_with_parent(collider.clone(), character_handle_no_snap, &mut bodies);
1151
1152        for i in 0..10000 {
1153            // Step once
1154            pipeline.step(
1155                gravity,
1156                &integration_parameters,
1157                &mut islands,
1158                &mut bf,
1159                &mut nf,
1160                &mut bodies,
1161                &mut colliders,
1162                &mut impulse_joints,
1163                &mut multibody_joints,
1164                &mut CCDSolver::new(),
1165                &(),
1166                &(),
1167            );
1168
1169            let mut update_character_controller =
1170                |controller: KinematicCharacterController, handle: RigidBodyHandle| {
1171                    let character_body = bodies.get(handle).unwrap();
1172                    // Use a closure to handle or collect the collisions while
1173                    // the character is being moved.
1174                    let mut collisions = vec![];
1175                    let filter_character_controller = QueryFilter::new().exclude_rigid_body(handle);
1176                    let query_pipeline = bf.as_query_pipeline(
1177                        nf.query_dispatcher(),
1178                        &bodies,
1179                        &colliders,
1180                        filter_character_controller,
1181                    );
1182                    let effective_movement = controller.move_shape(
1183                        integration_parameters.dt,
1184                        &query_pipeline,
1185                        character_shape,
1186                        character_body.position(),
1187                        Vector::new(0.1, -0.1, 0.1),
1188                        |collision| collisions.push(collision),
1189                    );
1190                    let character_body = bodies.get_mut(handle).unwrap();
1191                    let translation = character_body.translation();
1192                    assert!(
1193                        effective_movement.grounded,
1194                        "movement should be grounded at all times for current setup (iter: {}), pos: {}.",
1195                        i,
1196                        translation + effective_movement.translation
1197                    );
1198                    character_body.set_next_kinematic_translation(
1199                        translation + effective_movement.translation,
1200                    );
1201                };
1202
1203            update_character_controller(character_controller_no_snap, character_handle_no_snap);
1204            update_character_controller(character_controller_snap, character_handle_snap);
1205        }
1206
1207        let character_body = bodies.get_mut(character_handle_no_snap).unwrap();
1208        let translation = character_body.translation();
1209
1210        // accumulated numerical errors make the test go less far than it should,
1211        // but it's expected.
1212        assert!(
1213            translation.x >= 940.0,
1214            "actual translation.x:{}",
1215            translation.x
1216        );
1217        assert!(
1218            translation.z >= 940.0,
1219            "actual translation.z:{}",
1220            translation.z
1221        );
1222
1223        let character_body = bodies.get_mut(character_handle_snap).unwrap();
1224        let translation = character_body.translation();
1225        assert!(
1226            translation.x >= 960.0,
1227            "actual translation.x:{}",
1228            translation.x
1229        );
1230        assert!(
1231            translation.z >= 960.0,
1232            "actual translation.z:{}",
1233            translation.z
1234        );
1235    }
1236}