Skip to content

Commit

Permalink
Add logic to prevent slanted walls from lifting the player up
Browse files Browse the repository at this point in the history
  • Loading branch information
patowen committed Oct 24, 2023
1 parent ccea05c commit 4b6996a
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 18 deletions.
61 changes: 55 additions & 6 deletions common/src/character_controller/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
mod collision;
mod vector_bounds;

use std::mem::replace;

use tracing::warn;

use crate::{
Expand Down Expand Up @@ -149,7 +151,11 @@ fn get_ground_normal(
// We found the ground, so return its normal.
return Some(collision.normal);
}
allowed_displacement.add_bound(VectorBound::new(collision.normal, collision.normal));
allowed_displacement.add_bound(VectorBound::new(
collision.normal,
collision.normal,
true,
));
} else {
// Return `None` if we travel the whole `allowed_displacement` and don't find the ground.
return None;
Expand Down Expand Up @@ -221,6 +227,9 @@ fn apply_velocity(
const MAX_COLLISION_ITERATIONS: u32 = 6;

let mut bounded_vectors = BoundedVectors::new(expected_displacement, Some(*velocity));
let mut bounded_vectors_without_collisions = bounded_vectors.clone();

let mut ground_collision_handled = false;

let mut all_collisions_resolved = false;
for _ in 0..MAX_COLLISION_ITERATIONS {
Expand All @@ -237,8 +246,16 @@ fn apply_velocity(
- collision_result.displacement_vector.magnitude()
/ bounded_vectors.displacement().magnitude();
bounded_vectors.scale_displacement(displacement_reduction_factor);

handle_collision(ctx, collision, &mut bounded_vectors, ground_normal);
bounded_vectors_without_collisions.scale_displacement(displacement_reduction_factor);

handle_collision(
ctx,
collision,
&bounded_vectors_without_collisions,
&mut bounded_vectors,
ground_normal,
&mut ground_collision_handled,
);
} else {
all_collisions_resolved = true;
break;
Expand All @@ -256,18 +273,50 @@ fn apply_velocity(
fn handle_collision(
ctx: &CharacterControllerContext,
collision: Collision,
bounded_vectors_without_collisions: &BoundedVectors,
bounded_vectors: &mut BoundedVectors,
ground_normal: &mut Option<na::UnitVector3<f32>>,
ground_collision_handled: &mut bool,
) {
// Collisions are divided into two categories: Ground collisions and wall collisions.
// Ground collisions will only affect vertical movement of the character, while wall collisions will
// push the character away from the wall in a perpendicular direction.
// push the character away from the wall in a perpendicular direction. If the character is on the ground,
// we have extra logic: Using a temporary bound to ensure that slanted wall collisions do not lift the
// character off the ground.
if is_ground(ctx, &collision.normal) {
bounded_vectors.add_bound(VectorBound::new(collision.normal, ctx.up));
if !*ground_collision_handled {
// Wall collisions can turn vertical momentum into unwanted horizontal momentum. This can
// occur if the character jumps at the corner between the ground and a slanted wall. If the wall
// collision is handled first, this horizontal momentum will push the character away from the wall.
// This can also occur if the character is on the ground and walks into a slanted wall. A single frame
// of downward momentum caused by gravity can turn into unwanted horizontal momentum that pushes
// the character away from the wall. Neither of these issues can occur if the ground collision is
// handled first, so when computing how the velocity vectors change, we rewrite history as if
// the ground collision was first. This is only necessary for the first ground collision, since
// afterwards, there is no more unexpected vertical momentum.
let old_bounded_vectors =
replace(bounded_vectors, bounded_vectors_without_collisions.clone());
bounded_vectors.add_temp_bound(VectorBound::new(collision.normal, ctx.up, false));
bounded_vectors.add_bound(VectorBound::new(collision.normal, ctx.up, true));
for bound in old_bounded_vectors.bounds() {
bounded_vectors.add_bound(bound.clone());
}
bounded_vectors.clear_temp_bounds();

*ground_collision_handled = true;
} else {
bounded_vectors.add_temp_bound(VectorBound::new(collision.normal, ctx.up, false));
bounded_vectors.add_bound(VectorBound::new(collision.normal, ctx.up, true));
bounded_vectors.clear_temp_bounds();
}

*ground_normal = Some(collision.normal);
} else {
bounded_vectors.add_bound(VectorBound::new(collision.normal, collision.normal));
if let Some(ground_normal) = ground_normal {
bounded_vectors.add_temp_bound(VectorBound::new(*ground_normal, ctx.up, false));
}
bounded_vectors.add_bound(VectorBound::new(collision.normal, collision.normal, true));
bounded_vectors.clear_temp_bounds();
}
}

Expand Down
68 changes: 56 additions & 12 deletions common/src/character_controller/vector_bounds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub struct BoundedVectors {
displacement: na::Vector3<f32>,
velocity: Option<na::Vector3<f32>>,
bounds: Vec<VectorBound>,
temp_bounds: Vec<VectorBound>,
error_margin: f32,
}

Expand All @@ -29,6 +30,7 @@ impl BoundedVectors {
displacement,
velocity,
bounds: vec![],
temp_bounds: vec![],
error_margin,
}
}
Expand All @@ -47,6 +49,11 @@ impl BoundedVectors {
self.velocity.as_ref()
}

/// Returns the internal list of `VectorBound`s contained in the `BoundedVectors` struct.
pub fn bounds(&self) -> &[VectorBound] {
&self.bounds
}

/// Constrains `vector` with `new_bound` while keeping the existing constraints satisfied. All projection
/// transformations applied to `vector` are also applied to `tagalong` to allow two vectors to be transformed consistently
/// with each other.
Expand All @@ -55,6 +62,19 @@ impl BoundedVectors {
self.bounds.push(new_bound);
}

/// Temporarily constrains `vector` with `new_bound` while keeping the existing constraints satisfied. All projection
/// transformations applied to `vector` are also applied to `tagalong` to allow two vectors to be transformed consistently
/// with each other. Use `clear_temporary_bounds` to get rid of any existing temporary bounds
pub fn add_temp_bound(&mut self, new_bound: VectorBound) {
self.apply_bound(&new_bound);
self.temp_bounds.push(new_bound);
}

/// Removes all temporary bounds
pub fn clear_temp_bounds(&mut self) {
self.temp_bounds.clear();
}

/// Helper function to apply a new bound without adding it to any lists.
fn apply_bound(&mut self, new_bound: &VectorBound) {
// There likely isn't a perfect way to get a vector properly constrained with a list of bounds. The main
Expand All @@ -65,6 +85,9 @@ impl BoundedVectors {
// bound that allows all bounds to be satisfied, and (3) zero out the vector if no such pairing works, as we
// assume that we need to apply three linearly independent bounds.

// Combine existing bounds with temporary bounds into an iterator
let bounds_iter = self.bounds.iter().chain(self.temp_bounds.iter());

// Apply new_bound if necessary.
if !new_bound.check_vector(&self.displacement, self.error_margin) {
new_bound.constrain_vector(&mut self.displacement, self.error_margin);
Expand All @@ -75,14 +98,14 @@ impl BoundedVectors {
}

// Check if all constraints are satisfied
if (self.bounds.iter()).all(|b| b.check_vector(&self.displacement, self.error_margin)) {
if (bounds_iter.clone()).all(|b| b.check_vector(&self.displacement, self.error_margin)) {
return;
}

// If not all constraints are satisfied, find the first constraint that if applied will satisfy
// the remaining constriants
for bound in
(self.bounds.iter()).filter(|b| !b.check_vector(&self.displacement, self.error_margin))
(bounds_iter.clone()).filter(|b| !b.check_vector(&self.displacement, self.error_margin))
{
let Some(ortho_bound) = bound.get_self_constrained_with_bound(new_bound) else {
warn!("Unsatisfied existing bound is parallel to new bound. Is the character squeezed between two walls?");
Expand All @@ -92,7 +115,7 @@ impl BoundedVectors {
let mut candidate = self.displacement;
ortho_bound.constrain_vector(&mut candidate, self.error_margin);

if (self.bounds.iter()).all(|b| b.check_vector(&candidate, self.error_margin)) {
if (bounds_iter.clone()).all(|b| b.check_vector(&candidate, self.error_margin)) {
self.displacement = candidate;
if let Some(ref mut velocity) = self.velocity {
ortho_bound.constrain_vector(velocity, 0.0);
Expand All @@ -116,17 +139,29 @@ impl BoundedVectors {
pub struct VectorBound {
normal: na::UnitVector3<f32>,
projection_direction: na::UnitVector3<f32>,
front_facing: bool, // Only used for `check_vector` function
}

impl VectorBound {
/// Creates a `VectorBound` that pushes vectors away from the plane given
/// by the normal in `projection_direction`. After applying such a bound to
/// a vector, its dot product with `normal` should be close to zero but positive
/// even considering floating point error.
pub fn new(normal: na::UnitVector3<f32>, projection_direction: na::UnitVector3<f32>) -> Self {
///
/// The `VectorBound` will only push vectors that do not currently fulfill the bounds.
/// If `front_facing` is true, the bound wants the vector to be "in front" of the plane,
/// in the direction given by `normal`. Otherwise, the bound wants the vector to be "behind"
/// the plane. Error margins are set so that two planes, one front_facing and one not, with the
/// same `normal` and `projection_direction`, can both act on a vector without interfering.
pub fn new(
normal: na::UnitVector3<f32>,
projection_direction: na::UnitVector3<f32>,
front_facing: bool,
) -> Self {
VectorBound {
normal,
projection_direction,
front_facing,
}
}

Expand All @@ -152,10 +187,14 @@ impl VectorBound {
// An additional margin of error is needed when the bound is checked to ensure that an
// applied bound always passes the check. Ostensibly, for an applied bound, the dot
// product is equal to the error margin.

// Using 0.5 here should ensure that the check will pass after the bound is applied, and it will fail if the
// dot product is too close to zero to guarantee that it won't be treated as negative during collision checking
subject.dot(&self.normal) >= error_margin * 0.5
if self.front_facing {
// Using 0.5 here should ensure that the check will pass after the bound is applied, and it will fail if the
// dot product is too close to zero to guarantee that it won't be treated as negative during collision checking
subject.dot(&self.normal) >= error_margin * 0.5
} else {
// Using 1.5 here keeps the additional margin of error equivalent in magnitude to the front-facing case
subject.dot(&self.normal) <= error_margin * 1.5
}
}

/// Returns a `VectorBound` that is an altered version of `self` so that it no longer interferes
Expand All @@ -174,6 +213,7 @@ impl VectorBound {
na::UnitVector3::try_new(ortho_bound_projection_direction, 1e-5).map(|d| VectorBound {
normal: self.normal,
projection_direction: d,
front_facing: self.front_facing,
})
}
}
Expand All @@ -192,6 +232,7 @@ mod tests {
bounded_vector.add_bound(VectorBound::new(
unit_vector(1.0, 3.0, 4.0),
unit_vector(1.0, 2.0, 2.0),
true,
));

assert_ne!(bounded_vector.displacement, na::Vector3::zero());
Expand All @@ -200,6 +241,7 @@ mod tests {
bounded_vector.add_bound(VectorBound::new(
unit_vector(2.0, -3.0, -4.0),
unit_vector(1.0, -2.0, -1.0),
true,
));

assert_ne!(bounded_vector.displacement, na::Vector3::zero());
Expand All @@ -208,6 +250,7 @@ mod tests {
bounded_vector.add_bound(VectorBound::new(
unit_vector(2.0, -3.0, -5.0),
unit_vector(1.0, -2.0, -2.0),
true,
));

assert_ne!(bounded_vector.displacement, na::Vector3::zero());
Expand All @@ -217,6 +260,7 @@ mod tests {
bounded_vector.add_bound(VectorBound::new(
unit_vector(-3.0, 3.0, -2.0),
unit_vector(-3.0, 3.0, -2.0),
true,
));

// Using assert_eq instead of assert_ne here
Expand All @@ -230,7 +274,7 @@ mod tests {
let normal = unit_vector(1.0, 3.0, 4.0);
let projection_direction = unit_vector(1.0, 2.0, 2.0);
let error_margin = 1e-4;
let bound = VectorBound::new(normal, projection_direction);
let bound = VectorBound::new(normal, projection_direction, true);

let initial_vector = na::Vector3::new(-4.0, -3.0, 1.0);

Expand All @@ -256,8 +300,8 @@ mod tests {
let normal1 = unit_vector(1.0, -4.0, 3.0);
let projection_direction1 = unit_vector(1.0, -2.0, 1.0);

let bound0 = VectorBound::new(normal0, projection_direction0);
let bound1 = VectorBound::new(normal1, projection_direction1);
let bound0 = VectorBound::new(normal0, projection_direction0, true);
let bound1 = VectorBound::new(normal1, projection_direction1, true);

let initial_vector = na::Vector3::new(2.0, -1.0, -3.0);
let mut constrained_vector = initial_vector;
Expand All @@ -282,7 +326,7 @@ mod tests {
}

fn assert_bounds_achieved(bounds: &BoundedVectors) {
for bound in &bounds.bounds {
for bound in bounds.bounds() {
assert!(bound.check_vector(&bounds.displacement, bounds.error_margin));
}
}
Expand Down

0 comments on commit 4b6996a

Please sign in to comment.