rapier3d/control/
character_controller.rs

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)]
13/// A length measure used for various options of a character controller.
14pub enum CharacterLength {
15    /// The length is specified relative to some of the character shape’s size.
16    ///
17    /// For example setting `CharacterAutostep::max_height` to `CharacterLength::Relative(0.1)`
18    /// for a shape with a height equal to 20.0 will result in a maximum step height
19    /// of `0.1 * 20.0 = 2.0`.
20    Relative(Real),
21    /// The length is specified as an absolute value, independent from the character shape’s size.
22    ///
23    /// For example setting `CharacterAutostep::max_height` to `CharacterLength::Relative(0.1)`
24    /// for a shape with a height equal to 20.0 will result in a maximum step height
25    /// of `0.1` (the shape height is ignored in for this value).
26    Absolute(Real),
27}
28
29impl CharacterLength {
30    /// Returns `self` with its value changed by the closure `f` if `self` is the `Self::Absolute`
31    /// variant.
32    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    /// Returns `self` with its value changed by the closure `f` if `self` is the `Self::Relative`
41    /// variant.
42    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/// Configuration for the auto-stepping character controller feature.
66#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
67#[derive(Copy, Clone, Debug, PartialEq)]
68pub struct CharacterAutostep {
69    /// The maximum step height a character can automatically step over.
70    pub max_height: CharacterLength,
71    /// The minimum width of free space that must be available after stepping on a stair.
72    pub min_width: CharacterLength,
73    /// Can the character automatically step over dynamic bodies too?
74    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    // NOTE: we don’t store the penetration part since we don’t really need it
93    //       for anything.
94}
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/// A collision between the character and its environment during its movement.
103#[derive(Copy, Clone, Debug)]
104pub struct CharacterCollision {
105    /// The collider hit by the character.
106    pub handle: ColliderHandle,
107    /// The position of the character when the collider was hit.
108    pub character_pos: Isometry<Real>,
109    /// The translation that was already applied to the character when the hit happens.
110    pub translation_applied: Vector<Real>,
111    /// The translations that was still waiting to be applied to the character when the hit happens.
112    pub translation_remaining: Vector<Real>,
113    /// Geometric information about the hit.
114    pub hit: ShapeCastHit,
115}
116
117/// A kinematic character controller for player/NPC movement (walking, climbing, sliding).
118///
119/// This provides classic game character movement: walking on floors, sliding on slopes,
120/// climbing stairs, and snapping to ground. It's kinematic (not physics-based), meaning
121/// you control movement directly rather than applying forces.
122///
123/// **Not suitable for:** Ragdolls, vehicles, or physics-driven movement (use dynamic bodies instead).
124///
125/// # How it works
126///
127/// 1. You provide desired movement (e.g., "move forward 5 units")
128/// 2. Controller casts the character shape through the world
129/// 3. It handles collisions: sliding along walls, stepping up stairs, snapping to ground
130/// 4. Returns the final movement to apply
131///
132/// # Example
133///
134/// ```
135/// # use rapier3d::prelude::*;
136/// # use rapier3d::control::{CharacterAutostep, KinematicCharacterController};
137/// # use nalgebra::Isometry3;
138/// # let mut bodies = RigidBodySet::new();
139/// # let mut colliders = ColliderSet::new();
140/// # let broad_phase = BroadPhaseBvh::new();
141/// # let narrow_phase = NarrowPhase::new();
142/// # let dt = 1.0 / 60.0;
143/// # let speed = 5.0;
144/// # let (input_x, input_z) = (1.0, 0.0);
145/// # let character_shape = Ball::new(0.5);
146/// # let mut character_pos = Isometry3::identity();
147/// # let query_pipeline = broad_phase.as_query_pipeline(
148/// #     narrow_phase.query_dispatcher(),
149/// #     &bodies,
150/// #     &colliders,
151/// #     QueryFilter::default(),
152/// # );
153/// let controller = KinematicCharacterController {
154///     slide: true,  // Slide along walls instead of stopping
155///     autostep: Some(CharacterAutostep::default()),  // Auto-climb stairs
156///     max_slope_climb_angle: 45.0_f32.to_radians(),  // Max climbable slope
157///     ..Default::default()
158/// };
159///
160/// // In your game loop:
161/// let desired_movement = vector![input_x, 0.0, input_z] * speed * dt;
162/// let movement = controller.move_shape(
163///     dt,
164///     &query_pipeline,
165///     &character_shape,
166///     &character_pos,
167///     desired_movement,
168///     |_| {}  // Collision event callback
169/// );
170/// character_pos.translation.vector += movement.translation;
171/// ```
172#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
173#[derive(Copy, Clone, Debug)]
174pub struct KinematicCharacterController {
175    /// The direction that goes "up". Used to determine where the floor is, and the floor’s angle.
176    pub up: UnitVector<Real>,
177    /// A small gap to preserve between the character and its surroundings.
178    ///
179    /// This value should not be too large to avoid visual artifacts, but shouldn’t be too small
180    /// (must not be zero) to improve numerical stability of the character controller.
181    pub offset: CharacterLength,
182    /// Should the character try to slide against the floor if it hits it?
183    pub slide: bool,
184    /// Should the character automatically step over small obstacles? (disabled by default)
185    ///
186    /// Note that autostepping is currently a very computationally expensive feature, so it
187    /// is disabled by default.
188    pub autostep: Option<CharacterAutostep>,
189    /// The maximum angle (radians) between the floor’s normal and the `up` vector that the
190    /// character is able to climb.
191    pub max_slope_climb_angle: Real,
192    /// The minimum angle (radians) between the floor’s normal and the `up` vector before the
193    /// character starts to slide down automatically.
194    pub min_slope_slide_angle: Real,
195    /// Should the character be automatically snapped to the ground if the distance between
196    /// the ground and its feed are smaller than the specified threshold?
197    pub snap_to_ground: Option<CharacterLength>,
198    /// Increase this number if your character appears to get stuck when sliding against surfaces.
199    ///
200    /// This is a small distance applied to the movement toward the contact normals of shapes hit
201    /// by the character controller. This helps shape-casting not getting stuck in an always-penetrating
202    /// state during the sliding calculation.
203    ///
204    /// This value should remain fairly small since it can introduce artificial "bumps" when sliding
205    /// along a flat surface.
206    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/// The effective movement computed by the character controller.
225#[derive(Debug)]
226pub struct EffectiveCharacterMovement {
227    /// The movement to apply.
228    pub translation: Vector<Real>,
229    /// Is the character touching the ground after applying `EffectiveKineamticMovement::translation`?
230    pub grounded: bool,
231    /// Is the character sliding down a slope due to slope angle being larger than `min_slope_slide_angle`?
232    pub is_sliding_down_slope: bool,
233}
234
235impl KinematicCharacterController {
236    fn check_and_fix_penetrations(&self) {
237        /*
238        // 1/ Check if the body is grounded and if there are penetrations.
239        let mut grounded = false;
240        let mut penetrating = false;
241
242        let mut contacts = vec![];
243
244        let aabb = shape
245            .compute_aabb(shape_pos)
246            .loosened(self.offset);
247        queries.colliders_with_aabb_intersecting_aabb(&aabb, |handle| {
248            // TODO: apply the filter.
249            if let Some(collider) = colliders.get(*handle) {
250                if let Ok(Some(contact)) = parry::query::contact(
251                    &shape_pos,
252                    shape,
253                    collider.position(),
254                    collider.shape(),
255                    self.offset,
256                ) {
257                    contacts.push((contact, collider));
258                }
259            }
260
261            true
262        });
263         */
264    }
265
266    /// Computes the possible movement for a shape.
267    #[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        // 1. Check and fix penetrations.
285        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            // 2. Cast towards the movement direction.
315            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                // We hit something, compute and apply the allowed interference-free translation.
327                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                // Try to go upstairs.
343                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                    // No stairs, try to move along slopes.
354                    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                // No interference along the path.
364                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        // When not moving, `detect_grounded_status_and_apply_friction` is not reached
392        // so we call it explicitly here.
393        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 needed, and if we are not already grounded, snap to the ground.
405        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        // Return the result.
416        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                    // Apply the snap.
443                    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        // TODO: allow custom dispatchers.
471        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; // We can stop the search early.
552                    }
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        // For the controller to be grounded, the angle between the contact normal and the up vector
568        // has to be smaller than acos(1.0e-3) = 89.94 degrees.
569        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        // An object is trying to slip if the tangential movement induced by its vertical movement
593        // points downward.
594        let slipping_intent = self.up.dot(&horiz_input_decomp.vertical_tangent) < 0.0;
595        // An object is slipping if its vertical movement points downward.
596        let slipping = self.up.dot(&decomp.vertical_tangent) < 0.0;
597
598        // An object is trying to climb if its vertical input motion points upward.
599        let climbing_intent = self.up.dot(&_vertical_input) > 0.0;
600        // An object is climbing if the tangential movement induced by its vertical movement points upward.
601        let climbing = self.up.dot(&decomp.vertical_tangent) > 0.0;
602
603        let allowed_movement = if hit.is_wall && climbing && !climbing_intent {
604            // Can’t climb the slope, remove the vertical tangent motion induced by the forward motion.
605            decomp.horizontal_tangent + decomp.normal_part
606        } else if hit.is_nonslip_slope && slipping && !slipping_intent {
607            // Prevent the vertical movement from sliding down.
608            decomp.horizontal_tangent + decomp.normal_part
609        } else {
610            // Let it slide (including climbing the slope).
611            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        // Only try to autostep on walls.
693        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                // The "stair" is a dynamic body, which the user wants to ignore.
711                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            // We can’t go up.
740            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            // We don’t have enough room on the stair to stay on it.
758            return false;
759        }
760
761        // Check that we are not getting into a ramp that is too steep
762        // after stepping.
763        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; // The target ramp is too steep.
785            }
786        }
787
788        // We can step, we need to find the actual step height.
789        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        // Remove the step height from the vertical part of the self.
806        let step = *self.up * step_height;
807        *translation_remaining -= step;
808
809        // Advance the collider on the step horizontally, to make sure further
810        // movement won’t just get stuck on its edge.
811        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    /// For the given collisions between a character and its environment, this method will apply
820    /// impulses to the rigid-bodies surrounding the character shape at the time of the collisions.
821    /// Note that the impulse calculation is only approximate as it is not based on a global
822    /// constraints resolution scheme.
823    #[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    /// For the given collision between a character and its environment, this method will apply
844    /// impulses to the rigid-bodies surrounding the character shape at the time of the collision.
845    /// Note that the impulse calculation is only approximate as it is not based on a global
846    /// constraints resolution scheme.
847    #[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        // TODO: allow custom dispatchers.
863        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    // This fixes some instances of moving through walls
924    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        /*
953         * Create a flat ground
954         */
955        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        /*
961         * Create a slope we can climb.
962         */
963        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        /*
971         * Create a slope we can’t climb.
972         */
973        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        // Initialize character which can climb
987        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        // Initialize character which cannot climb
997        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            // Step once
1009            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                    // Use a closure to handle or collect the collisions while
1028                    // the character is being moved.
1029                    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        /*
1097         * Create a flat ground
1098         */
1099        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        // Initialize character with snap to ground
1108        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        // Initialize character without snap to ground
1122        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            // Step once
1138            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                    // Use a closure to handle or collect the collisions while
1157                    // the character is being moved.
1158                    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        // accumulated numerical errors make the test go less far than it should,
1195        // but it's expected.
1196        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}