Sync head orientation, add simple player visuals

main
copygirl 3 weeks ago
parent df8cc62ece
commit b67e9191ee
  1. 2
      client/src/assets/mod.rs
  2. 38
      client/src/assets/player.rs
  3. 56
      client/src/camera.rs
  4. 35
      client/src/input/client_inputs.rs
  5. 30
      client/src/input/head_orientation.rs
  6. 7
      client/src/input/mod.rs
  7. 20
      client/src/main.rs
  8. 9
      common/src/network/protocol.rs
  9. 75
      common/src/player.rs

@ -7,7 +7,9 @@
use bevy::prelude::*; use bevy::prelude::*;
mod block; mod block;
mod player;
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_plugins(block::plugin); app.add_plugins(block::plugin);
app.add_plugins(player::plugin);
} }

@ -0,0 +1,38 @@
use bevy::prelude::*;
use common::prelude::*;
pub fn plugin(app: &mut App) {
app.load_resource::<PlayerAssets>();
app.add_observer(insert_player_visuals);
}
#[derive(Resource, Asset, Reflect, Clone)]
#[reflect(Resource)]
pub struct PlayerAssets {
#[dependency]
mesh: Handle<Mesh>,
#[dependency]
material: Handle<StandardMaterial>,
}
impl FromWorld for PlayerAssets {
fn from_world(world: &mut World) -> Self {
let assets = world.resource::<AssetServer>();
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<Add, Player>,
mut commands: Commands,
block_assets: Res<PlayerAssets>,
) {
let player = event.entity;
commands.entity(player).insert((
Mesh3d(block_assets.mesh.clone()),
MeshMaterial3d(block_assets.material.clone()),
));
}

@ -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<f32>,
/// 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<AccumulatedMouseMotion>,
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);
}

@ -1,5 +1,5 @@
//! Handles client-side input using [`lightyear`], updating the //! 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 bevy::prelude::*;
use common::prelude::*; use common::prelude::*;
@ -8,8 +8,6 @@ use lightyear::prelude::input::native::*;
use bevy::window::{CursorGrabMode, CursorOptions}; use bevy::window::{CursorGrabMode, CursorOptions};
use crate::camera::CameraFreeLook;
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_systems( app.add_systems(
FixedPreUpdate, FixedPreUpdate,
@ -18,27 +16,30 @@ pub fn plugin(app: &mut App) {
} }
fn buffer_input( fn buffer_input(
key_input: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
mut action_state: Single<&mut ActionState<Inputs>, With<InputMarker<Inputs>>>, player: Single<(&mut ActionState<Inputs>, &HeadOrientation), With<InputMarker<Inputs>>>,
cursor: Single<&CursorOptions>, cursor: Single<&CursorOptions>,
camera: Single<&CameraFreeLook>,
) { ) {
action_state.0 = if cursor.grab_mode == CursorGrabMode::None { let (mut inputs, orientation) = player.into_inner();
// If cursor is not grabbed, reset movement input. inputs.0 = if cursor.grab_mode == CursorGrabMode::None {
Default::default() Inputs {
look: *orientation,
..default()
}
} else { } else {
Inputs { Inputs {
look: *orientation,
#[rustfmt::skip] #[rustfmt::skip]
movement: { movement: {
let mut movement = Vec2::ZERO; let mut movement = Vec2::ZERO;
if key_input.pressed(KeyCode::KeyD) { movement.x += 1.0; } if keys.pressed(KeyCode::KeyD) { movement.x += 1.0; }
if key_input.pressed(KeyCode::KeyA) { movement.x -= 1.0; } if keys.pressed(KeyCode::KeyA) { movement.x -= 1.0; }
if key_input.pressed(KeyCode::KeyS) { movement.y += 1.0; } if keys.pressed(KeyCode::KeyS) { movement.y += 1.0; }
if key_input.pressed(KeyCode::KeyW) { movement.y -= 1.0; } if keys.pressed(KeyCode::KeyW) { movement.y -= 1.0; }
Rot2::radians(-camera.yaw) * movement.clamp_length_max(1.0) movement.clamp_length_max(1.0)
}, },
up: key_input.pressed(KeyCode::Space), up: keys.pressed(KeyCode::Space),
down: key_input.pressed(KeyCode::ShiftLeft), down: keys.pressed(KeyCode::ShiftLeft),
}
} }
};
} }

@ -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::<MouseSensitivity>();
app.add_systems(Update, update_head_orientation.run_if(is_cursor_grabbed));
}
fn update_head_orientation(
accumulated_mouse_motion: Res<AccumulatedMouseMotion>,
mouse_sensitivity: Res<MouseSensitivity>,
mut orientation: Single<&mut HeadOrientation, With<InputMarker<Inputs>>>,
) {
let delta = accumulated_mouse_motion.delta * **mouse_sensitivity;
**orientation = orientation.update(delta);
}

@ -2,9 +2,14 @@ use bevy::prelude::*;
mod client_inputs; mod client_inputs;
mod cursor_grab; mod cursor_grab;
mod head_orientation;
pub use cursor_grab::is_cursor_grabbed; pub use cursor_grab::is_cursor_grabbed;
pub fn plugin(app: &mut App) { 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,
));
} }

@ -10,13 +10,11 @@ use lightyear::prelude::server::Started;
mod args; mod args;
mod assets; mod assets;
mod camera;
mod input; mod input;
mod placement; mod placement;
mod ui; mod ui;
use args::*; use args::*;
use camera::*;
use placement::*; use placement::*;
use input::is_cursor_grabbed; use input::is_cursor_grabbed;
@ -30,6 +28,7 @@ pub enum Screen {
fn main() -> Result { fn main() -> Result {
let args = Args::parse(); let args = Args::parse();
let mut app = App::new(); let mut app = App::new();
app.insert_resource(args);
app.add_plugins( app.add_plugins(
DefaultPlugins DefaultPlugins
@ -63,17 +62,11 @@ fn main() -> Result {
ui::plugin, ui::plugin,
)); ));
app.insert_resource(args);
app.insert_state(Screen::Loading);
app.add_systems( app.add_systems(
OnEnter(Screen::Gameplay), OnEnter(Screen::Gameplay),
(setup_scene, start_server_or_connect), (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`. // `place_break_blocks` requires the camera's `GlobalTransform`.
// For a most up-to-date value, run it after that's been updated. // For a most up-to-date value, run it after that's been updated.
app.add_systems( app.add_systems(
@ -86,6 +79,7 @@ fn main() -> Result {
app.add_observer(spawn_initial_blocks); app.add_observer(spawn_initial_blocks);
app.add_observer(handle_predicted_player_spawn); app.add_observer(handle_predicted_player_spawn);
app.insert_state(Screen::Loading);
app.run(); app.run();
Ok(()) Ok(())
} }
@ -132,10 +126,10 @@ fn spawn_initial_blocks(_event: On<Add, Started>, mut blocks: Blocks) {
/// When the `Predicted` player we control is being spawned, insert necessary components. /// When the `Predicted` player we control is being spawned, insert necessary components.
fn handle_predicted_player_spawn(event: On<Add, Predicted>, mut commands: Commands) { fn handle_predicted_player_spawn(event: On<Add, Predicted>, mut commands: Commands) {
let player = event.entity; let player = event.entity;
commands commands.entity(player).insert((
.entity(player)
// Handle inputs on this entity. // Handle inputs on this entity.
.insert(InputMarker::<Inputs>::default()) InputMarker::<Inputs>::default(),
// Add a camera that can be freely rotated. // TODO: Attach camera to player head eventually.
.with_child((Camera3d::default(), CameraFreeLook::default())); Camera3d::default(),
));
} }

@ -6,7 +6,7 @@ use lightyear::input::native::plugin::InputPlugin;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::block::Block; use crate::block::Block;
use crate::player::Player; use crate::player::{HeadOrientation, Player};
pub(super) fn plugin(app: &mut App) { pub(super) fn plugin(app: &mut App) {
app.add_plugins(InputPlugin::<Inputs>::default()); app.add_plugins(InputPlugin::<Inputs>::default());
@ -23,17 +23,14 @@ pub(super) fn plugin(app: &mut App) {
app.add_systems(FixedUpdate, crate::player::movement); 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 struct Inputs {
pub look: HeadOrientation,
pub movement: Vec2, pub movement: Vec2,
pub up: bool, pub up: bool,
pub down: bool, pub down: bool,
} }
impl MapEntities for Inputs {
fn map_entities<E: EntityMapper>(&mut self, _entity_mapper: &mut E) {}
}
fn interpolate_transform(start: Transform, other: Transform, t: f32) -> Transform { fn interpolate_transform(start: Transform, other: Transform, t: f32) -> Transform {
Transform { Transform {
translation: start.translation.lerp(other.translation, t), translation: start.translation.lerp(other.translation, t),

@ -1,3 +1,5 @@
use std::f32::consts::{PI, TAU};
use bevy::prelude::*; use bevy::prelude::*;
use lightyear::prelude::*; use lightyear::prelude::*;
@ -7,34 +9,87 @@ use serde::{Deserialize, Serialize};
use crate::network::protocol::Inputs; use crate::network::protocol::Inputs;
const MOVEMENT_SPEED: f32 = 5.0; const MOVEMENT_SPEED: f32 = 5.0;
const PITCH_LIMIT: f32 = TAU / 4.0;
#[derive(Component, PartialEq, Deserialize, Serialize)] #[derive(Component, Deserialize, Serialize, PartialEq)]
#[require(Transform)] #[require(Transform, HeadOrientation)]
pub struct Player; 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<HeadOrientationUnchecked> for HeadOrientation {
fn from(value: HeadOrientationUnchecked) -> Self {
Self::new(value.yaw, value.pitch)
}
}
pub fn movement( pub fn movement(
time: Res<Time<Fixed>>, time: Res<Time<Fixed>>,
mut players: Query< mut players: Query<
(&mut Transform, &ActionState<Inputs>), (&mut Transform, &mut HeadOrientation, &ActionState<Inputs>),
// Must be a `Player` which is either be `ControlledBy` a remote // Must be a `Player` which is either be `ControlledBy` a
// client (server-side) or its movement `Predicted` on the client. // remote client (server-side) or `Predicted` on the client.
(With<Player>, Or<(With<ControlledBy>, With<Predicted>)>), (With<Player>, Or<(With<ControlledBy>, With<Predicted>)>),
>, >,
) { ) {
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 dt = time.delta_secs();
let mut translation = Vec3::ZERO; let mut translation = Vec3::ZERO;
if input.movement != Vec2::ZERO { if inputs.movement != Vec2::ZERO {
let movement = input.movement.clamp_length_max(1.0); let rotation = Rot2::radians(-orientation.yaw());
let movement = rotation * inputs.movement.clamp_length_max(1.0);
translation.x += movement.x; translation.x += movement.x;
translation.z += movement.y; translation.z += movement.y;
} }
if input.up { if inputs.up {
translation.y += 1.0; translation.y += 1.0;
} }
if input.down { if inputs.down {
translation.y -= 1.0; translation.y -= 1.0;
} }
transform.translation += translation * MOVEMENT_SPEED * dt; transform.translation += translation * MOVEMENT_SPEED * dt;
// TODO: Should affect camera/head rotation only, not the entire player.
transform.rotation = orientation.to_quat();
} }
} }

Loading…
Cancel
Save