Skip to content

Commit

Permalink
Add character controller physics
Browse files Browse the repository at this point in the history
  • Loading branch information
patowen committed Oct 24, 2023
1 parent fed63f8 commit ccea05c
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 23 deletions.
10 changes: 10 additions & 0 deletions client/src/graphics/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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,
Expand Down Expand Up @@ -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();
Expand Down
25 changes: 24 additions & 1 deletion client/src/local_character_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<f32> {
// 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();
}
Expand Down Expand Up @@ -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
Expand All @@ -172,34 +190,39 @@ 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);
yaw += 0.5;
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);
yaw -= 0.2;
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);
yaw += 6.2;
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);
yaw -= 1.2;
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]
Expand Down
15 changes: 14 additions & 1 deletion client/src/prediction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub struct PredictedMotion {
generation: u16,
predicted_position: Position,
predicted_velocity: na::Vector3<f32>,
predicted_on_ground: bool,
}

impl PredictedMotion {
Expand All @@ -28,6 +29,7 @@ impl PredictedMotion {
generation: 0,
predicted_position: initial_position,
predicted_velocity: na::Vector3::zeros(),
predicted_on_ground: false,
}
}

Expand All @@ -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(),
);
Expand All @@ -55,6 +58,7 @@ impl PredictedMotion {
generation: u16,
position: Position,
velocity: na::Vector3<f32>,
on_ground: bool,
) {
let first_gen = self.generation.wrapping_sub(self.log.len() as u16);
let obsolete = usize::from(generation.wrapping_sub(first_gen));
Expand All @@ -65,13 +69,15 @@ 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(
cfg,
graph,
&mut self.predicted_position,
&mut self.predicted_velocity,
&mut self.predicted_on_ground,
input,
cfg.step_interval.as_secs_f32(),
);
Expand All @@ -86,6 +92,10 @@ impl PredictedMotion {
pub fn predicted_velocity(&self) -> &na::Vector3<f32> {
&self.predicted_velocity
}

pub fn predicted_on_ground(&self) -> &bool {
&self.predicted_on_ground
}
}

#[cfg(test)]
Expand All @@ -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,
};

Expand All @@ -121,6 +133,7 @@ mod tests {
generation,
pos(),
na::Vector3::zeros(),
false,
)
};

Expand Down
42 changes: 38 additions & 4 deletions client/src/sim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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(),
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -233,6 +254,7 @@ impl Sim {
latest_input,
*pos,
ch.state.velocity,
ch.state.on_ground,
);
}

Expand Down Expand Up @@ -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
Expand All @@ -313,21 +339,29 @@ 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(
&self.cfg,
&self.graph,
&mut view_position,
&mut view_velocity,
&mut view_on_ground,
&predicted_input,
self.since_input_sent.as_secs_f32(),
);
Expand Down
Loading

0 comments on commit ccea05c

Please sign in to comment.