Skip to content

Commit

Permalink
Update the character controller to allow walking and jumping
Browse files Browse the repository at this point in the history
  • Loading branch information
patowen committed Mar 23, 2023
1 parent a765574 commit bae56f2
Show file tree
Hide file tree
Showing 9 changed files with 515 additions and 153 deletions.
18 changes: 10 additions & 8 deletions client/src/graphics/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ impl Window {
let mut right = false;
let mut up = false;
let mut down = false;
let mut jump = false;
let mut jump_sticky = false;
let mut clockwise = false;
let mut anticlockwise = false;
let mut last_frame = Instant::now();
Expand All @@ -138,6 +140,8 @@ impl Window {
} else {
move_direction
});
self.sim.set_jump(jump || jump_sticky);
jump_sticky = false;

self.sim.rotate(&na::UnitQuaternion::from_axis_angle(
&-na::Vector3::z_axis(),
Expand All @@ -162,14 +166,8 @@ impl Window {
Event::DeviceEvent { event, .. } => match event {
DeviceEvent::MouseMotion { delta } if mouse_captured => {
const SENSITIVITY: f32 = 2e-3;
let rot = na::UnitQuaternion::from_axis_angle(
&na::Vector3::y_axis(),
-delta.0 as f32 * SENSITIVITY,
) * na::UnitQuaternion::from_axis_angle(
&na::Vector3::x_axis(),
-delta.1 as f32 * SENSITIVITY,
);
self.sim.rotate(&rot);
self.sim
.look(-delta.0 as f32 * SENSITIVITY, -delta.1 as f32 * SENSITIVITY);
}
_ => {}
},
Expand Down Expand Up @@ -230,6 +228,10 @@ impl Window {
VirtualKeyCode::F => {
down = state == ElementState::Pressed;
}
VirtualKeyCode::Space => {
jump = state == ElementState::Pressed;
jump_sticky = jump || jump_sticky;
}
VirtualKeyCode::V if state == ElementState::Pressed => {
self.sim.toggle_no_clip();
}
Expand Down
1 change: 1 addition & 0 deletions client/src/prediction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ mod tests {
let mock_graph = DualGraph::new();
let mock_character_input = CharacterInput {
movement: na::Vector3::x(),
jump: false,
no_clip: true,
};

Expand Down
172 changes: 163 additions & 9 deletions client/src/sim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ pub struct Sim {
no_clip: bool,
/// Whether no_clip will be toggled next step
toggle_no_clip: bool,
jump: bool,
jump_next_step: bool,
jump_next_step_sticky: bool,
prediction: PredictedMotion,
/// The last extrapolated inter-frame view position, used for rendering and gravity-specific
/// orientation computations
view_position: Position,
}

impl Sim {
Expand All @@ -63,17 +69,96 @@ impl Sim {
average_movement_input: na::zero(),
no_clip: true,
toggle_no_clip: false,
jump: false,
jump_next_step: false,
jump_next_step_sticky: false,
prediction: PredictedMotion::new(proto::Position {
node: NodeId::ROOT,
local: na::one(),
}),
view_position: Position::origin(),
}
}

pub fn rotate(&mut self, delta: &na::UnitQuaternion<f32>) {
self.orientation *= delta;
}

pub fn look(&mut self, delta_yaw: f32, delta_pitch: f32) {
if self.no_clip {
self.look_free(delta_yaw, delta_pitch);
} else {
self.look_with_gravity(delta_yaw, delta_pitch);
}
}

fn look_free(&mut self, delta_yaw: f32, delta_pitch: f32) {
self.orientation *= na::UnitQuaternion::from_axis_angle(&na::Vector3::y_axis(), delta_yaw)
* na::UnitQuaternion::from_axis_angle(&na::Vector3::x_axis(), delta_pitch);
}

fn look_with_gravity(&mut self, delta_yaw: f32, delta_pitch: f32) {
let Some(up) = self
.get_relative_up(&self.view_position)
.map(|up| self.orientation.conjugate() * up)
else {
return;
};

self.orientation *= na::UnitQuaternion::from_axis_angle(&up, delta_yaw);

if up.x.abs() < 0.9 {
// Full pitch implementation with logic to prevent turning upside-down
let current_pitch = -up.z.atan2(up.y);
let mut target_pitch = current_pitch + delta_pitch;
if delta_pitch > 0.0 {
target_pitch = target_pitch
.min(std::f32::consts::FRAC_PI_2) // Don't allow pitching up far enough to be upside-down
.max(current_pitch); // But if already upside-down, don't make any corrections.
} else {
target_pitch = target_pitch
.max(-std::f32::consts::FRAC_PI_2) // Don't allow pitching down far enough to be upside-down
.min(current_pitch); // But if already upside-down, don't make any corrections.
}

self.orientation *= na::UnitQuaternion::from_axis_angle(
&na::Vector3::x_axis(),
target_pitch - current_pitch,
);
} else {
// Player is rolled about 90 degrees. Since player view is sideways, we just
// allow them to pitch as far as they want.
self.orientation *=
na::UnitQuaternion::from_axis_angle(&na::Vector3::x_axis(), delta_pitch);
}
}

fn get_horizontal_orientation(&self) -> na::UnitQuaternion<f32> {
let Some(up) = self
.get_relative_up(&self.view_position)
.map(|up| self.orientation.conjugate() * up)
else {
return self.orientation;
};

if up.x.abs() < 0.9 {
let current_pitch = -up.z.atan2(up.y);
self.orientation
* na::UnitQuaternion::from_axis_angle(&na::Vector3::x_axis(), -current_pitch)
} else {
self.orientation
}
}

/// Returns the up-direction relative to the given position
fn get_relative_up(&self, position: &Position) -> Option<na::UnitVector3<f32>> {
self.graph.get(position.node).as_ref().map(|node| {
na::UnitVector3::new_normalize(
(common::math::mtranspose(&position.local) * node.state.up_direction()).xyz(),
)
})
}

pub fn set_movement_input(&mut self, movement_input: na::Vector3<f32>) {
self.movement_input = movement_input;
}
Expand All @@ -85,6 +170,11 @@ impl Sim {
self.toggle_no_clip = true;
}

pub fn set_jump(&mut self, jump: bool) {
self.jump_next_step = jump;
self.jump_next_step_sticky = jump || self.jump_next_step_sticky;
}

pub fn params(&self) -> Option<&Parameters> {
self.params.as_ref()
}
Expand Down Expand Up @@ -117,6 +207,9 @@ impl Sim {
self.toggle_no_clip = false;
}

self.jump = self.jump_next_step || self.jump_next_step_sticky;
self.jump_next_step_sticky = 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 All @@ -134,6 +227,36 @@ impl Sim {
self.average_movement_input +=
self.movement_input * dt.as_secs_f32() / step_interval.as_secs_f32();
}
self.update_view_position();
if !self.no_clip {
self.align_to_gravity();
}
}
}

fn align_to_gravity(&mut self) {
let Some(up) = self
.get_relative_up(&self.view_position)
.map(|up| self.orientation.conjugate() * up)
else {
return;
};

if up.z.abs() < 0.9 {
// If facing not too vertically, roll the camera to make it vertical.
let delta_roll = -up.x.atan2(up.y);
self.orientation *=
na::UnitQuaternion::from_axis_angle(&na::Vector3::z_axis(), delta_roll);
} else if up.y > 0.0 {
// Otherwise, if not upside-down, pan the camera to make it vertical.
let delta_yaw = (up.x / up.z).atan();
self.orientation *=
na::UnitQuaternion::from_axis_angle(&na::Vector3::y_axis(), delta_yaw);
} else {
// Otherwise, rotate the camera to look straight up or down.
self.orientation *=
na::UnitQuaternion::rotation_between(&(na::Vector3::z() * up.z.signum()), &up)
.unwrap();
}
}

Expand Down Expand Up @@ -288,8 +411,14 @@ impl Sim {

fn send_input(&mut self) {
let params = self.params.as_ref().unwrap();
let orientation = if self.no_clip {
self.orientation
} else {
self.get_horizontal_orientation()
};
let character_input = CharacterInput {
movement: sanitize_motion_input(self.orientation * self.average_movement_input),
movement: sanitize_motion_input(orientation * self.average_movement_input),
jump: self.jump,
no_clip: self.no_clip,
};
let generation = self
Expand All @@ -304,32 +433,57 @@ impl Sim {
});
}

pub fn view(&self) -> Position {
let mut result = *self.prediction.predicted_position();
let mut predicted_velocity = *self.prediction.predicted_velocity();
fn update_view_position(&mut self) {
let mut view_position = *self.prediction.predicted_position();
let mut view_velocity = *self.prediction.predicted_velocity();
let orientation = if self.no_clip {
self.orientation
} else {
self.get_horizontal_orientation()
};
if let Some(ref params) = self.params {
// 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.orientation * self.average_movement_input
movement: orientation * self.average_movement_input
/ (self.since_input_sent.as_secs_f32()
/ params.cfg.step_interval.as_secs_f32()),
jump: self.jump,
no_clip: self.no_clip,
};
character_controller::run_character_step(
&params.cfg,
&self.graph,
&mut result,
&mut predicted_velocity,
&mut view_position,
&mut view_velocity,
&predicted_input,
self.since_input_sent.as_secs_f32(),
);
}
result.local *= self.orientation.to_homogeneous();
result

// Rotate the player orientation to stay consistent with changes in gravity
if !self.no_clip {
if let (Some(old_up), Some(new_up)) = (
self.get_relative_up(&self.view_position),
self.get_relative_up(&view_position),
) {
self.orientation = na::UnitQuaternion::rotation_between_axis(&old_up, &new_up)
.unwrap_or(na::UnitQuaternion::identity())
* self.orientation;
}
}

self.view_position = view_position;
}

pub fn view(&self) -> Position {
Position {
node: self.view_position.node,
local: self.view_position.local * self.orientation.to_homogeneous(),
}
}

/// Destroy all aspects of an entity
Expand Down
Loading

0 comments on commit bae56f2

Please sign in to comment.