use bevy_ecs::entity::Entity;
use bevy_ecs::prelude::{Component, Query, Res, ResMut, With, Without};
use serde::{Deserialize, Serialize};
use super::cpu_physics::world_position;
use super::sim_math;
use super::{
ActionMap, Camera, CharacterMove, Children, Collider, ColliderShape,
FrameTime, GlobalTransform, InputBinding, KeyCode, MouseButton, PadButton,
Parent, PhysicsWorld, RigidBody, RigidBodyKind, RuntimeInput, Stick,
};
use crate::Transform;
const CAMERA_RADIUS: f32 = 0.2;
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 mouse_look: bool,
pub collision_mask: u32,
pub yaw: f32,
pub pitch: f32,
pub camera_distance: f32,
pub camera_offset: [f32; 3],
pub camera_height: f32,
pub max_slope: f32,
pub max_step_height: f32,
pub push_bodies: bool,
pub turn_speed: f32,
#[serde(skip)]
pub vertical_speed: f32,
#[serde(skip)]
pub grounded: bool,
#[serde(skip)]
pub jump_requested: bool,
#[serde(skip)]
pub floor: Option<(Entity, [f32; 3])>,
#[serde(skip)]
pub wall: Option<Entity>,
#[serde(skip)]
pub velocity: [f32; 3],
}
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,
mouse_look: true,
collision_mask: u32::MAX,
yaw: 0.0,
pitch: 0.0,
camera_distance: 0.0,
camera_height: 0.6,
camera_offset: [0.0; 3],
max_slope: 45.0_f32.to_radians(),
max_step_height: 0.3,
push_bodies: true,
turn_speed: 0.0,
vertical_speed: 0.0,
grounded: false,
jump_requested: false,
floor: None,
wall: None,
velocity: [0.0; 3],
}
}
}
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));
}
}
let pads: [(&str, &[PadButton]); 6] = [
(PLAYER_FORWARD, &[PadButton::LeftStickUp, PadButton::DpadUp]),
(
PLAYER_BACK,
&[PadButton::LeftStickDown, PadButton::DpadDown],
),
(
PLAYER_LEFT,
&[PadButton::LeftStickLeft, PadButton::DpadLeft],
),
(
PLAYER_RIGHT,
&[PadButton::LeftStickRight, PadButton::DpadRight],
),
(PLAYER_JUMP, &[PadButton::South]),
(PLAYER_SPRINT, &[PadButton::LeftStick]),
];
for (action, buttons) in pads {
for button in buttons {
map.bind(action, InputBinding::Pad(*button));
}
}
}
const PAD_LOOK_SPEED: f32 = 3.0;
const PAD_DEAD_ZONE: f32 = 0.15;
pub(super) fn player_look(
mut input: ResMut<RuntimeInput>,
actions: Res<ActionMap>,
physics: Res<PhysicsWorld>,
time: Res<FrameTime>,
mut players: Query<(
Entity,
&mut PlayerController,
&mut Transform,
Option<&Children>,
)>,
mut cameras: Query<
&mut Transform,
(With<Camera>, Without<PlayerController>),
>,
) {
let Some(mouse_look) = players
.iter()
.map(|(_, player, ..)| player.mouse_look)
.reduce(|a, b| a || b)
else {
return;
};
if input.key_just_pressed(KeyCode::Escape) || !mouse_look {
if input.cursor_captured() {
input.set_cursor_captured(false);
}
} else if input.mouse_just_pressed(MouseButton::Left) {
input.set_cursor_captured(true);
}
let motion = input.mouse_motion();
let stick = input.stick(Stick::Right).map(|axis| {
if axis.abs() < PAD_DEAD_ZONE {
0.0
} else {
axis
}
});
let pad_turn = PAD_LOOK_SPEED * time.real_delta.as_secs_f32();
let jump = actions.just_pressed(&input, PLAYER_JUMP);
for (entity, mut player, mut transform, children) in &mut players {
if input.cursor_captured() && player.mouse_look {
let sensitivity = player.look_sensitivity;
player.yaw -= motion[0] * sensitivity;
player.pitch = (player.pitch - motion[1] * sensitivity)
.clamp(-MAX_PITCH, MAX_PITCH);
}
player.yaw -= stick[0] * pad_turn;
player.pitch =
(player.pitch + stick[1] * pad_turn).clamp(-MAX_PITCH, MAX_PITCH);
player.jump_requested |= jump;
transform.rotation = [0.0, player.yaw, 0.0];
let (sin, cos) = sim_math::sin_cos(player.pitch);
let orbit = player.camera_distance > 0.0;
let mut distance = player.camera_distance;
if orbit {
let [x, y, z] = transform.position;
let (yaw_sin, yaw_cos) = sim_math::sin_cos(player.yaw);
let back = [cos * yaw_sin, -sin, cos * yaw_cos];
if let Some(hit) = physics.shape_cast(
ColliderShape::Sphere {
radius: CAMERA_RADIUS,
},
[x, y + player.camera_height, z],
back,
distance,
u32::MAX,
Some(entity),
) {
distance = hit.distance;
}
}
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];
let [x, y, z] = player.camera_offset;
if orbit {
camera.position = [
x,
y + player.camera_height - sin * distance,
z + cos * distance,
];
} else if player.camera_offset != [0.0; 3] {
camera.position = player.camera_offset;
}
}
}
}
}
fn push(
bodies: &mut Query<&mut RigidBody, Without<PlayerController>>,
wall: Entity,
normal: [f32; 3],
motion: [f32; 3],
dt: f32,
) {
let Ok(mut body) = bodies.get_mut(wall) else {
return;
};
let length = normal[0].hypot(normal[2]);
if body.kind != RigidBodyKind::Dynamic || length < 1e-3 || dt <= 0.0 {
return;
}
let into = [-normal[0] / length, -normal[2] / length];
let speed = (motion[0] * into[0] + motion[2] * into[1]) / dt;
let velocity = &mut body.linear_velocity;
let current = velocity[0] * into[0] + velocity[2] * into[1];
if speed > current {
velocity[0] += into[0] * (speed - current);
velocity[2] += into[1] * (speed - current);
}
}
#[allow(clippy::type_complexity)]
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>,
)>,
floors: Query<
(&Transform, Option<&Parent>, Option<&GlobalTransform>),
Without<PlayerController>,
>,
mut pushed: Query<&mut RigidBody, Without<PlayerController>>,
) {
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 shape =
collider.map_or(DEFAULT_PLAYER_SHAPE, |collider| collider.shape);
let start = CharacterMove::ride(
&physics,
shape,
transform.position,
player.floor,
player.collision_mask,
entity,
|floor| {
floors.get(floor).ok().map(|(t, parent, global)| {
world_position(t, parent, global)
})
},
);
let moved = physics.move_character_on_foot(
shape,
start,
motion,
player.collision_mask,
Some(entity),
player.max_slope,
player.max_step_height,
);
if let Some((wall, normal)) = moved.wall.filter(|_| player.push_bodies)
{
push(&mut pushed, wall, normal, motion, dt);
}
player.grounded = moved.grounded;
if (moved.grounded && player.vertical_speed < 0.0)
|| (moved.ceiling && player.vertical_speed > 0.0)
{
player.vertical_speed = 0.0;
}
player.floor = moved.floor.and_then(|floor| {
let (t, parent, global) = floors.get(floor).ok()?;
Some((floor, world_position(t, parent, global)))
});
player.wall = moved.wall.map(|(wall, _)| wall);
if dt > 0.0 {
player.velocity = std::array::from_fn(|axis| {
(moved.position[axis] - transform.position[axis]) / dt
});
}
transform.position = moved.position;
}
}
pub(super) fn player_face(
time: Res<FrameTime>,
players: Query<(&PlayerController, Option<&Children>)>,
mut rigs: Query<
&mut Transform,
(Without<Camera>, Without<PlayerController>),
>,
) {
use std::f32::consts::{PI, TAU};
let dt = time.fixed_delta.as_secs_f32();
for (player, children) in &players {
let [vx, _, vz] = player.velocity;
if player.turn_speed <= 0.0 || vx.hypot(vz) < 0.1 {
continue;
}
let heading = sim_math::atan2(-vx, -vz) - player.yaw;
for child in children.into_iter().flat_map(|children| &children.0) {
if let Ok(mut rig) = rigs.get_mut(*child) {
let turn =
(heading - rig.rotation[1] + PI).rem_euclid(TAU) - PI;
let step = player.turn_speed * dt;
rig.rotation[1] += turn.clamp(-step, step);
}
}
}
}