From 65794f19fc3bddc5e8955eb58733057b6e2f3a31 Mon Sep 17 00:00:00 2001 From: Patrick Owen Date: Sun, 21 May 2023 12:48:51 -0400 Subject: [PATCH] Add logic to prevent slanted walls from lifting the player up --- common/src/character_controller/mod.rs | 61 +++++++++++++++-- .../src/character_controller/vector_bounds.rs | 68 +++++++++++++++---- 2 files changed, 111 insertions(+), 18 deletions(-) diff --git a/common/src/character_controller/mod.rs b/common/src/character_controller/mod.rs index 93a898c7..498b3d3b 100644 --- a/common/src/character_controller/mod.rs +++ b/common/src/character_controller/mod.rs @@ -1,6 +1,8 @@ mod collision; mod vector_bounds; +use std::mem::replace; + use tracing::warn; use crate::{ @@ -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; @@ -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 { @@ -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; @@ -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>, + 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(); } } diff --git a/common/src/character_controller/vector_bounds.rs b/common/src/character_controller/vector_bounds.rs index 97b25949..a9896f5b 100644 --- a/common/src/character_controller/vector_bounds.rs +++ b/common/src/character_controller/vector_bounds.rs @@ -12,6 +12,7 @@ pub struct BoundedVectors { displacement: na::Vector3, velocity: Option>, bounds: Vec, + temp_bounds: Vec, error_margin: f32, } @@ -29,6 +30,7 @@ impl BoundedVectors { displacement, velocity, bounds: vec![], + temp_bounds: vec![], error_margin, } } @@ -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. @@ -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 @@ -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); @@ -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?"); @@ -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); @@ -116,6 +139,7 @@ impl BoundedVectors { pub struct VectorBound { normal: na::UnitVector3, projection_direction: na::UnitVector3, + front_facing: bool, // Only used for `check_vector` function } impl VectorBound { @@ -123,10 +147,21 @@ impl VectorBound { /// 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, projection_direction: na::UnitVector3) -> 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, + projection_direction: na::UnitVector3, + front_facing: bool, + ) -> Self { VectorBound { normal, projection_direction, + front_facing, } } @@ -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 @@ -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, }) } } @@ -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()); @@ -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()); @@ -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()); @@ -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 @@ -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); @@ -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; @@ -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)); } }