diff --git a/common/src/character_controller/mod.rs b/common/src/character_controller/mod.rs index 3ba62a00..e3632ac2 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::{ @@ -147,8 +149,10 @@ fn get_ground_normal( // We found the ground, so return its normal. return Some(collision.normal); } - allowed_displacement - .apply_and_add_bound(VectorBound::new(collision.normal, collision.normal)); + allowed_displacement.apply_and_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; @@ -220,6 +224,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 { @@ -236,8 +243,16 @@ fn apply_velocity( - collision_result.displacement_vector.magnitude() / expected_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; @@ -255,18 +270,55 @@ 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 to ensure that slanted wall collisions do not lift the character off the ground. if is_ground(ctx, &collision.normal) { - bounded_vectors.apply_and_add_bound(VectorBound::new(collision.normal, ctx.up)); + let stay_on_ground_bounds = [VectorBound::new(collision.normal, ctx.up, false)]; + 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.apply_and_add_bound( + VectorBound::new(collision.normal, ctx.up, true), + &stay_on_ground_bounds, + ); + for bound in old_bounded_vectors.bounds() { + bounded_vectors.apply_and_add_bound(bound.clone(), &stay_on_ground_bounds); + } + + *ground_collision_handled = true; + } else { + bounded_vectors.apply_and_add_bound( + VectorBound::new(collision.normal, ctx.up, true), + &stay_on_ground_bounds, + ); + } *ground_normal = Some(collision.normal); } else { - bounded_vectors.apply_and_add_bound(VectorBound::new(collision.normal, collision.normal)); + let mut stay_on_ground_bounds = Vec::new(); + if let Some(ground_normal) = ground_normal { + stay_on_ground_bounds.push(VectorBound::new(*ground_normal, ctx.up, false)); + } + bounded_vectors.apply_and_add_bound( + VectorBound::new(collision.normal, collision.normal, true), + &stay_on_ground_bounds, + ); } } diff --git a/common/src/character_controller/vector_bounds.rs b/common/src/character_controller/vector_bounds.rs index a83f31ad..e17928cd 100644 --- a/common/src/character_controller/vector_bounds.rs +++ b/common/src/character_controller/vector_bounds.rs @@ -47,16 +47,25 @@ impl BoundedVectors { self.velocity.as_ref() } - /// Constrains `vector` with `new_bound` while keeping the existing constraints satisfied. - /// All projection transformations applied to `vector` are also applied + /// 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 and any constraints in + /// `temporary_bounds` satisfied. All projection transformations applied to `vector` are also applied /// to `tagalong` to allow two vectors to be transformed consistently with each other. - pub fn apply_and_add_bound(&mut self, new_bound: VectorBound) { - self.apply_bound(&new_bound); + pub fn apply_and_add_bound( + &mut self, + new_bound: VectorBound, + temporary_bounds: &[VectorBound], + ) { + self.apply_bound(&new_bound, temporary_bounds); self.bounds.push(new_bound); } /// Helper function to logically separate the "add" and the "apply" in `apply_and_add_bound` function. - fn apply_bound(&mut self, new_bound: &VectorBound) { + fn apply_bound(&mut self, new_bound: &VectorBound, temporary_bounds: &[VectorBound]) { // There likely isn't a perfect way to get a vector properly constrained with a list of bounds. The main // difficulty is finding which set of linearly independent bounds need to be applied so that all bounds are // satisfied. Since bounds are one-sided and not guaranteed to be linearly independent from each other, this @@ -65,6 +74,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(temporary_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 +87,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 { @@ -93,7 +105,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); @@ -117,6 +129,7 @@ impl BoundedVectors { pub struct VectorBound { normal: na::UnitVector3, projection_direction: na::UnitVector3, + front_facing: bool, // Only used for `check_vector` function } impl VectorBound { @@ -124,10 +137,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, } } @@ -153,10 +177,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 @@ -175,6 +203,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, }) } } @@ -190,35 +219,47 @@ mod tests { let mut bounded_vector = BoundedVectors::new(na::Vector3::new(-4.0, -3.0, 1.0), None); // Add a bunch of bounds that are achievable with nonzero vectors - bounded_vector.apply_and_add_bound(VectorBound::new( - unit_vector(1.0, 3.0, 4.0), - unit_vector(1.0, 2.0, 2.0), - )); + bounded_vector.apply_and_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()); assert_bounds_achieved(&bounded_vector); - bounded_vector.apply_and_add_bound(VectorBound::new( - unit_vector(2.0, -3.0, -4.0), - unit_vector(1.0, -2.0, -1.0), - )); + bounded_vector.apply_and_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()); assert_bounds_achieved(&bounded_vector); - bounded_vector.apply_and_add_bound(VectorBound::new( - unit_vector(2.0, -3.0, -5.0), - unit_vector(1.0, -2.0, -2.0), - )); + bounded_vector.apply_and_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()); assert_bounds_achieved(&bounded_vector); // Finally, add a bound that overconstrains the system - bounded_vector.apply_and_add_bound(VectorBound::new( - unit_vector(-3.0, 3.0, -2.0), - unit_vector(-3.0, 3.0, -2.0), - )); + bounded_vector.apply_and_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 assert_eq!(bounded_vector.displacement, na::Vector3::zero()); @@ -231,7 +272,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); @@ -257,8 +298,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; @@ -283,7 +324,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)); } }