From b67e9191ee40253a3c71d3729302fab8dc769ee9 Mon Sep 17 00:00:00 2001 From: copygirl Date: Wed, 12 Nov 2025 17:34:25 +0100 Subject: [PATCH] Sync head orientation, add simple player visuals --- client/src/assets/mod.rs | 2 + client/src/assets/player.rs | 38 ++++++++++++++ client/src/camera.rs | 56 --------------------- client/src/input/client_inputs.rs | 35 ++++++------- client/src/input/head_orientation.rs | 30 +++++++++++ client/src/input/mod.rs | 7 ++- client/src/main.rs | 20 +++----- common/src/network/protocol.rs | 9 ++-- common/src/player.rs | 75 ++++++++++++++++++++++++---- 9 files changed, 169 insertions(+), 103 deletions(-) create mode 100644 client/src/assets/player.rs delete mode 100644 client/src/camera.rs create mode 100644 client/src/input/head_orientation.rs diff --git a/client/src/assets/mod.rs b/client/src/assets/mod.rs index 20b1d07..47f803b 100644 --- a/client/src/assets/mod.rs +++ b/client/src/assets/mod.rs @@ -7,7 +7,9 @@ use bevy::prelude::*; mod block; +mod player; pub fn plugin(app: &mut App) { app.add_plugins(block::plugin); + app.add_plugins(player::plugin); } diff --git a/client/src/assets/player.rs b/client/src/assets/player.rs new file mode 100644 index 0000000..fb6971e --- /dev/null +++ b/client/src/assets/player.rs @@ -0,0 +1,38 @@ +use bevy::prelude::*; +use common::prelude::*; + +pub fn plugin(app: &mut App) { + app.load_resource::(); + app.add_observer(insert_player_visuals); +} + +#[derive(Resource, Asset, Reflect, Clone)] +#[reflect(Resource)] +pub struct PlayerAssets { + #[dependency] + mesh: Handle, + #[dependency] + material: Handle, +} + +impl FromWorld for PlayerAssets { + fn from_world(world: &mut World) -> Self { + let assets = world.resource::(); + Self { + mesh: assets.add(Cuboid::new(1.0, 1.0, 1.0).into()), + material: assets.add(Color::srgb_u8(124, 255, 144).into()), + } + } +} + +fn insert_player_visuals( + event: On, + mut commands: Commands, + block_assets: Res, +) { + let player = event.entity; + commands.entity(player).insert(( + Mesh3d(block_assets.mesh.clone()), + MeshMaterial3d(block_assets.material.clone()), + )); +} diff --git a/client/src/camera.rs b/client/src/camera.rs deleted file mode 100644 index 945b696..0000000 --- a/client/src/camera.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::f32::consts::TAU; - -use bevy::prelude::*; - -use bevy::input::mouse::AccumulatedMouseMotion; - -#[derive(Component, Debug)] -pub struct CameraFreeLook { - /// The mouse sensitivity, in radians per pixel. - pub sensitivity: Vec2, - /// How far the camera can be tilted up and down. - pub pitch_limit: std::ops::RangeInclusive, - - /// Upon initialization, `pitch` and `yaw` will - /// be set from the camera transform's rotation. - initialized: bool, - /// The current yaw (right/left) of the camera, in radians. - pub yaw: f32, - /// The current pitch (tilt) of the camera, in radians. - pub pitch: f32, -} - -impl Default for CameraFreeLook { - fn default() -> Self { - Self { - sensitivity: Vec2::splat(0.2).map(f32::to_radians), - pitch_limit: -(TAU / 4.0)..=(TAU / 4.0), - initialized: false, - yaw: 0.0, - pitch: 0.0, - } - } -} - -pub fn camera_look( - accumulated_mouse_motion: Res, - camera: Single<(&mut Transform, &mut CameraFreeLook)>, -) { - let (mut transform, mut look) = camera.into_inner(); - - // Ensure the yaw and pitch are initialized once - // from the camera transform's current rotation. - if !look.initialized { - (look.yaw, look.pitch, _) = transform.rotation.to_euler(EulerRot::YXZ); - look.initialized = true; - } - - // Update the current camera state's internal yaw and pitch. - let motion = accumulated_mouse_motion.delta * look.sensitivity; - let (min, max) = look.pitch_limit.clone().into_inner(); - look.yaw = (look.yaw - motion.x).rem_euclid(TAU); // keep within 0°..360° - look.pitch = (look.pitch - motion.y).clamp(min, max); - - // Override the camera transform's rotation. - transform.rotation = Quat::from_euler(EulerRot::ZYX, 0.0, look.yaw, look.pitch); -} diff --git a/client/src/input/client_inputs.rs b/client/src/input/client_inputs.rs index ad4b841..2a8c597 100644 --- a/client/src/input/client_inputs.rs +++ b/client/src/input/client_inputs.rs @@ -1,5 +1,5 @@ //! Handles client-side input using [`lightyear`], updating the -//! [`ActionState`] of the affected entities (such as the `Player`). +//! [`ActionState`] of the affected entities, such as the `Player`. use bevy::prelude::*; use common::prelude::*; @@ -8,8 +8,6 @@ use lightyear::prelude::input::native::*; use bevy::window::{CursorGrabMode, CursorOptions}; -use crate::camera::CameraFreeLook; - pub fn plugin(app: &mut App) { app.add_systems( FixedPreUpdate, @@ -18,27 +16,30 @@ pub fn plugin(app: &mut App) { } fn buffer_input( - key_input: Res>, - mut action_state: Single<&mut ActionState, With>>, + keys: Res>, + player: Single<(&mut ActionState, &HeadOrientation), With>>, cursor: Single<&CursorOptions>, - camera: Single<&CameraFreeLook>, ) { - action_state.0 = if cursor.grab_mode == CursorGrabMode::None { - // If cursor is not grabbed, reset movement input. - Default::default() + let (mut inputs, orientation) = player.into_inner(); + inputs.0 = if cursor.grab_mode == CursorGrabMode::None { + Inputs { + look: *orientation, + ..default() + } } else { Inputs { + look: *orientation, #[rustfmt::skip] movement: { let mut movement = Vec2::ZERO; - if key_input.pressed(KeyCode::KeyD) { movement.x += 1.0; } - if key_input.pressed(KeyCode::KeyA) { movement.x -= 1.0; } - if key_input.pressed(KeyCode::KeyS) { movement.y += 1.0; } - if key_input.pressed(KeyCode::KeyW) { movement.y -= 1.0; } - Rot2::radians(-camera.yaw) * movement.clamp_length_max(1.0) + if keys.pressed(KeyCode::KeyD) { movement.x += 1.0; } + if keys.pressed(KeyCode::KeyA) { movement.x -= 1.0; } + if keys.pressed(KeyCode::KeyS) { movement.y += 1.0; } + if keys.pressed(KeyCode::KeyW) { movement.y -= 1.0; } + movement.clamp_length_max(1.0) }, - up: key_input.pressed(KeyCode::Space), - down: key_input.pressed(KeyCode::ShiftLeft), + up: keys.pressed(KeyCode::Space), + down: keys.pressed(KeyCode::ShiftLeft), } - }; + } } diff --git a/client/src/input/head_orientation.rs b/client/src/input/head_orientation.rs new file mode 100644 index 0000000..605f341 --- /dev/null +++ b/client/src/input/head_orientation.rs @@ -0,0 +1,30 @@ +use bevy::prelude::*; +use common::prelude::*; +use lightyear::prelude::input::native::*; + +use bevy::input::mouse::AccumulatedMouseMotion; + +use crate::input::is_cursor_grabbed; + +#[derive(Resource, Deref, DerefMut, Debug)] +pub struct MouseSensitivity(Vec2); + +impl Default for MouseSensitivity { + fn default() -> Self { + Self(Vec2::splat(0.2).map(f32::to_radians)) + } +} + +pub fn plugin(app: &mut App) { + app.init_resource::(); + app.add_systems(Update, update_head_orientation.run_if(is_cursor_grabbed)); +} + +fn update_head_orientation( + accumulated_mouse_motion: Res, + mouse_sensitivity: Res, + mut orientation: Single<&mut HeadOrientation, With>>, +) { + let delta = accumulated_mouse_motion.delta * **mouse_sensitivity; + **orientation = orientation.update(delta); +} diff --git a/client/src/input/mod.rs b/client/src/input/mod.rs index d3fa23a..4034ea3 100644 --- a/client/src/input/mod.rs +++ b/client/src/input/mod.rs @@ -2,9 +2,14 @@ use bevy::prelude::*; mod client_inputs; mod cursor_grab; +mod head_orientation; pub use cursor_grab::is_cursor_grabbed; pub fn plugin(app: &mut App) { - app.add_plugins((client_inputs::plugin, cursor_grab::plugin)); + app.add_plugins(( + client_inputs::plugin, + cursor_grab::plugin, + head_orientation::plugin, + )); } diff --git a/client/src/main.rs b/client/src/main.rs index 8280f32..a0c2f35 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -10,13 +10,11 @@ use lightyear::prelude::server::Started; mod args; mod assets; -mod camera; mod input; mod placement; mod ui; use args::*; -use camera::*; use placement::*; use input::is_cursor_grabbed; @@ -30,6 +28,7 @@ pub enum Screen { fn main() -> Result { let args = Args::parse(); let mut app = App::new(); + app.insert_resource(args); app.add_plugins( DefaultPlugins @@ -63,17 +62,11 @@ fn main() -> Result { ui::plugin, )); - app.insert_resource(args); - app.insert_state(Screen::Loading); - app.add_systems( OnEnter(Screen::Gameplay), (setup_scene, start_server_or_connect), ); - // TODO: Create and configure `SystemSet`s for gameplay, etc. - app.add_systems(Update, camera_look.run_if(is_cursor_grabbed)); - // `place_break_blocks` requires the camera's `GlobalTransform`. // For a most up-to-date value, run it after that's been updated. app.add_systems( @@ -86,6 +79,7 @@ fn main() -> Result { app.add_observer(spawn_initial_blocks); app.add_observer(handle_predicted_player_spawn); + app.insert_state(Screen::Loading); app.run(); Ok(()) } @@ -132,10 +126,10 @@ fn spawn_initial_blocks(_event: On, mut blocks: Blocks) { /// When the `Predicted` player we control is being spawned, insert necessary components. fn handle_predicted_player_spawn(event: On, mut commands: Commands) { let player = event.entity; - commands - .entity(player) + commands.entity(player).insert(( // Handle inputs on this entity. - .insert(InputMarker::::default()) - // Add a camera that can be freely rotated. - .with_child((Camera3d::default(), CameraFreeLook::default())); + InputMarker::::default(), + // TODO: Attach camera to player head eventually. + Camera3d::default(), + )); } diff --git a/common/src/network/protocol.rs b/common/src/network/protocol.rs index 40b730c..ead826e 100644 --- a/common/src/network/protocol.rs +++ b/common/src/network/protocol.rs @@ -6,7 +6,7 @@ use lightyear::input::native::plugin::InputPlugin; use serde::{Deserialize, Serialize}; use crate::block::Block; -use crate::player::Player; +use crate::player::{HeadOrientation, Player}; pub(super) fn plugin(app: &mut App) { app.add_plugins(InputPlugin::::default()); @@ -23,17 +23,14 @@ pub(super) fn plugin(app: &mut App) { app.add_systems(FixedUpdate, crate::player::movement); } -#[derive(Default, Deserialize, Serialize, Reflect, Clone, PartialEq, Debug)] +#[derive(Default, MapEntities, Deserialize, Serialize, Reflect, Clone, PartialEq, Debug)] pub struct Inputs { + pub look: HeadOrientation, pub movement: Vec2, pub up: bool, pub down: bool, } -impl MapEntities for Inputs { - fn map_entities(&mut self, _entity_mapper: &mut E) {} -} - fn interpolate_transform(start: Transform, other: Transform, t: f32) -> Transform { Transform { translation: start.translation.lerp(other.translation, t), diff --git a/common/src/player.rs b/common/src/player.rs index bace3e3..2f9012d 100644 --- a/common/src/player.rs +++ b/common/src/player.rs @@ -1,3 +1,5 @@ +use std::f32::consts::{PI, TAU}; + use bevy::prelude::*; use lightyear::prelude::*; @@ -7,34 +9,87 @@ use serde::{Deserialize, Serialize}; use crate::network::protocol::Inputs; const MOVEMENT_SPEED: f32 = 5.0; +const PITCH_LIMIT: f32 = TAU / 4.0; -#[derive(Component, PartialEq, Deserialize, Serialize)] -#[require(Transform)] +#[derive(Component, Deserialize, Serialize, PartialEq)] +#[require(Transform, HeadOrientation)] pub struct Player; +#[derive(Component, Deserialize, Serialize, Reflect, Default, Clone, Copy, PartialEq, Debug)] +#[serde(from = "HeadOrientationUnchecked")] +pub struct HeadOrientation { + yaw: f32, + pitch: f32, +} + +impl HeadOrientation { + pub fn new(yaw: f32, pitch: f32) -> Self { + Self { + yaw: (yaw + PI).rem_euclid(TAU) - PI, + pitch: pitch.clamp(-PITCH_LIMIT, PITCH_LIMIT), + } + } + + pub fn yaw(&self) -> f32 { + self.yaw + } + + pub fn pitch(&self) -> f32 { + self.pitch + } + + pub fn update(&self, motion: Vec2) -> Self { + Self::new(self.yaw - motion.x, self.pitch - motion.y) + } + + pub fn to_quat(&self) -> Quat { + Quat::from_euler(EulerRot::ZYX, 0.0, self.yaw, self.pitch) + } +} + +#[derive(Deserialize)] +#[serde(rename = "HeadOrientation")] +struct HeadOrientationUnchecked { + yaw: f32, + pitch: f32, +} + +impl From for HeadOrientation { + fn from(value: HeadOrientationUnchecked) -> Self { + Self::new(value.yaw, value.pitch) + } +} + pub fn movement( time: Res>, mut players: Query< - (&mut Transform, &ActionState), - // Must be a `Player` which is either be `ControlledBy` a remote - // client (server-side) or its movement `Predicted` on the client. + (&mut Transform, &mut HeadOrientation, &ActionState), + // Must be a `Player` which is either be `ControlledBy` a + // remote client (server-side) or `Predicted` on the client. (With, Or<(With, With)>), >, ) { - for (mut transform, input) in players.iter_mut() { + for (mut transform, mut orientation, inputs) in players.iter_mut() { + *orientation = inputs.look; + let dt = time.delta_secs(); let mut translation = Vec3::ZERO; - if input.movement != Vec2::ZERO { - let movement = input.movement.clamp_length_max(1.0); + if inputs.movement != Vec2::ZERO { + let rotation = Rot2::radians(-orientation.yaw()); + let movement = rotation * inputs.movement.clamp_length_max(1.0); translation.x += movement.x; translation.z += movement.y; } - if input.up { + if inputs.up { translation.y += 1.0; } - if input.down { + if inputs.down { translation.y -= 1.0; } + transform.translation += translation * MOVEMENT_SPEED * dt; + + // TODO: Should affect camera/head rotation only, not the entire player. + transform.rotation = orientation.to_quat(); } }