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, world_rotation, NO_ROTATION};
use super::sim_math;
use super::{
ActionMap, Camera, CharacterMove, Children, Collider, ColliderShape,
FrameTime, GlobalTransform, InputBinding, KeyCode, MouseButton, PadButton,
Parent, PhysicsWorld, RigidBody, RigidBodyKind, RuntimeInput, Stick,
WaterBody,
};
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_CROUCH: &str = "player.crouch";
pub const PLAYER_ACTIONS: [&str; 7] = [
PLAYER_FORWARD,
PLAYER_BACK,
PLAYER_LEFT,
PLAYER_RIGHT,
PLAYER_JUMP,
PLAYER_SPRINT,
PLAYER_CROUCH,
];
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 air_jumps: u32,
pub gravity: f32,
pub look_sensitivity: f32,
pub mouse_look: bool,
pub collision_mask: u32,
pub yaw: f32,
pub pitch: f32,
pub pitch_limits: [f32; 2],
pub yaw_limits: Option<[f32; 2]>,
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,
pub crouch_height: f32,
pub crouch_multiplier: f32,
pub swim_speed: f32,
pub float_depth: f32,
#[serde(skip)]
pub swimming: bool,
#[serde(skip)]
pub crouched: bool,
#[serde(skip)]
pub crouch_drop: f32,
#[serde(skip)]
pub camera_drop: f32,
#[serde(skip)]
pub vertical_speed: f32,
#[serde(skip)]
pub grounded: bool,
#[serde(skip)]
pub jump_requested: bool,
#[serde(skip)]
pub air_jumps_used: u32,
#[serde(skip)]
pub floor: Option<(Entity, [f32; 3])>,
#[serde(skip)]
pub floor_rotation: [f32; 4],
#[serde(skip)]
pub wall: Option<Entity>,
#[serde(skip)]
pub velocity: [f32; 3],
#[serde(skip)]
pub dash_velocity: [f32; 3],
#[serde(skip)]
pub dash_left: f32,
}
impl Default for PlayerController {
fn default() -> Self {
Self {
walk_speed: 4.0,
sprint_multiplier: 1.8,
jump_speed: 5.0,
air_jumps: 0,
gravity: 9.81,
look_sensitivity: 0.002,
mouse_look: true,
collision_mask: u32::MAX,
yaw: 0.0,
pitch: 0.0,
pitch_limits: [-MAX_PITCH, MAX_PITCH],
yaw_limits: None,
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,
crouch_height: 0.0,
crouch_multiplier: 0.5,
swim_speed: 0.0,
float_depth: 0.5,
swimming: false,
crouched: false,
crouch_drop: 0.0,
camera_drop: 0.0,
vertical_speed: 0.0,
grounded: false,
jump_requested: false,
air_jumps_used: 0,
floor: None,
floor_rotation: NO_ROTATION,
wall: None,
velocity: [0.0; 3],
dash_velocity: [0.0; 3],
dash_left: 0.0,
}
}
}
const MAX_PITCH: f32 = 89.0_f32.to_radians();
pub(super) fn bind_default_actions(map: &mut ActionMap) {
let bindings: [(&str, &[KeyCode]); 7] = [
(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]),
(PLAYER_CROUCH, &[KeyCode::KeyC, KeyCode::ControlLeft]),
];
for (action, keys) in bindings {
for key in keys {
map.bind(action, InputBinding::Key(*key));
}
}
let pads: [(&str, &[PadButton]); 7] = [
(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]),
(PLAYER_CROUCH, &[PadButton::East]),
];
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 -= motion[1] * sensitivity;
}
player.yaw -= stick[0] * pad_turn;
player.pitch += stick[1] * pad_turn;
let [low, high] = player.pitch_limits;
player.pitch = player
.pitch
.max(low.max(-MAX_PITCH))
.min(high.min(MAX_PITCH));
if let Some([low, high]) = player.yaw_limits {
player.yaw = player.yaw.max(low).min(high);
}
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;
camera.position[1] -= player.crouch_drop;
} else {
camera.position[1] -=
player.crouch_drop - player.camera_drop;
}
}
}
if !orbit && player.camera_offset == [0.0; 3] {
player.camera_drop = player.crouch_drop;
}
}
}
fn crouch_shape(
shape: ColliderShape,
height: f32,
) -> Option<(ColliderShape, f32)> {
let half = height / 2.0;
match shape {
ColliderShape::Capsule {
half_height,
radius,
} if half_height + radius > half.max(radius) => {
let half = half.max(radius);
Some((
ColliderShape::Capsule {
half_height: half - radius,
radius,
},
half_height + radius - half,
))
}
ColliderShape::Box { half_extents } if half_extents[1] > half => {
Some((
ColliderShape::Box {
half_extents: [half_extents[0], half, half_extents[2]],
},
half_extents[1] - half,
))
}
_ => None,
}
}
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, clippy::too_many_arguments)]
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>,
>,
waters: Query<
(
&WaterBody,
&Transform,
Option<&Parent>,
Option<&GlobalTransform>,
),
Without<PlayerController>,
>,
mut pushed: Query<&mut RigidBody, Without<PlayerController>>,
) {
let dt = time.fixed_delta.as_secs_f32();
let seconds =
(time.fixed_tick as f64 * time.fixed_delta.as_secs_f64()) as f32;
let pose_of = |floor: Entity| {
let (t, parent, global) = floors.get(floor).ok()?;
Some((
world_position(t, parent, global),
world_rotation(t, parent, global),
))
};
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);
let crouch = actions.held(&input, PLAYER_CROUCH);
let rise = actions.held(&input, PLAYER_JUMP);
for (entity, mut player, mut transform, collider) in &mut players {
let mut shape =
collider.map_or(DEFAULT_PLAYER_SHAPE, |collider| collider.shape);
let water = (player.swim_speed > 0.0)
.then(|| water_at(&waters, transform.position, seconds))
.flatten()
.filter(|(surface, _)| transform.position[1] < *surface);
player.swimming = water.is_some();
match (player.crouch_height > 0.0 && !player.swimming)
.then(|| crouch_shape(shape, player.crouch_height))
.flatten()
{
Some((low, drop)) => {
if crouch && !player.crouched {
player.crouched = true;
transform.position[1] -= drop;
} else if !crouch && player.crouched {
let blocked = physics
.shape_cast(
low,
transform.position,
[0.0, 1.0, 0.0],
2.0 * drop,
player.collision_mask,
Some(entity),
)
.is_some();
if !blocked {
player.crouched = false;
transform.position[1] += drop;
}
}
if player.crouched {
shape = low;
}
player.crouch_drop = if player.crouched { drop } else { 0.0 };
}
None => {
player.crouched = false;
player.crouch_drop = 0.0;
}
}
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 base = if player.swimming {
player.swim_speed
} else {
player.walk_speed
};
let speed = base
* if player.crouched {
player.crouch_multiplier
} else if sprint {
player.sprint_multiplier
} else {
1.0
};
if length > 0.0 {
motion = motion.map(|value| value / length * speed * dt);
}
if let Some((surface, current)) = water {
let rest = surface - player.float_depth;
let y = transform.position[1];
player.vertical_speed = if player.jump_requested && y >= rest - 0.1
{
player.jump_speed
} else if crouch {
-speed
} else if rise && y < rest {
speed
} else {
(2.0 * (rest - y)).clamp(-speed, speed)
};
for axis in [0, 2] {
motion[axis] += current[axis] * dt;
}
} else {
if player.jump_requested && !player.crouched {
if player.grounded {
player.vertical_speed = player.jump_speed;
} else if player.air_jumps_used < player.air_jumps {
player.air_jumps_used += 1;
player.vertical_speed = player.jump_speed;
}
}
player.vertical_speed -= player.gravity * dt;
}
player.jump_requested = false;
motion[1] = player.vertical_speed * dt;
if player.dash_left > 0.0 {
motion = player.dash_velocity.map(|value| value * dt);
player.vertical_speed = 0.0;
player.dash_left = (player.dash_left - dt).max(0.0);
}
let (start, turn) = CharacterMove::ride(
&physics,
shape,
transform.position,
player
.floor
.map(|(floor, at)| (floor, at, player.floor_rotation)),
player.collision_mask,
entity,
pose_of,
);
if turn != 0.0 {
player.yaw += turn;
transform.rotation = [0.0, player.yaw, 0.0];
}
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.air_jumps_used = 0;
}
if (moved.grounded && player.vertical_speed < 0.0)
|| (moved.ceiling && player.vertical_speed > 0.0)
{
player.vertical_speed = 0.0;
}
let floor =
moved.floor.and_then(|floor| Some((floor, pose_of(floor)?)));
player.floor = floor.map(|(floor, (at, _))| (floor, at));
player.floor_rotation =
floor.map_or(NO_ROTATION, |(_, (_, turn))| turn);
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;
}
}
#[allow(clippy::type_complexity)]
fn water_at(
waters: &Query<
(
&WaterBody,
&Transform,
Option<&Parent>,
Option<&GlobalTransform>,
),
Without<PlayerController>,
>,
position: [f32; 3],
seconds: f32,
) -> Option<(f32, [f32; 3])> {
let [x, _, z] = position;
waters
.iter()
.filter_map(|(water, transform, parent, global)| {
let origin = world_position(transform, parent, global);
water.contains(origin, x, z).then(|| {
(origin[1] + water.wave(x, z, seconds).0, water.current())
})
})
.max_by(|a, b| {
a.0.total_cmp(&b.0)
.then(a.1[0].total_cmp(&b.1[0]))
.then(a.1[2].total_cmp(&b.1[2]))
})
}
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);
}
}
}
}