From ccea05cda0740fcf89f1618fba3b56829c75e3d1 Mon Sep 17 00:00:00 2001 From: Patrick Owen Date: Sun, 21 May 2023 12:45:56 -0400 Subject: [PATCH] Add character controller physics --- client/src/graphics/window.rs | 10 ++ client/src/local_character_controller.rs | 25 +++- client/src/prediction.rs | 15 ++- client/src/sim.rs | 42 +++++- common/src/character_controller/mod.rs | 162 +++++++++++++++++++++-- common/src/proto.rs | 2 + common/src/sim_config.rs | 29 ++++ server/src/sim.rs | 8 +- 8 files changed, 270 insertions(+), 23 deletions(-) diff --git a/client/src/graphics/window.rs b/client/src/graphics/window.rs index 4c1064c4..55e461b1 100644 --- a/client/src/graphics/window.rs +++ b/client/src/graphics/window.rs @@ -120,6 +120,7 @@ impl Window { let mut right = false; let mut up = false; let mut down = false; + let mut jump = false; let mut clockwise = false; let mut anticlockwise = false; let mut last_frame = Instant::now(); @@ -141,6 +142,7 @@ impl Window { up as u8 as f32 - down as u8 as f32, back as u8 as f32 - forward as u8 as f32, )); + sim.set_jump_held(jump); sim.look( 0.0, @@ -225,6 +227,14 @@ impl Window { VirtualKeyCode::F => { down = state == ElementState::Pressed; } + VirtualKeyCode::Space => { + if let Some(sim) = self.sim.as_mut() { + if !jump && state == ElementState::Pressed { + sim.set_jump_pressed_true(); + } + jump = state == ElementState::Pressed; + } + } VirtualKeyCode::V if state == ElementState::Pressed => { if let Some(sim) = self.sim.as_mut() { sim.toggle_no_clip(); diff --git a/client/src/local_character_controller.rs b/client/src/local_character_controller.rs index a8b8c16b..a4197d7a 100644 --- a/client/src/local_character_controller.rs +++ b/client/src/local_character_controller.rs @@ -122,6 +122,24 @@ impl LocalCharacterController { } } + /// Returns an orientation quaternion that is as faithful as possible to the current orientation quaternion + /// while being restricted to ensuring the view is level and does not look up or down. This function's main + /// purpose is to figure out what direction the character should go when a movement key is pressed. + pub fn horizontal_orientation(&mut self) -> na::UnitQuaternion { + // Get orientation-relative up + let up = self.orientation.inverse() * self.up; + + let forward = if up.x.abs() < 0.9 { + // Rotate the local forward vector about the locally horizontal axis until it is horizontal + na::Vector3::new(0.0, -up.z, up.y) + } else { + // Project the local forward vector to the level plane + na::Vector3::z() - up.into_inner() * up.z + }; + + self.orientation * na::UnitQuaternion::face_towards(&forward, &up) + } + pub fn renormalize_orientation(&mut self) { self.orientation.renormalize_fast(); } @@ -156,7 +174,7 @@ mod tests { } #[test] - fn look_level_examples() { + fn look_level_and_horizontal_orientation_examples() { let mut subject = LocalCharacterController::new(); // Pick an arbitrary orientation @@ -172,6 +190,7 @@ mod tests { // Sanity check that the setup makes sense assert_aligned_to_gravity(&subject); assert_yaw_and_pitch_correct(base_orientation, yaw, pitch, subject.orientation); + assert_yaw_and_pitch_correct(base_orientation, yaw, 0.0, subject.horizontal_orientation()); // Standard look_level expression subject.look_level(0.5, -0.4); @@ -179,6 +198,7 @@ mod tests { pitch -= 0.4; assert_aligned_to_gravity(&subject); assert_yaw_and_pitch_correct(base_orientation, yaw, pitch, subject.orientation); + assert_yaw_and_pitch_correct(base_orientation, yaw, 0.0, subject.horizontal_orientation()); // Look up past the cap subject.look_level(-0.2, 3.0); @@ -186,6 +206,7 @@ mod tests { pitch = std::f32::consts::FRAC_PI_2; assert_aligned_to_gravity(&subject); assert_yaw_and_pitch_correct(base_orientation, yaw, pitch, subject.orientation); + assert_yaw_and_pitch_correct(base_orientation, yaw, 0.0, subject.horizontal_orientation()); // Look down past the cap subject.look_level(6.2, -7.2); @@ -193,6 +214,7 @@ mod tests { pitch = -std::f32::consts::FRAC_PI_2; assert_aligned_to_gravity(&subject); assert_yaw_and_pitch_correct(base_orientation, yaw, pitch, subject.orientation); + assert_yaw_and_pitch_correct(base_orientation, yaw, 0.0, subject.horizontal_orientation()); // Go back to a less unusual orientation subject.look_level(-1.2, 0.8); @@ -200,6 +222,7 @@ mod tests { pitch += 0.8; assert_aligned_to_gravity(&subject); assert_yaw_and_pitch_correct(base_orientation, yaw, pitch, subject.orientation); + assert_yaw_and_pitch_correct(base_orientation, yaw, 0.0, subject.horizontal_orientation()); } #[test] diff --git a/client/src/prediction.rs b/client/src/prediction.rs index a5011f7b..87ddcdcd 100644 --- a/client/src/prediction.rs +++ b/client/src/prediction.rs @@ -19,6 +19,7 @@ pub struct PredictedMotion { generation: u16, predicted_position: Position, predicted_velocity: na::Vector3, + predicted_on_ground: bool, } impl PredictedMotion { @@ -28,6 +29,7 @@ impl PredictedMotion { generation: 0, predicted_position: initial_position, predicted_velocity: na::Vector3::zeros(), + predicted_on_ground: false, } } @@ -39,6 +41,7 @@ impl PredictedMotion { graph, &mut self.predicted_position, &mut self.predicted_velocity, + &mut self.predicted_on_ground, input, cfg.step_interval.as_secs_f32(), ); @@ -55,6 +58,7 @@ impl PredictedMotion { generation: u16, position: Position, velocity: na::Vector3, + on_ground: bool, ) { let first_gen = self.generation.wrapping_sub(self.log.len() as u16); let obsolete = usize::from(generation.wrapping_sub(first_gen)); @@ -65,6 +69,7 @@ impl PredictedMotion { self.log.drain(..obsolete); self.predicted_position = position; self.predicted_velocity = velocity; + self.predicted_on_ground = on_ground; for input in self.log.iter() { character_controller::run_character_step( @@ -72,6 +77,7 @@ impl PredictedMotion { graph, &mut self.predicted_position, &mut self.predicted_velocity, + &mut self.predicted_on_ground, input, cfg.step_interval.as_secs_f32(), ); @@ -86,6 +92,10 @@ impl PredictedMotion { pub fn predicted_velocity(&self) -> &na::Vector3 { &self.predicted_velocity } + + pub fn predicted_on_ground(&self) -> &bool { + &self.predicted_on_ground + } } #[cfg(test)] @@ -103,9 +113,11 @@ mod tests { #[test] fn wraparound() { let mock_cfg = SimConfig::from_raw(&common::SimConfigRaw::default()); - let mock_graph = DualGraph::new(); + let mut mock_graph = DualGraph::new(); + common::node::populate_fresh_nodes(&mut mock_graph); let mock_character_input = CharacterInput { movement: na::Vector3::x(), + jump: false, no_clip: true, }; @@ -121,6 +133,7 @@ mod tests { generation, pos(), na::Vector3::zeros(), + false, ) }; diff --git a/client/src/sim.rs b/client/src/sim.rs index 4dc5cc30..9dae2f2b 100644 --- a/client/src/sim.rs +++ b/client/src/sim.rs @@ -41,6 +41,12 @@ pub struct Sim { no_clip: bool, /// Whether no_clip will be toggled next step toggle_no_clip: bool, + /// Whether the current step starts with a jump + is_jumping: bool, + /// Whether the jump button has been pressed since the last step + jump_pressed: bool, + /// Whether the jump button is currently held down + jump_held: bool, prediction: PredictedMotion, local_character_controller: LocalCharacterController, } @@ -64,6 +70,9 @@ impl Sim { average_movement_input: na::zero(), no_clip: true, toggle_no_clip: false, + is_jumping: false, + jump_pressed: false, + jump_held: false, prediction: PredictedMotion::new(proto::Position { node: NodeId::ROOT, local: na::one(), @@ -102,6 +111,15 @@ impl Sim { self.toggle_no_clip = true; } + pub fn set_jump_held(&mut self, jump_held: bool) { + self.jump_held = jump_held; + self.jump_pressed = jump_held || self.jump_pressed; + } + + pub fn set_jump_pressed_true(&mut self) { + self.jump_pressed = true; + } + pub fn cfg(&self) -> &SimConfig { &self.cfg } @@ -130,6 +148,9 @@ impl Sim { self.toggle_no_clip = false; } + self.is_jumping = self.jump_held || self.jump_pressed; + self.jump_pressed = false; + // Reset state for the next step if overflow > step_interval { // If it's been more than two timesteps since we last sent input, skip ahead @@ -233,6 +254,7 @@ impl Sim { latest_input, *pos, ch.state.velocity, + ch.state.on_ground, ); } @@ -292,10 +314,14 @@ impl Sim { } fn send_input(&mut self, net: &mut Net) { + let orientation = if self.no_clip { + self.local_character_controller.orientation() + } else { + self.local_character_controller.horizontal_orientation() + }; let character_input = CharacterInput { - movement: sanitize_motion_input( - self.local_character_controller.orientation() * self.average_movement_input, - ), + movement: sanitize_motion_input(orientation * self.average_movement_input), + jump: self.is_jumping, no_clip: self.no_clip, }; let generation = self @@ -313,14 +339,21 @@ impl Sim { fn update_view_position(&mut self) { let mut view_position = *self.prediction.predicted_position(); let mut view_velocity = *self.prediction.predicted_velocity(); + let mut view_on_ground = *self.prediction.predicted_on_ground(); + let orientation = if self.no_clip { + self.local_character_controller.orientation() + } else { + self.local_character_controller.horizontal_orientation() + }; // Apply input that hasn't been sent yet let predicted_input = CharacterInput { // We divide by how far we are through the timestep because self.average_movement_input // is always over the entire timestep, filling in zeroes for the future, and we // want to use the average over what we have so far. Dividing by zero is handled // by the character_controller sanitizing this input. - movement: self.local_character_controller.orientation() * self.average_movement_input + movement: orientation * self.average_movement_input / (self.since_input_sent.as_secs_f32() / self.cfg.step_interval.as_secs_f32()), + jump: self.is_jumping, no_clip: self.no_clip, }; character_controller::run_character_step( @@ -328,6 +361,7 @@ impl Sim { &self.graph, &mut view_position, &mut view_velocity, + &mut view_on_ground, &predicted_input, self.since_input_sent.as_secs_f32(), ); diff --git a/common/src/character_controller/mod.rs b/common/src/character_controller/mod.rs index 6476c1d1..93a898c7 100644 --- a/common/src/character_controller/mod.rs +++ b/common/src/character_controller/mod.rs @@ -5,7 +5,7 @@ use tracing::warn; use crate::{ character_controller::{ - collision::{check_collision, CollisionContext}, + collision::{check_collision, Collision, CollisionContext}, vector_bounds::{BoundedVectors, VectorBound}, }, math, @@ -22,6 +22,7 @@ pub fn run_character_step( graph: &DualGraph, position: &mut Position, velocity: &mut na::Vector3, + on_ground: &mut bool, input: &CharacterInput, dt_seconds: f32, ) { @@ -32,14 +33,16 @@ pub fn run_character_step( chunk_layout: ChunkLayout::new(sim_config.chunk_size as usize), radius: sim_config.character_config.character_radius, }, + up: graph.get_relative_up(position).unwrap(), dt_seconds, movement_input: sanitize_motion_input(input.movement), + jump_input: input.jump, }; if input.no_clip { - run_no_clip_character_step(&ctx, position, velocity); + run_no_clip_character_step(&ctx, position, velocity, on_ground); } else { - run_standard_character_step(&ctx, position, velocity); + run_standard_character_step(&ctx, position, velocity, on_ground); } // Renormalize @@ -55,18 +58,38 @@ fn run_standard_character_step( ctx: &CharacterControllerContext, position: &mut Position, velocity: &mut na::Vector3, + on_ground: &mut bool, ) { + let mut ground_normal = None; + if *on_ground { + ground_normal = get_ground_normal(ctx, position); + } + + // Handle jumping + if ctx.jump_input && ground_normal.is_some() { + let horizontal_velocity = *velocity - *ctx.up * ctx.up.dot(velocity); + *velocity = horizontal_velocity + *ctx.up * ctx.cfg.jump_speed; + ground_normal = None; + } + let old_velocity = *velocity; // Update velocity - let current_to_target_velocity = ctx.movement_input * ctx.cfg.max_ground_speed - *velocity; - let max_delta_velocity = ctx.cfg.ground_acceleration * ctx.dt_seconds; - if current_to_target_velocity.norm_squared() > math::sqr(max_delta_velocity) { - *velocity += current_to_target_velocity.normalize() * max_delta_velocity; + if let Some(ground_normal) = ground_normal { + apply_ground_controls(ctx, &ground_normal, velocity); } else { - *velocity += current_to_target_velocity; + apply_air_controls(ctx, velocity); + + // Apply air resistance + *velocity *= (-ctx.cfg.air_resistance * ctx.dt_seconds).exp(); } + // Apply gravity + *velocity -= *ctx.up * ctx.cfg.gravity_acceleration * ctx.dt_seconds; + + // Apply speed cap + *velocity = velocity.cap_magnitude(ctx.cfg.speed_cap); + // Estimate the average velocity by using the average of the old velocity and new velocity, // which has the effect of modeling a velocity that changes linearly over the timestep. // This is necessary to avoid the following two issues: @@ -74,37 +97,128 @@ fn run_standard_character_step( // 2. Movement artifacts, which would occur if only the new velocity was used. One // example of such an artifact is the character moving backwards slightly when they // stop moving after releasing a direction key. - let estimated_average_velocity = (*velocity + old_velocity) * 0.5; + let average_velocity = (*velocity + old_velocity) * 0.5; + // Handle actual movement apply_velocity( ctx, - estimated_average_velocity * ctx.dt_seconds, + average_velocity * ctx.dt_seconds, position, velocity, + &mut ground_normal, ); + + *on_ground = ground_normal.is_some(); } fn run_no_clip_character_step( ctx: &CharacterControllerContext, position: &mut Position, velocity: &mut na::Vector3, + on_ground: &mut bool, ) { *velocity = ctx.movement_input * ctx.cfg.no_clip_movement_speed; + *on_ground = false; position.local *= math::translate_along(&(*velocity * ctx.dt_seconds)); } +/// Returns the normal corresponding to the ground below the character, up to the `allowed_distance`. If +/// no such ground exists, returns `None`. +fn get_ground_normal( + ctx: &CharacterControllerContext, + position: &Position, +) -> Option> { + // Since the character can be at a corner between a slanted wall and the ground, the first collision + // directly below the character is not guaranteed to be part of the ground regardless of whether the + // character is on the ground. To handle this, we repeatedly redirect the direction we search to be + // parallel to walls we collide with to ensure that we find the ground if is indeed below the character. + const MAX_COLLISION_ITERATIONS: u32 = 6; + let mut allowed_displacement = BoundedVectors::new( + -ctx.up.into_inner() * ctx.cfg.ground_distance_tolerance, + None, + ); + + for _ in 0..MAX_COLLISION_ITERATIONS { + let collision_result = check_collision( + &ctx.collision_context, + position, + allowed_displacement.displacement(), + ); + if let Some(collision) = collision_result.collision.as_ref() { + if is_ground(ctx, &collision.normal) { + // We found the ground, so return its normal. + return Some(collision.normal); + } + allowed_displacement.add_bound(VectorBound::new(collision.normal, collision.normal)); + } else { + // Return `None` if we travel the whole `allowed_displacement` and don't find the ground. + return None; + } + } + // Return `None` if we fail to find the ground after the maximum number of attempts + None +} + +/// Checks whether the given normal is flat enough to be considered part of the ground +fn is_ground(ctx: &CharacterControllerContext, normal: &na::UnitVector3) -> bool { + let min_slope_up_component = 1.0 / (ctx.cfg.max_ground_slope.powi(2) + 1.0).sqrt(); + normal.dot(&ctx.up) > min_slope_up_component +} + +/// Updates the velocity based on user input assuming the character is on the ground +fn apply_ground_controls( + ctx: &CharacterControllerContext, + ground_normal: &na::UnitVector3, + velocity: &mut na::Vector3, +) { + // Set `target_ground_velocity` to have a consistent magnitude regardless + // of the movement direction, but ensure that the horizontal direction matches + // the horizontal direction of the intended movement direction. + let movement_norm = ctx.movement_input.norm(); + let target_ground_velocity = if movement_norm < 1e-16 { + na::Vector3::zeros() + } else { + let mut unit_movement = ctx.movement_input / movement_norm; + math::project_to_plane(&mut unit_movement, ground_normal, &ctx.up, 0.0); + unit_movement.try_normalize_mut(1e-16); + unit_movement * movement_norm * ctx.cfg.max_ground_speed + }; + + // Set `ground_velocity` to be the current velocity's ground-parallel component, + // using a basis that contains the up vector to ensure that the result is unaffected + // by gravity. + let mut ground_velocity = *velocity; + math::project_to_plane(&mut ground_velocity, ground_normal, &ctx.up, 0.0); + + // Adjust the ground-parallel component of the velocity vector to be closer to the + // target velocity. + let current_to_target_velocity = target_ground_velocity - ground_velocity; + let max_delta_velocity = ctx.cfg.ground_acceleration * ctx.dt_seconds; + if current_to_target_velocity.norm_squared() > max_delta_velocity.powi(2) { + *velocity += current_to_target_velocity.normalize() * max_delta_velocity; + } else { + *velocity += current_to_target_velocity; + } +} + +/// Updates the velocity based on user input assuming the character is in the air +fn apply_air_controls(ctx: &CharacterControllerContext, velocity: &mut na::Vector3) { + *velocity += ctx.movement_input * ctx.cfg.air_acceleration * ctx.dt_seconds; +} + /// Updates the character's position based on the given average velocity while handling collisions. -/// Also updates the velocity based on collisions that occur. +/// Also updates the velocity and ground normal based on collisions that occur. fn apply_velocity( ctx: &CharacterControllerContext, expected_displacement: na::Vector3, position: &mut Position, velocity: &mut na::Vector3, + ground_normal: &mut Option>, ) { // To prevent an unbounded runtime, we only allow a limited number of collisions to be processed in // a single step. If the character encounters excessively complex geometry, it is possible to hit this limit, // in which case further movement processing is delayed until the next time step. - const MAX_COLLISION_ITERATIONS: u32 = 5; + const MAX_COLLISION_ITERATIONS: u32 = 6; let mut bounded_vectors = BoundedVectors::new(expected_displacement, Some(*velocity)); @@ -124,8 +238,7 @@ fn apply_velocity( / bounded_vectors.displacement().magnitude(); bounded_vectors.scale_displacement(displacement_reduction_factor); - // Block further movement towards the wall. - bounded_vectors.add_bound(VectorBound::new(collision.normal, collision.normal)); + handle_collision(ctx, collision, &mut bounded_vectors, ground_normal); } else { all_collisions_resolved = true; break; @@ -139,11 +252,32 @@ fn apply_velocity( *velocity = *bounded_vectors.velocity().unwrap(); } +/// Updates character information based on the results of a single collision +fn handle_collision( + ctx: &CharacterControllerContext, + collision: Collision, + bounded_vectors: &mut BoundedVectors, + ground_normal: &mut Option>, +) { + // 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. + if is_ground(ctx, &collision.normal) { + bounded_vectors.add_bound(VectorBound::new(collision.normal, ctx.up)); + + *ground_normal = Some(collision.normal); + } else { + bounded_vectors.add_bound(VectorBound::new(collision.normal, collision.normal)); + } +} + /// Contains all information about a character that the character controller doesn't change during /// one of its simulation steps struct CharacterControllerContext<'a> { collision_context: CollisionContext<'a>, + up: na::UnitVector3, cfg: &'a CharacterConfig, dt_seconds: f32, movement_input: na::Vector3, + jump_input: bool, } diff --git a/common/src/proto.rs b/common/src/proto.rs index df701969..e1dd0002 100644 --- a/common/src/proto.rs +++ b/common/src/proto.rs @@ -40,6 +40,7 @@ pub struct StateDelta { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CharacterState { pub velocity: na::Vector3, + pub on_ground: bool, pub orientation: na::UnitQuaternion, } @@ -62,6 +63,7 @@ pub struct Command { pub struct CharacterInput { /// Relative to the character's current position, excluding orientation pub movement: na::Vector3, + pub jump: bool, pub no_clip: bool, } diff --git a/common/src/sim_config.rs b/common/src/sim_config.rs index a4ffafc0..8c2c81f6 100644 --- a/common/src/sim_config.rs +++ b/common/src/sim_config.rs @@ -75,8 +75,22 @@ pub struct CharacterConfigRaw { pub no_clip_movement_speed: Option, /// Character maximumum movement speed while on the ground in m/s pub max_ground_speed: Option, + /// Character artificial speed cap to avoid overloading the server in m/s + pub speed_cap: Option, + /// Maximum ground slope (0=horizontal, 1=45 degrees) + pub max_ground_slope: Option, /// Character acceleration while on the ground in m/s^2 pub ground_acceleration: Option, + /// Character acceleration while in the air in m/s^2 + pub air_acceleration: Option, + /// Acceleration of gravity in m/s^2 + pub gravity_acceleration: Option, + /// Air resistance in (m/s^2) per (m/s); scales linearly with respect to speed + pub air_resistance: Option, + /// How fast the player jumps off the ground in m/s + pub jump_speed: Option, + /// How far away the player needs to be from the ground in meters to be considered in the air + pub ground_distance_tolerance: Option, /// Radius of the character in meters pub character_radius: Option, } @@ -86,7 +100,14 @@ pub struct CharacterConfigRaw { pub struct CharacterConfig { pub no_clip_movement_speed: f32, pub max_ground_speed: f32, + pub speed_cap: f32, + pub max_ground_slope: f32, pub ground_acceleration: f32, + pub air_acceleration: f32, + pub gravity_acceleration: f32, + pub air_resistance: f32, + pub jump_speed: f32, + pub ground_distance_tolerance: f32, pub character_radius: f32, } @@ -95,7 +116,15 @@ impl CharacterConfig { CharacterConfig { no_clip_movement_speed: x.no_clip_movement_speed.unwrap_or(12.0) * meters_to_absolute, max_ground_speed: x.max_ground_speed.unwrap_or(4.0) * meters_to_absolute, + speed_cap: x.speed_cap.unwrap_or(30.0) * meters_to_absolute, + max_ground_slope: x.max_ground_slope.unwrap_or(1.73), // 60 degrees ground_acceleration: x.ground_acceleration.unwrap_or(20.0) * meters_to_absolute, + air_acceleration: x.air_acceleration.unwrap_or(2.0) * meters_to_absolute, + gravity_acceleration: x.gravity_acceleration.unwrap_or(20.0) * meters_to_absolute, + air_resistance: x.air_resistance.unwrap_or(0.2), + jump_speed: x.jump_speed.unwrap_or(8.0) * meters_to_absolute, + ground_distance_tolerance: x.ground_distance_tolerance.unwrap_or(0.2) + * meters_to_absolute, character_radius: x.character_radius.unwrap_or(0.4) * meters_to_absolute, } } diff --git a/server/src/sim.rs b/server/src/sim.rs index 0b243683..2f6118a0 100644 --- a/server/src/sim.rs +++ b/server/src/sim.rs @@ -136,10 +136,12 @@ impl Sim { state: CharacterState { orientation: na::one(), velocity: na::Vector3::zeros(), + on_ground: false, }, }; let initial_input = CharacterInput { movement: na::Vector3::zeros(), + jump: false, no_clip: true, }; let entity = self.world.spawn((id, position, character, initial_input)); @@ -204,6 +206,7 @@ impl Sim { &self.graph, position, &mut character.state.velocity, + &mut character.state.on_ground, input, self.cfg.step_interval.as_secs_f32(), ); @@ -246,11 +249,10 @@ impl Sim { // We want to load all chunks that a player can interact with in a single step, so chunk_generation_distance // is set up to cover that distance. - // TODO: Use actual max speed instead of max ground speed. let chunk_generation_distance = dodeca::BOUNDING_SPHERE_RADIUS + self.cfg.character_config.character_radius as f64 - + self.cfg.character_config.max_ground_speed as f64 - * self.cfg.step_interval.as_secs_f64() + + self.cfg.character_config.speed_cap as f64 * self.cfg.step_interval.as_secs_f64() + + self.cfg.character_config.ground_distance_tolerance as f64 + 0.001; // Load all chunks around entities corresponding to clients, which correspond to entities