// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
pub fn CharacterLength::relative(value : @core.Real) -> CharacterLength {
Relative(value)
}
///|
pub fn CharacterLength::absolute(value : @core.Real) -> CharacterLength {
Absolute(value)
}
///|
pub fn CharacterLength::map_absolute(
self : CharacterLength,
f : (@core.Real) -> @core.Real,
) -> CharacterLength {
match self {
Absolute(value) => Absolute(f(value))
_ => self
}
}
///|
pub fn CharacterLength::map_relative(
self : CharacterLength,
f : (@core.Real) -> @core.Real,
) -> CharacterLength {
match self {
Relative(value) => Relative(f(value))
_ => self
}
}
///|
fn character_length_eval(
length : CharacterLength,
reference : @core.Real,
) -> @core.Real {
match length {
Relative(value) => value * reference
Absolute(value) => value
}
}
///|
pub struct CharacterCollision {
handle : @collision.ColliderHandle
hit : @collision.ShapeCastHit
translation_applied : @core.Vec2
translation_remaining : @core.Vec2
character_pos : @core.Isometry2
}
///|
pub fn CharacterCollision::handle(
self : CharacterCollision,
) -> @collision.ColliderHandle {
self.handle
}
///|
pub fn CharacterCollision::hit(
self : CharacterCollision,
) -> @collision.ShapeCastHit {
self.hit
}
///|
pub fn CharacterCollision::translation_applied(
self : CharacterCollision,
) -> @core.Vec2 {
self.translation_applied
}
///|
pub fn CharacterCollision::translation_remaining(
self : CharacterCollision,
) -> @core.Vec2 {
self.translation_remaining
}
///|
pub fn CharacterCollision::character_pos(
self : CharacterCollision,
) -> @core.Isometry2 {
self.character_pos
}
///|
pub struct CharacterAutostep {
max_height : CharacterLength
min_width : CharacterLength
include_dynamic_bodies : Bool
}
///|
pub fn CharacterAutostep::CharacterAutostep(
max_height : CharacterLength,
min_width : CharacterLength,
) -> CharacterAutostep {
// Rapier parity: include dynamic bodies by default.
{ max_height, min_width, include_dynamic_bodies: true, }
}
///|
pub fn CharacterAutostep::default() -> CharacterAutostep {
// Rapier parity (v0.32.0):
// - max_height: Relative(0.25)
// - min_width: Relative(0.5)
// - include_dynamic_bodies: true
{
max_height: CharacterLength::relative(0.25F),
min_width: CharacterLength::relative(0.5F),
include_dynamic_bodies: true,
}
}
///|
pub fn CharacterAutostep::max_height(
self : CharacterAutostep,
) -> CharacterLength {
self.max_height
}
///|
pub fn CharacterAutostep::min_width(
self : CharacterAutostep,
) -> CharacterLength {
self.min_width
}
///|
pub fn CharacterAutostep::include_dynamic_bodies(
self : CharacterAutostep,
) -> Bool {
self.include_dynamic_bodies
}
///|
pub fn CharacterAutostep::with_include_dynamic_bodies(
self : CharacterAutostep,
include_dynamic_bodies : Bool,
) -> CharacterAutostep {
{
max_height: self.max_height,
min_width: self.min_width,
include_dynamic_bodies,
}
}
///|
pub struct EffectiveMovement {
translation : @core.Vec2
grounded : Bool
is_sliding_down_slope : Bool
}
///|
pub type EffectiveCharacterMovement = EffectiveMovement
///|
pub fn EffectiveMovement::translation(self : EffectiveMovement) -> @core.Vec2 {
self.translation
}
///|
pub fn EffectiveMovement::is_grounded(self : EffectiveMovement) -> Bool {
self.grounded
}
///|
pub fn EffectiveMovement::is_sliding_down_slope(
self : EffectiveMovement,
) -> Bool {
self.is_sliding_down_slope
}
///|
pub struct KinematicCharacterController {
up : @core.Vec2
offset : CharacterLength
slide : Bool
max_slope_climb_angle : @core.Real
min_slope_slide_angle : @core.Real
snap_to_ground : CharacterLength?
autostep : CharacterAutostep?
normal_nudge_factor : @core.Real
}
///|
pub fn KinematicCharacterController::KinematicCharacterController() -> KinematicCharacterController {
{
up: Vec2(0.0F, 1.0F),
offset: CharacterLength::relative(0.01F),
slide: true,
max_slope_climb_angle: 0.785398163F,
min_slope_slide_angle: 0.785398163F,
snap_to_ground: Some(CharacterLength::relative(0.2F)),
autostep: None,
normal_nudge_factor: 1.0e-4F,
}
}
///|
pub fn KinematicCharacterController::default() -> KinematicCharacterController {
KinematicCharacterController()
}
///|
pub fn KinematicCharacterController::with_up(
self : KinematicCharacterController,
up : @core.Vec2,
) -> KinematicCharacterController {
{
up,
offset: self.offset,
slide: self.slide,
max_slope_climb_angle: self.max_slope_climb_angle,
min_slope_slide_angle: self.min_slope_slide_angle,
snap_to_ground: self.snap_to_ground,
autostep: self.autostep,
normal_nudge_factor: self.normal_nudge_factor,
}
}
///|
pub fn KinematicCharacterController::with_offset(
self : KinematicCharacterController,
offset : CharacterLength,
) -> KinematicCharacterController {
{
up: self.up,
offset,
slide: self.slide,
max_slope_climb_angle: self.max_slope_climb_angle,
min_slope_slide_angle: self.min_slope_slide_angle,
snap_to_ground: self.snap_to_ground,
autostep: self.autostep,
normal_nudge_factor: self.normal_nudge_factor,
}
}
///|
pub fn KinematicCharacterController::with_slide(
self : KinematicCharacterController,
slide : Bool,
) -> KinematicCharacterController {
{
up: self.up,
offset: self.offset,
slide,
max_slope_climb_angle: self.max_slope_climb_angle,
min_slope_slide_angle: self.min_slope_slide_angle,
snap_to_ground: self.snap_to_ground,
autostep: self.autostep,
normal_nudge_factor: self.normal_nudge_factor,
}
}
///|
pub fn KinematicCharacterController::with_max_slope_climb_angle(
self : KinematicCharacterController,
angle : @core.Real,
) -> KinematicCharacterController {
{
up: self.up,
offset: self.offset,
slide: self.slide,
max_slope_climb_angle: angle,
min_slope_slide_angle: self.min_slope_slide_angle,
snap_to_ground: self.snap_to_ground,
autostep: self.autostep,
normal_nudge_factor: self.normal_nudge_factor,
}
}
///|
pub fn KinematicCharacterController::with_min_slope_slide_angle(
self : KinematicCharacterController,
angle : @core.Real,
) -> KinematicCharacterController {
{
up: self.up,
offset: self.offset,
slide: self.slide,
max_slope_climb_angle: self.max_slope_climb_angle,
min_slope_slide_angle: angle,
snap_to_ground: self.snap_to_ground,
autostep: self.autostep,
normal_nudge_factor: self.normal_nudge_factor,
}
}
///|
pub fn KinematicCharacterController::with_snap_to_ground(
self : KinematicCharacterController,
snap : CharacterLength?,
) -> KinematicCharacterController {
{
up: self.up,
offset: self.offset,
slide: self.slide,
max_slope_climb_angle: self.max_slope_climb_angle,
min_slope_slide_angle: self.min_slope_slide_angle,
snap_to_ground: snap,
autostep: self.autostep,
normal_nudge_factor: self.normal_nudge_factor,
}
}
///|
pub fn KinematicCharacterController::with_autostep(
self : KinematicCharacterController,
autostep : CharacterAutostep?,
) -> KinematicCharacterController {
{
up: self.up,
offset: self.offset,
slide: self.slide,
max_slope_climb_angle: self.max_slope_climb_angle,
min_slope_slide_angle: self.min_slope_slide_angle,
snap_to_ground: self.snap_to_ground,
autostep,
normal_nudge_factor: self.normal_nudge_factor,
}
}
///|
pub fn KinematicCharacterController::with_normal_nudge_factor(
self : KinematicCharacterController,
normal_nudge_factor : @core.Real,
) -> KinematicCharacterController {
{
up: self.up,
offset: self.offset,
slide: self.slide,
max_slope_climb_angle: self.max_slope_climb_angle,
min_slope_slide_angle: self.min_slope_slide_angle,
snap_to_ground: self.snap_to_ground,
autostep: self.autostep,
normal_nudge_factor,
}
}
///|
fn vec2_scale(v : @core.Vec2, s : @core.Real) -> @core.Vec2 {
Vec2(v.x * s, v.y * s)
}
///|
fn vec2_neg(v : @core.Vec2) -> @core.Vec2 {
Vec2(-v.x, -v.y)
}
///|
fn try_normalize_and_get_length(
v : @core.Vec2,
eps : @core.Real,
) -> (@core.Vec2, @core.Real)? {
let len = v.length()
if len <= eps {
None
} else {
Some((vec2_scale(v, 1.0F / len), len))
}
}
///|
fn compute_extents(shape : @collision.Shape) -> (@core.Real, @core.Real) {
match shape {
Round(inner, radius) => {
let (hw, hh) = compute_extents(inner.val)
(hw + radius, hh + radius)
}
Ball(r) => (r, r)
Cuboid(hw, hh) => (hw, hh)
HalfSpace(_) => (0.0F, 0.0F)
CapsuleX(half_height, radius) => (half_height + radius, radius)
CapsuleY(half_height, radius) => (radius, half_height + radius)
Segment(a, b) => {
let hw = max_real(@core.abs(a.x), @core.abs(b.x))
let hh = max_real(@core.abs(a.y), @core.abs(b.y))
(hw, hh)
}
Polyline(vertices, _) => {
let mut hw = 0.0F
let mut hh = 0.0F
for i in 0.. hw {
hw = ax
}
if ay > hh {
hh = ay
}
}
(hw, hh)
}
HeightField(heights, scale) => {
let hw = @core.abs(scale.x) * 0.5F
let mut hh = 0.0F
for i in 0.. hh {
hh = ay
}
}
(hw, hh)
}
ConvexPolygon(vertices) => {
let mut hw = 0.0F
let mut hh = 0.0F
for i in 0.. hw {
hw = ax
}
if ay > hh {
hh = ay
}
}
(hw, hh)
}
TriMesh(vertices, _) => {
let mut hw = 0.0F
let mut hh = 0.0F
for i in 0.. hw {
hw = ax
}
if ay > hh {
hh = ay
}
}
(hw, hh)
}
Compound(parts) => {
let mut hw = 0.0F
let mut hh = 0.0F
for i in 0.. hw {
hw = cand_hw
}
if cand_hh > hh {
hh = cand_hh
}
}
(hw, hh)
}
}
}
///|
fn compute_dims_with_up(
shape : @collision.Shape,
up : @core.Vec2,
) -> (@core.Real, @core.Real) {
let (hw, hh) = compute_extents(shape)
let extents = @core.Vec2(hw, hh)
let up_abs = @core.Vec2(@core.abs(up.x), @core.abs(up.y))
let up_extent = extents.dot(up_abs)
let side_vec = extents.sub(vec2_scale(up_abs, up_extent))
let side_extent = side_vec.length()
(side_extent, up_extent)
}
///|
fn min_real(a : @core.Real, b : @core.Real) -> @core.Real {
if a < b {
a
} else {
b
}
}
///|
fn max_real(a : @core.Real, b : @core.Real) -> @core.Real {
if a > b {
a
} else {
b
}
}
///|
fn angle_between(u : @core.Vec2, v : @core.Vec2) -> @core.Real {
// Robust 2D angle computation without acos: angle = atan2(|u×v|, u·v).
let cross = u.cross(v)
let dot = u.dot(v)
@core.abs(@core.atan2(cross, dot))
}
///|
fn split_into_components(
up : @core.Vec2,
translation : @core.Vec2,
) -> (@core.Vec2, @core.Vec2) {
let vertical = vec2_scale(up, up.dot(translation))
let horizontal = translation.sub(vertical)
(vertical, horizontal)
}
///|
fn subtract_hit(
translation : @core.Vec2,
hit : @collision.ShapeCastHit,
) -> @core.Vec2 {
let n = hit.normal()
let surface_correction = max_real(vec2_neg(translation).dot(n), 0.0F)
// Match Rapier's small multiplier to avoid tunneling through walls.
let surface_correction = surface_correction * (1.0F + 1.0e-5F)
translation.add(vec2_scale(n, surface_correction))
}
///|
priv struct HitInfo {
toi : @collision.ShapeCastHit
is_wall : Bool
is_nonslip_slope : Bool
}
///|
priv struct HitDecomposition {
normal_part : @core.Vec2
horizontal_tangent : @core.Vec2
vertical_tangent : @core.Vec2
}
///|
fn HitDecomposition::unconstrained_slide_part(
self : HitDecomposition,
) -> @core.Vec2 {
self.normal_part.add(self.horizontal_tangent).add(self.vertical_tangent)
}
///|
fn compute_hit_info(
controller : KinematicCharacterController,
toi : @collision.ShapeCastHit,
) -> HitInfo {
let up = controller.up
let angle_with_floor = angle_between(up, toi.normal())
let is_ceiling = up.dot(toi.normal()) < 0.0F
let is_wall = angle_with_floor >= controller.max_slope_climb_angle &&
!is_ceiling
let is_nonslip_slope = angle_with_floor <= controller.min_slope_slide_angle
{ toi, is_wall, is_nonslip_slope, }
}
///|
fn decompose_hit(
controller : KinematicCharacterController,
translation : @core.Vec2,
hit : @collision.ShapeCastHit,
) -> HitDecomposition {
let n = hit.normal()
let dist_to_surface = translation.dot(n)
let (normal_part, penetration_part) = if dist_to_surface < 0.0F {
(@core.Vec2::zero(), vec2_scale(n, dist_to_surface))
} else {
(vec2_scale(n, dist_to_surface), @core.Vec2::zero())
}
let tangent = translation.sub(normal_part).sub(penetration_part)
// Rapier dim2 parity: the tangent space is 1D, so horizontal_tangent is always zero.
controller |> ignore
{
normal_part,
horizontal_tangent: @core.Vec2::zero(),
vertical_tangent: tangent,
}
}
///|
fn handle_slopes(
controller : KinematicCharacterController,
hit : HitInfo,
movement_input : @core.Vec2,
translation_remaining : @core.Vec2,
) -> (@core.Vec2, Bool) {
let up = controller.up
let (vertical_input, horizontal_input) = split_into_components(
up, movement_input,
)
let horiz_input_decomp = decompose_hit(controller, horizontal_input, hit.toi)
let decomp = decompose_hit(controller, translation_remaining, hit.toi)
// An object is trying to slip if the tangential movement induced by its horizontal movement points downward.
let slipping_intent = up.dot(horiz_input_decomp.vertical_tangent) < 0.0F
// An object is slipping if its tangential movement points downward.
let slipping = up.dot(decomp.vertical_tangent) < 0.0F
// An object is trying to climb if its vertical input motion points upward.
let climbing_intent = up.dot(vertical_input) > 0.0F
// An object is climbing if the tangential movement induced by its movement points upward.
let climbing = up.dot(decomp.vertical_tangent) > 0.0F
let mut is_sliding = false
let allowed = if hit.is_wall && climbing && !climbing_intent {
decomp.horizontal_tangent.add(decomp.normal_part)
} else if hit.is_nonslip_slope && slipping && !slipping_intent {
decomp.horizontal_tangent.add(decomp.normal_part)
} else {
is_sliding = true
decomp.unconstrained_slide_part()
}
let nudged = allowed.add(
vec2_scale(hit.toi.normal(), controller.normal_nudge_factor),
)
(nudged, is_sliding)
}
///|
fn detect_ground_contact(
query : @collision.QueryPipeline,
bodies : @dynamics.RigidBodySet,
colliders : @collision.ColliderSet,
shape : @collision.Shape,
position : @core.Isometry2,
up : @core.Vec2,
prediction : @core.Real,
offset : @core.Real,
) -> (@collision.ColliderHandle, @core.Vec2, @core.Vec2)? {
// Rapier parity: grounded detection uses a conservative prediction based on offset.
let down = vec2_neg(up)
let options = @collision.ShapeCastOptions(prediction, true).with_target_distance(
offset,
)
if query.cast_shape(bodies, colliders, position, down, shape, options)
is Some(hit) {
let normal = hit.1.normal()
// For the controller to be grounded, the angle between the contact normal and the up vector
// has to be smaller than acos(1.0e-3) ≈ 89.94 degrees.
if normal.dot(up) >= 1.0e-3F {
Some((hit.0, normal, hit.1.point()))
} else {
None
}
} else {
None
}
}
///|
fn attempt_autostep(
query : @collision.QueryPipeline,
bodies : @dynamics.RigidBodySet,
colliders : @collision.ColliderSet,
shape : @collision.Shape,
start_pos : @core.Isometry2,
remaining : @core.Vec2,
controller : KinematicCharacterController,
dims : (@core.Real, @core.Real),
stair_handle : @collision.ColliderHandle,
autostep : CharacterAutostep,
) -> (@core.Vec2, @core.Vec2)? {
let up = controller.up
let down = vec2_neg(up)
let (side_extent, up_extent) = dims
let offset = character_length_eval(controller.offset, up_extent)
let min_width = character_length_eval(autostep.min_width, side_extent) +
offset
let max_height = character_length_eval(autostep.max_height, up_extent) +
offset
if max_height <= 0.0F || min_width <= 0.0F {
return None
}
if !autostep.include_dynamic_bodies() {
// If the obstacle is dynamic, ignore it (Rapier parity).
if query.collider(colliders, stair_handle) is Some(co) &&
co.parent() is Some(parent) &&
query.rigid_body(bodies, parent) is Some(body) &&
body.is_dynamic() {
return None
}
}
let shifted_pos = @core.Isometry2(
start_pos.translation.add(vec2_scale(up, max_height)),
start_pos.rotation,
)
let horiz = remaining.sub(vec2_scale(up, up.dot(remaining)))
let horizontal_dir = if try_normalize_and_get_length(horiz, 1.0e-6F)
is Some(step) {
step.0
} else {
return None
}
// Can we go up?
if query.cast_shape(
bodies,
colliders,
start_pos,
up,
shape,
@collision.ShapeCastOptions(max_height, false).with_target_distance(
offset,
),
)
is Some(_) {
return None
}
// Is there enough room on the stair after stepping?
if query.cast_shape(
bodies,
colliders,
shifted_pos,
horizontal_dir,
shape,
@collision.ShapeCastOptions(min_width, false).with_target_distance(offset),
)
is Some(_) {
return None
}
// Check the ramp we would land on isn't too steep.
let pos_on_stair = @core.Isometry2(
shifted_pos.translation.add(vec2_scale(horizontal_dir, min_width)),
shifted_pos.rotation,
)
if query.cast_shape(
bodies,
colliders,
pos_on_stair,
down,
shape,
@collision.ShapeCastOptions(max_height, false).with_target_distance(
offset,
),
)
is Some(hit_down) {
let (vertical_input, horizontal_input) = split_into_components(
up, remaining,
)
let vertical_slope = subtract_hit(vertical_input, hit_down.1)
let horizontal_slope = subtract_hit(horizontal_input, hit_down.1)
let slope_translation = horizontal_slope.add(vertical_slope)
let angle = angle_between(up, hit_down.1.normal())
let climbing = up.dot(slope_translation) >= 0.0F
if climbing && angle > controller.max_slope_climb_angle {
return None
}
}
// Find actual step height.
let toi_down = if query.cast_shape(
bodies,
colliders,
pos_on_stair,
down,
shape,
@collision.ShapeCastOptions(max_height, false).with_target_distance(
offset,
),
)
is Some(hit2) {
hit2.1.toi()
} else {
max_height
}
let step_height = max_height - toi_down
let step = vec2_scale(up, step_height)
let remaining1 = remaining.sub(step)
let nudge_amt = min_real(horizontal_dir.dot(remaining1), min_width)
let horizontal_nudge = vec2_scale(horizontal_dir, nudge_amt)
let remaining2 = remaining1.sub(horizontal_nudge)
let delta = step.add(horizontal_nudge)
Some((remaining2, delta))
}
///|
fn detect_grounded(
query : @collision.QueryPipeline,
bodies : @dynamics.RigidBodySet,
colliders : @collision.ColliderSet,
shape : @collision.Shape,
position : @core.Isometry2,
up : @core.Vec2,
prediction : @core.Real,
offset : @core.Real,
) -> Bool {
detect_ground_contact(
query, bodies, colliders, shape, position, up, prediction, offset,
)
is Some(_)
}
///|
fn grounded_info(
query : @collision.QueryPipeline,
bodies : @dynamics.RigidBodySet,
colliders : @collision.ColliderSet,
shape : @collision.Shape,
position : @core.Isometry2,
up : @core.Vec2,
prediction : @core.Real,
offset : @core.Real,
) -> (Bool, @core.Vec2) {
if detect_ground_contact(
query, bodies, colliders, shape, position, up, prediction, offset,
)
is Some(hit) {
let handle = hit.0
if query.collider(colliders, handle) is Some(co) &&
co.parent is Some(parent) &&
query.rigid_body(bodies, parent) is Some(body) &&
body.is_kinematic() {
(true, body.linvel())
} else {
(true, @core.Vec2::zero())
}
} else {
(false, @core.Vec2::zero())
}
}
///|
pub fn KinematicCharacterController::move_shape(
self : KinematicCharacterController,
dt : @core.Real,
query : @collision.QueryPipeline,
bodies : @dynamics.RigidBodySet,
colliders : @collision.ColliderSet,
shape : @collision.Shape,
position : @core.Isometry2,
translation : @core.Vec2,
handle_collision : (CharacterCollision) -> Unit,
) -> EffectiveMovement {
let up = self.up
let (side_extent, up_extent) = compute_dims_with_up(shape, up)
let offset = character_length_eval(self.offset, up_extent)
let prediction = offset + 0.05F
let dt_value = if dt < 0.0F { 0.0F } else { dt }
let mut result_translation = @core.Vec2::zero()
let mut sliding_down_slope = false
// Rapier parity: `check_and_fix_penetrations` is currently a stub upstream.
let grounded_at_start = grounded_info(
query,
bodies,
colliders,
shape,
Isometry2(position.translation.add(result_translation), position.rotation),
up,
prediction,
offset,
).0
let mut remaining = translation
let mut grounded = false
let mut iters = 0
while try_normalize_and_get_length(remaining, 1.0e-5F) is Some(step) {
if iters >= 20 {
break
}
iters = iters + 1
let dir = step.0
let dist = step.1
let current_pos = @core.Isometry2(
position.translation.add(result_translation),
position.rotation,
)
let options = @collision.ShapeCastOptions(dist, false).with_target_distance(
offset,
)
if query.cast_shape(bodies, colliders, current_pos, dir, shape, options)
is Some(hit) {
let toi = hit.1.toi()
let allowed_dist = toi
let applied = vec2_scale(dir, allowed_dist)
result_translation = result_translation.add(applied)
remaining = remaining.sub(applied)
let event_pos = @core.Isometry2(
position.translation.add(result_translation),
position.rotation,
)
handle_collision({
handle: hit.0,
hit: hit.1,
translation_applied: result_translation,
translation_remaining: remaining,
character_pos: event_pos,
})
let hit_info = compute_hit_info(self, hit.1)
if self.autostep is Some(autostep) && hit_info.is_wall {
if attempt_autostep(
query,
bodies,
colliders,
shape,
event_pos,
remaining,
self,
(side_extent, up_extent),
hit.0,
autostep,
)
is Some(step) {
remaining = step.0
result_translation = result_translation.add(step.1)
continue
}
}
let (new_remaining, slid) = handle_slopes(
self, hit_info, translation, remaining,
)
remaining = new_remaining
sliding_down_slope = sliding_down_slope || slid
} else {
result_translation = result_translation.add(remaining)
remaining = @core.Vec2::zero()
break
}
grounded = grounded ||
detect_grounded(
query,
bodies,
colliders,
shape,
Isometry2(
position.translation.add(result_translation),
position.rotation,
),
up,
prediction,
offset,
)
if !self.slide {
break
}
}
if iters == 0 {
grounded = grounded_info(
query, bodies, colliders, shape, position, up, prediction, offset,
).0
}
if grounded_at_start && !grounded && self.snap_to_ground is Some(snap_length) {
let snap_dist = character_length_eval(snap_length, up_extent)
if result_translation.dot(up) < -1.0e-5F && snap_dist > 0.0F {
let current_pos = @core.Isometry2(
position.translation.add(result_translation),
position.rotation,
)
let down = vec2_neg(up)
let options = @collision.ShapeCastOptions(snap_dist, false).with_target_distance(
offset,
)
if query.cast_shape(bodies, colliders, current_pos, down, shape, options)
is Some(hit) {
result_translation = result_translation.add(
vec2_scale(down, hit.1.toi()),
)
grounded = true
}
}
}
let final_pos = @core.Isometry2(
position.translation.add(result_translation),
position.rotation,
)
let end_info = grounded_info(
query, bodies, colliders, shape, final_pos, up, prediction, offset,
)
if end_info.0 {
grounded = true
result_translation = result_translation.add(
vec2_scale(end_info.1, dt_value),
)
}
{
translation: result_translation,
grounded,
is_sliding_down_slope: sliding_down_slope,
}
}
///|
pub fn KinematicCharacterController::solve_character_collision_impulses(
self : KinematicCharacterController,
dt : @core.Real,
query : @collision.QueryPipeline,
bodies : @dynamics.RigidBodySet,
colliders : @collision.ColliderSet,
character_shape : @collision.Shape,
character_mass : @core.Real,
collisions : Array[CharacterCollision],
) -> Unit {
self |> ignore
character_shape |> ignore
if dt <= 0.0F {
return
}
// Simplified parity with Rapier: transfer a portion of the character's remaining movement
// along the collision normal as an impulse applied to the dynamic body hit by the shape-cast.
for i in 0.. ignore
}
}
}