pub use super::velocity_project::*;
use crate::{collision::collider::contact_query::contact_manifolds, prelude::*};
use bevy::{ecs::system::SystemParam, prelude::*};
use core::time::Duration;
const DOT_EPSILON: Scalar = 0.005;
#[allow(clippy::excessive_precision)]
pub const COS_5_DEGREES: Scalar = 0.99619469809;
#[derive(SystemParam)]
#[doc(alias = "CollideAndSlide")]
#[doc(alias = "StepSlide")]
pub struct MoveAndSlide<'w, 's> {
pub spatial_query: SpatialQuery<'w, 's>,
pub colliders: Query<
'w,
's,
(
&'static Collider,
&'static Position,
&'static Rotation,
Option<&'static CollisionLayers>,
),
(With<ColliderOf>, Without<Sensor>),
>,
pub length_unit: Res<'w, PhysicsLengthUnit>,
}
#[derive(Clone, Debug, PartialEq, Reflect)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
#[reflect(Debug, PartialEq)]
pub struct MoveAndSlideConfig {
pub move_and_slide_iterations: usize,
pub depenetration_iterations: usize,
pub max_depenetration_error: Scalar,
pub penetration_rejection_threshold: Scalar,
pub skin_width: Scalar,
pub planes: Vec<Dir>,
pub plane_similarity_dot_threshold: Scalar,
pub max_planes: usize,
}
impl Default for MoveAndSlideConfig {
fn default() -> Self {
let default_depen_cfg = DepenetrationConfig::default();
Self {
move_and_slide_iterations: 4,
depenetration_iterations: default_depen_cfg.depenetration_iterations,
max_depenetration_error: default_depen_cfg.max_depenetration_error,
penetration_rejection_threshold: default_depen_cfg.penetration_rejection_threshold,
skin_width: default_depen_cfg.skin_width,
planes: Vec::new(),
plane_similarity_dot_threshold: COS_5_DEGREES,
max_planes: 20,
}
}
}
#[derive(Clone, Debug, PartialEq, Reflect)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
#[reflect(Debug, PartialEq)]
pub struct DepenetrationConfig {
pub depenetration_iterations: usize,
pub max_depenetration_error: Scalar,
pub penetration_rejection_threshold: Scalar,
pub skin_width: Scalar,
}
impl Default for DepenetrationConfig {
fn default() -> Self {
Self {
depenetration_iterations: 16,
max_depenetration_error: 0.0001,
penetration_rejection_threshold: 0.5,
skin_width: 0.01,
}
}
}
impl From<&MoveAndSlideConfig> for DepenetrationConfig {
fn from(config: &MoveAndSlideConfig) -> Self {
Self {
depenetration_iterations: config.depenetration_iterations,
max_depenetration_error: config.max_depenetration_error,
penetration_rejection_threshold: config.penetration_rejection_threshold,
skin_width: config.skin_width,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Reflect)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
#[reflect(Debug, PartialEq)]
pub struct MoveAndSlideOutput {
pub position: Vector,
pub projected_velocity: Vector,
}
#[derive(Debug, PartialEq)]
pub struct MoveAndSlideHitData<'a> {
pub entity: Entity,
pub distance: Scalar,
pub point: Vector,
pub normal: &'a mut Dir,
pub position: &'a mut Vector,
pub velocity: &'a mut Vector,
#[doc(alias = "time_of_impact")]
pub collision_distance: Scalar,
}
impl<'a> MoveAndSlideHitData<'a> {
pub fn intersects(&self) -> bool {
self.collision_distance == 0.0
}
}
#[derive(Debug, PartialEq)]
pub enum MoveAndSlideHitResponse {
Accept,
Ignore,
Abort,
}
#[derive(Clone, Copy, Debug, PartialEq, Reflect)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
#[reflect(Debug, PartialEq)]
pub struct MoveHitData {
pub entity: Entity,
#[doc(alias = "time_of_impact")]
pub distance: Scalar,
pub point1: Vector,
pub point2: Vector,
pub normal1: Vector,
pub normal2: Vector,
#[doc(alias = "time_of_impact")]
pub collision_distance: Scalar,
}
impl MoveHitData {
pub fn intersects(self) -> bool {
self.collision_distance == 0.0
}
}
impl<'w, 's> MoveAndSlide<'w, 's> {
#[cfg_attr(
feature = "2d",
doc = "use avian2d::{prelude::*, math::{Vector, AdjustPrecision as _, AsF32 as _}};"
)]
#[cfg_attr(
feature = "3d",
doc = "use avian3d::{prelude::*, math::{Vector, AdjustPrecision as _, AsF32 as _}};"
)]
#[cfg_attr(
feature = "2d",
doc = " transform.translation.xy().adjust_precision(),"
)]
#[cfg_attr(
feature = "3d",
doc = " transform.translation.adjust_precision(),"
)]
#[cfg_attr(
feature = "2d",
doc = " transform.rotation.to_euler(EulerRot::XYZ).2.adjust_precision(),"
)]
#[cfg_attr(
feature = "3d",
doc = " transform.rotation.adjust_precision(),"
)]
#[cfg_attr(
feature = "2d",
doc = " transform.translation = out.position.f32().extend(0.0);"
)]
#[cfg_attr(
feature = "3d",
doc = " transform.translation = out.position.f32();"
)]
#[must_use]
#[doc(alias = "collide_and_slide")]
#[doc(alias = "step_slide")]
pub fn move_and_slide(
&self,
shape: &Collider,
shape_position: Vector,
shape_rotation: RotationValue,
mut velocity: Vector,
delta_time: Duration,
config: &MoveAndSlideConfig,
filter: &SpatialQueryFilter,
mut on_hit: impl FnMut(MoveAndSlideHitData) -> MoveAndSlideHitResponse,
) -> MoveAndSlideOutput {
let mut position = shape_position;
let mut time_left = {
#[cfg(feature = "f32")]
{
delta_time.as_secs_f32()
}
#[cfg(feature = "f64")]
{
delta_time.as_secs_f64()
}
};
let skin_width = self.length_unit.0 * config.skin_width;
let depenetration_offset =
self.depenetrate(shape, position, shape_rotation, &config.into(), filter);
position += depenetration_offset;
for _ in 0..config.move_and_slide_iterations {
let sweep = time_left * velocity;
let Some((vel_dir, distance)) = Dir::new_and_length(sweep.f32()).ok() else {
break;
};
let distance = distance.adjust_precision();
const MIN_DISTANCE: Scalar = 1e-4;
if distance < MIN_DISTANCE {
break;
}
let Some(sweep_hit) =
self.cast_move(shape, position, shape_rotation, sweep, skin_width, filter)
else {
position += sweep;
break;
};
let point = sweep_hit.point2;
time_left -= time_left * (sweep_hit.distance / distance);
position += vel_dir.adjust_precision() * sweep_hit.distance;
let mut planes: Vec<Dir> = config.planes.clone();
let mut first_normal = Dir::new_unchecked(sweep_hit.normal1.f32());
let hit_response = on_hit(MoveAndSlideHitData {
entity: sweep_hit.entity,
point,
normal: &mut first_normal,
collision_distance: sweep_hit.collision_distance,
distance: sweep_hit.distance,
position: &mut position,
velocity: &mut velocity,
});
if hit_response == MoveAndSlideHitResponse::Accept {
planes.push(first_normal);
} else if hit_response == MoveAndSlideHitResponse::Abort {
break;
}
let mut aborted = false;
self.intersections(
shape,
position,
shape_rotation,
skin_width * 2.0,
filter,
|contact_point, mut normal| {
for existing_normal in planes.iter_mut() {
if normal.dot(**existing_normal) as Scalar
>= config.plane_similarity_dot_threshold
{
let n_dot_v = normal.adjust_precision().dot(velocity);
let existing_n_dot_v = existing_normal.adjust_precision().dot(velocity);
if n_dot_v < existing_n_dot_v {
*existing_normal = normal;
}
return true;
}
}
if planes.len() >= config.max_planes {
return false;
}
let hit_response = on_hit(MoveAndSlideHitData {
entity: sweep_hit.entity,
point: contact_point.point,
normal: &mut normal,
collision_distance: sweep_hit.collision_distance,
distance: sweep_hit.distance,
position: &mut position,
velocity: &mut velocity,
});
match hit_response {
MoveAndSlideHitResponse::Accept => {
planes.push(normal);
true
}
MoveAndSlideHitResponse::Ignore => true,
MoveAndSlideHitResponse::Abort => {
aborted = true;
false
}
}
},
);
velocity = Self::project_velocity(velocity, &planes);
if aborted {
break;
}
}
let depenetration_offset =
self.depenetrate(shape, position, shape_rotation, &config.into(), filter);
position += depenetration_offset;
MoveAndSlideOutput {
position,
projected_velocity: velocity,
}
}
#[cfg_attr(
feature = "2d",
doc = "use avian2d::{prelude::*, math::{Vector, Dir, AdjustPrecision as _, AsF32 as _}};"
)]
#[cfg_attr(
feature = "3d",
doc = "use avian3d::{prelude::*, math::{Vector, Dir, AdjustPrecision as _, AsF32 as _}};"
)]
#[cfg_attr(
feature = "2d",
doc = " transform.translation.xy().adjust_precision(),"
)]
#[cfg_attr(
feature = "3d",
doc = " transform.translation.adjust_precision(),"
)]
#[cfg_attr(
feature = "2d",
doc = " transform.rotation.to_euler(EulerRot::XYZ).2.adjust_precision(),"
)]
#[cfg_attr(
feature = "3d",
doc = " transform.rotation.adjust_precision(),"
)]
#[cfg_attr(
feature = "2d",
doc = " transform.translation += offset.f32().extend(0.0);"
)]
#[cfg_attr(feature = "3d", doc = " transform.translation += offset.f32();")]
#[cfg_attr(
feature = "2d",
doc = " transform.translation.xy().adjust_precision(),"
)]
#[cfg_attr(
feature = "3d",
doc = " transform.translation.adjust_precision(),"
)]
#[cfg_attr(
feature = "2d",
doc = " transform.rotation.to_euler(EulerRot::XYZ).2.adjust_precision(),"
)]
#[cfg_attr(
feature = "3d",
doc = " transform.rotation.adjust_precision(),"
)]
#[cfg_attr(
feature = "2d",
doc = " transform.translation += (velocity.normalize_or_zero() * hit.distance).extend(0.0).f32();"
)]
#[cfg_attr(
feature = "3d",
doc = " transform.translation += (velocity.normalize_or_zero() * hit.distance).f32();"
)]
#[cfg_attr(
feature = "2d",
doc = " transform.translation += velocity.extend(0.0).f32();"
)]
#[cfg_attr(
feature = "3d",
doc = " transform.translation += velocity.f32();"
)]
#[must_use]
#[doc(alias = "sweep")]
pub fn cast_move(
&self,
shape: &Collider,
shape_position: Vector,
shape_rotation: RotationValue,
movement: Vector,
skin_width: Scalar,
filter: &SpatialQueryFilter,
) -> Option<MoveHitData> {
let (direction, distance) = Dir::new_and_length(movement.f32()).unwrap_or((Dir::X, 0.0));
let distance = distance.adjust_precision() + skin_width;
let shape_hit = self.spatial_query.cast_shape_predicate(
shape,
shape_position,
shape_rotation,
direction,
&ShapeCastConfig {
ignore_origin_penetration: true,
..ShapeCastConfig::from_max_distance(distance)
},
filter,
&|entity| self.colliders.contains(entity),
)?;
let safe_distance = if distance == 0.0 {
0.0
} else {
Self::pull_back(shape_hit, direction, skin_width)
};
Some(MoveHitData {
distance: safe_distance,
collision_distance: distance,
entity: shape_hit.entity,
point1: shape_hit.point1,
point2: shape_hit.point2,
normal1: shape_hit.normal1,
normal2: shape_hit.normal2,
})
}
#[must_use]
fn pull_back(hit: ShapeHitData, dir: Dir, skin_width: Scalar) -> Scalar {
let dot = dir.adjust_precision().dot(-hit.normal1).max(DOT_EPSILON);
let skin_distance = skin_width / dot;
(hit.distance - skin_distance).max(0.0)
}
#[cfg_attr(
feature = "2d",
doc = "use avian2d::{prelude::*, character_controller::move_and_slide::DepenetrationConfig, math::{AdjustPrecision as _, AsF32 as _}};"
)]
#[cfg_attr(
feature = "3d",
doc = "use avian3d::{prelude::*, character_controller::move_and_slide::DepenetrationConfig, math::{AdjustPrecision as _, AsF32 as _}};"
)]
#[cfg_attr(
feature = "2d",
doc = " transform.translation.xy().adjust_precision(),"
)]
#[cfg_attr(
feature = "3d",
doc = " transform.translation.adjust_precision(),"
)]
#[cfg_attr(
feature = "2d",
doc = " transform.rotation.to_euler(EulerRot::XYZ).2.adjust_precision(),"
)]
#[cfg_attr(
feature = "3d",
doc = " transform.rotation.adjust_precision(),"
)]
#[cfg_attr(
feature = "2d",
doc = " transform.translation += offset.f32().extend(0.0);"
)]
#[cfg_attr(feature = "3d", doc = " transform.translation += offset.f32();")]
pub fn depenetrate(
&self,
shape: &Collider,
shape_position: Vector,
shape_rotation: RotationValue,
config: &DepenetrationConfig,
filter: &SpatialQueryFilter,
) -> Vector {
if config.depenetration_iterations == 0 {
return Vector::ZERO;
}
let mut intersections = Vec::new();
self.intersections(
shape,
shape_position,
shape_rotation,
self.length_unit.0 * config.skin_width,
filter,
|contact_point, normal| {
intersections.push((
normal,
contact_point.penetration + self.length_unit.0 * config.skin_width,
));
true
},
);
self.depenetrate_intersections(config, &intersections)
}
#[cfg_attr(
feature = "2d",
doc = "use avian2d::{prelude::*, character_controller::move_and_slide::DepenetrationConfig, math::{AdjustPrecision as _, AsF32 as _}};"
)]
#[cfg_attr(
feature = "3d",
doc = "use avian3d::{prelude::*, character_controller::move_and_slide::DepenetrationConfig, math::{AdjustPrecision as _, AsF32 as _}};"
)]
#[cfg_attr(
feature = "2d",
doc = " transform.translation.xy().adjust_precision(),"
)]
#[cfg_attr(
feature = "3d",
doc = " transform.translation.adjust_precision(),"
)]
#[cfg_attr(
feature = "2d",
doc = " transform.rotation.to_euler(EulerRot::XYZ).2.adjust_precision(),"
)]
#[cfg_attr(
feature = "3d",
doc = " transform.rotation.adjust_precision(),"
)]
#[cfg_attr(
feature = "2d",
doc = " transform.translation += offset.f32().extend(0.0);"
)]
#[cfg_attr(feature = "3d", doc = " transform.translation += offset.f32();")]
#[must_use]
pub fn depenetrate_intersections(
&self,
config: &DepenetrationConfig,
intersections: &[(Dir, Scalar)],
) -> Vector {
let mut fixup = Vector::ZERO;
for _ in 0..config.depenetration_iterations {
let mut total_error = 0.0;
for (normal, dist) in intersections {
if *dist > self.length_unit.0 * config.penetration_rejection_threshold {
continue;
}
let normal = normal.adjust_precision();
let error = (dist - fixup.dot(normal)).max(0.0);
total_error += error;
fixup += error * normal;
}
if total_error < self.length_unit.0 * config.max_depenetration_error {
break;
}
}
fixup
}
pub fn intersections(
&self,
shape: &Collider,
shape_position: Vector,
shape_rotation: RotationValue,
prediction_distance: Scalar,
filter: &SpatialQueryFilter,
mut callback: impl FnMut(&ContactPoint, Dir) -> bool,
) {
let expanded_aabb = shape
.aabb(shape_position, shape_rotation)
.grow(Vector::splat(prediction_distance));
let aabb_intersections = self
.spatial_query
.aabb_intersections_with_aabb(expanded_aabb);
'outer: for intersection_entity in aabb_intersections {
let Ok((intersection_collider, intersection_pos, intersection_rot, layers)) =
self.colliders.get(intersection_entity)
else {
continue;
};
let layers = layers.copied().unwrap_or_default();
if !filter.test(intersection_entity, layers) {
continue;
}
let mut manifolds = Vec::new();
contact_manifolds(
shape,
shape_position,
shape_rotation,
intersection_collider,
*intersection_pos,
*intersection_rot,
prediction_distance,
&mut manifolds,
);
for manifold in manifolds {
let Some(deepest) = manifold.find_deepest_contact() else {
continue;
};
let normal = Dir::new_unchecked(-manifold.normal.f32());
if !callback(deepest, normal) {
break 'outer;
}
}
}
}
#[must_use]
pub fn project_velocity(v: Vector, normals: &[Dir]) -> Vector {
project_velocity(v, normals)
}
}