diff --git a/Cargo.toml b/Cargo.toml index d7304f7..56a85d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,4 +20,4 @@ thiserror = "2.0.17" [workspace.dependencies.lightyear] git = "https://github.com/cBournhonesque/lightyear.git" rev = "f8583d6630a72bdb8bebb4083a8f3f9cc20aecaa" -features = [ "raw_connection", "webtransport" ] +features = [ "raw_connection", "webtransport", "input_native" ] diff --git a/client/src/camera.rs b/client/src/camera.rs index b3c6f35..ca22da0 100644 --- a/client/src/camera.rs +++ b/client/src/camera.rs @@ -107,74 +107,6 @@ pub fn camera_look( transform.rotation = Quat::from_euler(EulerRot::ZYX, 0.0, look.yaw, look.pitch); } -// TODO: Make it possible to attach this to any entity, such as the player, -// with the camera being a child. This will require the noclip system -// to know which entity is the camera. - -#[derive(Component, Debug)] -pub struct CameraNoClip { - /// The speed at which the camera moves. - pub speed: f32, - - /// [`KeyCode`] for forward (relative to camera) translation. - pub key_forward: KeyCode, - /// [`KeyCode`] for backward (relative to camera) translation. - pub key_back: KeyCode, - /// [`KeyCode`] for right (relative to camera) translation. - pub key_right: KeyCode, - /// [`KeyCode`] for left (relative to camera) translation. - pub key_left: KeyCode, - /// [`KeyCode`] for up (global +Y) translation. - pub key_up: KeyCode, - /// [`KeyCode`] for down (global -Y) translation. - pub key_down: KeyCode, -} - -impl Default for CameraNoClip { - fn default() -> Self { - Self { - speed: 5.0, - key_forward: KeyCode::KeyW, - key_back: KeyCode::KeyS, - key_right: KeyCode::KeyD, - key_left: KeyCode::KeyA, - key_up: KeyCode::Space, - key_down: KeyCode::ShiftLeft, - } - } -} - -pub fn noclip_controller( - time: Res>, - key_input: Res>, - camera: Single<(&mut Transform, &CameraNoClip)>, -) { - let (mut transform, noclip) = camera.into_inner(); - - #[rustfmt::skip] - let movement = { - let mut movement = Vec3::ZERO; - if key_input.pressed(noclip.key_forward) { movement.z += 1.0; } - if key_input.pressed(noclip.key_back ) { movement.z -= 1.0; } - if key_input.pressed(noclip.key_right ) { movement.x += 1.0; } - if key_input.pressed(noclip.key_left ) { movement.x -= 1.0; } - movement = movement.clamp_length_max(1.0); - // Movement along the Y (up/down) axis shouldn't be clamped. - if key_input.pressed(noclip.key_up ) { movement.y += 1.0; } - if key_input.pressed(noclip.key_down) { movement.y -= 1.0; } - movement * noclip.speed - }; - - if movement != Vec3::ZERO { - let dt = time.delta_secs(); - let forward = transform.forward(); - let right = transform.right(); - transform.translation += movement.x * dt * right; - transform.translation += movement.y * dt * Vec3::Y; - transform.translation += movement.z * dt * forward; - } -} - #[derive(Component)] pub struct Crosshair; diff --git a/client/src/input.rs b/client/src/input.rs new file mode 100644 index 0000000..3f08e08 --- /dev/null +++ b/client/src/input.rs @@ -0,0 +1,41 @@ +use bevy::prelude::*; +use lightyear::prelude::input::client::*; +use lightyear::prelude::input::native::*; + +use bevy::window::{CursorGrabMode, CursorOptions}; + +use crate::camera::CameraFreeLook; +use common::network::Inputs; + +pub fn plugin(app: &mut App) { + app.add_systems( + FixedPreUpdate, + buffer_input.in_set(InputSystems::WriteClientInputs), + ); +} + +fn buffer_input( + key_input: Res>, + mut action_state: Single<&mut ActionState, 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() + } else { + Inputs { + #[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) + }, + up: key_input.pressed(KeyCode::Space), + down: key_input.pressed(KeyCode::ShiftLeft), + } + }; +} diff --git a/client/src/main.rs b/client/src/main.rs index e6e760e..4a498ae 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,14 +1,17 @@ use bevy::prelude::*; use common::prelude::network::*; use common::prelude::*; +use lightyear::prelude::*; use bevy::asset::AssetMetaCheck; use bevy::window::WindowResolution; +use lightyear::prelude::input::native::InputMarker; use lightyear::prelude::server::Started; mod args; mod block_assets; mod camera; +mod input; mod loading; mod placement; @@ -55,6 +58,7 @@ fn main() -> Result { common::asset_loading::plugin, block_assets::plugin, loading::plugin, + input::plugin, )); app.insert_resource(Args::parse()); @@ -72,9 +76,6 @@ fn main() -> Result { cursor_grab, update_crosshair_visibility.after(cursor_grab), camera_look.after(cursor_grab).run_if(is_cursor_grabbed), - noclip_controller - .after(camera_look) - .run_if(is_cursor_grabbed), ) .run_if(in_state(Screen::Gameplay)), ); @@ -88,8 +89,8 @@ fn main() -> Result { .run_if(in_state(Screen::Gameplay).and(is_cursor_grabbed)), ); - // TODO: Move this to a more general world generation module. app.add_observer(spawn_initial_blocks); + app.add_observer(setup_player); app.run(); Ok(()) @@ -103,21 +104,6 @@ fn setup_scene(mut commands: Commands) { }, Transform::from_xyz(4.0, 8.0, 4.0), )); - - commands.spawn(( - Camera3d::default(), - Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), - CameraFreeLook::default(), - CameraNoClip::default(), - )); -} - -fn spawn_initial_blocks(_event: On, mut blocks: Blocks) { - for x in -8..8 { - for z in -8..8 { - blocks.spawn(IVec3::new(x, 0, z)); - } - } } fn start_server_or_connect(args: Res, mut commands: Commands) -> Result { @@ -139,3 +125,23 @@ fn start_server_or_connect(args: Res, mut commands: Commands) -> Result { // connected to it thanks to the `autoconnect_host_client` observer. Ok(()) } + +/// When the server is started, spawn the initial blocks the world is made of. +fn spawn_initial_blocks(_event: On, mut blocks: Blocks) { + for x in -8..8 { + for z in -8..8 { + blocks.spawn(IVec3::new(x, 0, z)); + } + } +} + +/// When the player we control is being spawned, insert necessary components. +fn setup_player(event: On, mut commands: Commands) { + let player = event.entity; + commands + .entity(player) + // Handle inputs on this entity. + .insert(InputMarker::::default()) + // Add a camera that can be freely rotated. + .with_child((Camera3d::default(), CameraFreeLook::default())); +} diff --git a/common/src/lib.rs b/common/src/lib.rs index 5c91849..6a57afa 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,11 +1,15 @@ pub mod asset_loading; pub mod block; pub mod network; +pub mod player; -// This is mostly just re-exportings things for now, but in the future -// we might want to limit what gets exposed by the `prelude` module. +// This is mostly just re-exporting everything for now, but in the future, +// we might want to select exactly what's exposed by the `prelude` module. pub mod prelude { - pub use super::asset_loading::LoadResource; - pub use super::block::*; pub use super::network; + + pub use super::block::*; + pub use super::player::*; + + pub use super::asset_loading::LoadResource; } diff --git a/common/src/network/protocol.rs b/common/src/network/protocol.rs index aab94d5..287f5ea 100644 --- a/common/src/network/protocol.rs +++ b/common/src/network/protocol.rs @@ -1,13 +1,32 @@ use bevy::prelude::*; use lightyear::prelude::*; +use bevy::ecs::entity::MapEntities; +use lightyear::input::native::plugin::InputPlugin; +use serde::{Deserialize, Serialize}; + use crate::block::Block; +use crate::player::Player; + +#[derive(Default, Deserialize, Serialize, Reflect, Clone, PartialEq, Debug)] +pub struct Inputs { + pub movement: Vec2, + pub up: bool, + pub down: bool, +} + +impl MapEntities for Inputs { + fn map_entities(&mut self, _entity_mapper: &mut E) {} +} pub struct ProtocolPlugin; impl Plugin for ProtocolPlugin { fn build(&self, app: &mut App) { + app.add_plugins(InputPlugin::::default()); + app.register_component::(); + app.register_component::(); app.register_component::(); } } diff --git a/common/src/network/server.rs b/common/src/network/server.rs index 91db614..470ded7 100644 --- a/common/src/network/server.rs +++ b/common/src/network/server.rs @@ -1,7 +1,11 @@ use bevy::prelude::*; +use lightyear::prelude::input::native::*; use lightyear::prelude::server::*; use lightyear::prelude::*; +use crate::network::Inputs; +use crate::player::Player; + #[cfg(not(target_family = "wasm"))] pub use super::server_webtransport::StartWebTransportServerCommand; @@ -24,6 +28,8 @@ impl Plugin for ServerPlugin { #[cfg(not(target_family = "wasm"))] app.add_observer(super::server_webtransport::print_certificate_digest); + + app.add_systems(FixedUpdate, crate::player::server_movement); } } @@ -60,7 +66,6 @@ fn handle_client_connected( let Ok(LinkOf { server }) = clients.get(client) else { return; // Not a client of the server. (client-side?) }; - info!("Client '{client}' connected to server '{server}'"); commands @@ -68,7 +73,16 @@ fn handle_client_connected( .insert_if_new(Name::from("RemoteClient")) .insert(ReplicationSender::default()); - // TODO: Spawn player entity. + commands.spawn(( + Player, + Name::from("Player"), + Replicate::to_clients(NetworkTarget::All), + ActionState::::default(), + ControlledBy { + owner: client, + lifetime: Lifetime::SessionBased, + }, + )); } fn handle_client_disconnected(event: On, clients: Query<&LinkOf>) { @@ -76,8 +90,5 @@ fn handle_client_disconnected(event: On, clients: Query<&Link let Ok(LinkOf { server }) = clients.get(client) else { return; // Not a client of the server. (client-side?) }; - info!("Client '{client}' disconnected from server '{server}'"); - - // TODO: Despawn player entity. } diff --git a/common/src/player.rs b/common/src/player.rs new file mode 100644 index 0000000..52f7f16 --- /dev/null +++ b/common/src/player.rs @@ -0,0 +1,38 @@ +use bevy::prelude::*; + +use lightyear::prelude::input::native::ActionState; +use serde::{Deserialize, Serialize}; + +use crate::network::Inputs; + +const MOVEMENT_SPEED: f32 = 5.0; + +#[derive(Component, PartialEq, Deserialize, Serialize)] +#[require(Transform)] +pub struct Player; + +pub fn server_movement( + time: Res>, + mut players: Query<(&mut Transform, &ActionState)>, +) { + for (mut transform, inputs) in players.iter_mut() { + shared_movement(&mut transform, *time, inputs); + } +} + +pub fn shared_movement(transform: &mut Transform, time: Time, input: &Inputs) { + let dt = time.delta_secs(); + let mut translation = Vec3::ZERO; + if input.movement != Vec2::ZERO { + let movement = input.movement.clamp_length_max(1.0); + translation.x += movement.x; + translation.z += movement.y; + } + if input.up { + translation.y += 1.0; + } + if input.down { + translation.y -= 1.0; + } + transform.translation += translation * MOVEMENT_SPEED * dt; +}