use bevy_ecs::entity::Entity;
use bevy_ecs::prelude::{Component, Query, Res, ResMut, With, Without};
use serde::{Deserialize, Serialize};
use super::sim_math;
use super::{
ActionMap, Camera, Children, Collider, ColliderShape, FrameTime,
InputBinding, KeyCode, MouseButton, PhysicsWorld, RuntimeInput,
};
use crate::Transform;
pub const PLAYER_FORWARD: &str = "player.forward";
pub const PLAYER_BACK: &str = "player.back";
pub const PLAYER_LEFT: &str = "player.left";
pub const PLAYER_RIGHT: &str = "player.right";
pub const PLAYER_JUMP: &str = "player.jump";
pub const PLAYER_SPRINT: &str = "player.sprint";
pub const PLAYER_ACTIONS: [&str; 6] = [
PLAYER_FORWARD,
PLAYER_BACK,
PLAYER_LEFT,
PLAYER_RIGHT,
PLAYER_JUMP,
PLAYER_SPRINT,
];
pub const DEFAULT_PLAYER_SHAPE: ColliderShape = ColliderShape::Capsule {
half_height: 0.6,
radius: 0.3,
};
#[derive(Component, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct PlayerController {
pub walk_speed: f32,
pub sprint_multiplier: f32,
pub jump_speed: f32,
pub gravity: f32,
pub look_sensitivity: f32,
pub collision_mask: u32,
pub yaw: f32,
pub pitch: f32,
#[serde(skip)]
pub vertical_speed: f32,
#[serde(skip)]
pub grounded: bool,
#[serde(skip)]
pub jump_requested: bool,
}
impl Default for PlayerController {
fn default() -> Self {
Self {
walk_speed: 4.0,
sprint_multiplier: 1.8,
jump_speed: 5.0,
gravity: 9.81,
look_sensitivity: 0.002,
collision_mask: u32::MAX,
yaw: 0.0,
pitch: 0.0,
vertical_speed: 0.0,
grounded: false,
jump_requested: false,
}
}
}
const MAX_PITCH: f32 = 89.0_f32.to_radians();
pub(super) fn bind_default_actions(map: &mut ActionMap) {
let bindings: [(&str, &[KeyCode]); 6] = [
(PLAYER_FORWARD, &[KeyCode::KeyW, KeyCode::ArrowUp]),
(PLAYER_BACK, &[KeyCode::KeyS, KeyCode::ArrowDown]),
(PLAYER_LEFT, &[KeyCode::KeyA, KeyCode::ArrowLeft]),
(PLAYER_RIGHT, &[KeyCode::KeyD, KeyCode::ArrowRight]),
(PLAYER_JUMP, &[KeyCode::Space]),
(PLAYER_SPRINT, &[KeyCode::ShiftLeft, KeyCode::ShiftRight]),
];
for (action, keys) in bindings {
for key in keys {
map.bind(action, InputBinding::Key(*key));
}
}
}
pub(super) fn player_look(
mut input: ResMut<RuntimeInput>,
actions: Res<ActionMap>,
mut players: Query<(
&mut PlayerController,
&mut Transform,
Option<&Children>,
)>,
mut cameras: Query<
&mut Transform,
(With<Camera>, Without<PlayerController>),
>,
) {
if players.is_empty() {
return;
}
if input.key_just_pressed(KeyCode::Escape) {
input.set_cursor_captured(false);
} else if input.mouse_just_pressed(MouseButton::Left) {
input.set_cursor_captured(true);
}
let motion = input.mouse_motion();
let jump = actions.just_pressed(&input, PLAYER_JUMP);
for (mut player, mut transform, children) in &mut players {
if input.cursor_captured() {
let sensitivity = player.look_sensitivity;
player.yaw -= motion[0] * sensitivity;
player.pitch = (player.pitch - motion[1] * sensitivity)
.clamp(-MAX_PITCH, MAX_PITCH);
}
player.jump_requested |= jump;
transform.rotation = [0.0, player.yaw, 0.0];
for child in children.into_iter().flat_map(|children| &children.0) {
if let Ok(mut camera) = cameras.get_mut(*child) {
camera.rotation = [player.pitch, 0.0, 0.0];
}
}
}
}
pub(super) fn player_move(
time: Res<FrameTime>,
input: Res<RuntimeInput>,
actions: Res<ActionMap>,
physics: Res<PhysicsWorld>,
mut players: Query<(
Entity,
&mut PlayerController,
&mut Transform,
Option<&Collider>,
)>,
) {
let dt = time.fixed_delta.as_secs_f32();
let axis = |positive, negative| {
f32::from(u8::from(actions.held(&input, positive)))
- f32::from(u8::from(actions.held(&input, negative)))
};
let (forward, right) = (
axis(PLAYER_FORWARD, PLAYER_BACK),
axis(PLAYER_RIGHT, PLAYER_LEFT),
);
let sprint = actions.held(&input, PLAYER_SPRINT);
for (entity, mut player, mut transform, collider) in &mut players {
let (sin, cos) = sim_math::sin_cos(player.yaw);
let mut motion = [
-sin * forward + cos * right,
0.0,
-cos * forward - sin * right,
];
let length = motion[0].hypot(motion[2]);
let speed = player.walk_speed
* if sprint {
player.sprint_multiplier
} else {
1.0
};
if length > 0.0 {
motion = motion.map(|value| value / length * speed * dt);
}
if player.grounded && player.jump_requested {
player.vertical_speed = player.jump_speed;
}
player.jump_requested = false;
player.vertical_speed -= player.gravity * dt;
motion[1] = player.vertical_speed * dt;
let moved = physics.move_character(
collider.map_or(DEFAULT_PLAYER_SHAPE, |collider| collider.shape),
transform.position,
motion,
player.collision_mask,
Some(entity),
);
player.grounded = moved.grounded;
if moved.grounded && player.vertical_speed < 0.0 {
player.vertical_speed = 0.0;
}
transform.position = moved.position;
}
}