diff --git a/client/src/block_assets.rs b/client/src/assets/block.rs similarity index 81% rename from client/src/block_assets.rs rename to client/src/assets/block.rs index 82ab4b2..e0c31fa 100644 --- a/client/src/block_assets.rs +++ b/client/src/assets/block.rs @@ -2,8 +2,8 @@ use bevy::prelude::*; use common::prelude::*; pub fn plugin(app: &mut App) { - app.add_observer(insert_block_visuals); app.load_resource::(); + app.add_observer(insert_block_visuals); } #[derive(Resource, Asset, Reflect, Clone)] @@ -25,14 +25,13 @@ impl FromWorld for BlockAssets { } } -/// Observer which automatically inserts block visuals (mesh and -/// material) when an entity has the `Block` component added to it. fn insert_block_visuals( - add: On, + event: On, mut commands: Commands, block_assets: Res, ) { - commands.entity(add.entity).insert(( + let block = event.entity; + commands.entity(block).insert(( Mesh3d(block_assets.mesh.clone()), MeshMaterial3d(block_assets.material.clone()), )); diff --git a/client/src/assets/mod.rs b/client/src/assets/mod.rs new file mode 100644 index 0000000..20b1d07 --- /dev/null +++ b/client/src/assets/mod.rs @@ -0,0 +1,13 @@ +//! Common module to handle asset loading and adding visual components +//! (such as [`Mesh`]) to entities when they are spawned from the server. +//! +//! This doesn't mean this is the only place assets are loaded. +//! For example UI related assets may be loaded in `ui` plugins. + +use bevy::prelude::*; + +mod block; + +pub fn plugin(app: &mut App) { + app.add_plugins(block::plugin); +} diff --git a/client/src/camera.rs b/client/src/camera.rs index ca22da0..945b696 100644 --- a/client/src/camera.rs +++ b/client/src/camera.rs @@ -3,58 +3,6 @@ use std::f32::consts::TAU; use bevy::prelude::*; use bevy::input::mouse::AccumulatedMouseMotion; -use bevy::window::{CursorGrabMode, CursorOptions}; - -pub fn cursor_grab( - mut mouse_button_input: ResMut>, - key_input: Res>, - window: Single<(&mut Window, &mut CursorOptions)>, -) { - let (mut window, mut cursor) = window.into_inner(); - - let is_grabbed = cursor.grab_mode != CursorGrabMode::None; - let request_grab = mouse_button_input.any_just_pressed([MouseButton::Left, MouseButton::Right]); - let request_ungrab = !window.focused || key_input.just_pressed(KeyCode::Escape); - - if !is_grabbed && request_grab && !request_ungrab { - cursor.grab_mode = CursorGrabMode::Locked; - - // To prevent other systems (such as `place_break_blocks`) - // from seeing the mouse button inputs, clear the state here. - mouse_button_input.clear(); - } - - if is_grabbed && request_ungrab && !request_grab { - cursor.grab_mode = CursorGrabMode::None; - } - - if is_grabbed && !request_ungrab { - // Set the cursor position to the center of the window, so that when - // it is ungrabbed, it will reappear there. Because `is_grabbed` is - // not updated on grab, this block is delayed by one frame. - // - // On Wayland, since the cursor is locked into place, this only needs - // to be done once. Unfortunately, for some reason this doesn't work - // in the same frame as setting `grab_mode`, and would log an error. - // - // On X11, the cursor can't be locked into place, only confined to the - // window bounds, so we repeatedly move the cursor back to the center - // while it's grabbed. - // - // On the web, the cursor can be locked, but setting its position is - // not supported at all, so this would instead log a bunch of errors. - let center = window.resolution.size() / 2.; - #[cfg(not(target_family = "wasm"))] // skip on web - window.set_cursor_position(Some(center)); - } - - // Keep cursor visbility in sync with `grab_mode`. - cursor.visible = cursor.grab_mode == CursorGrabMode::None; -} - -pub fn is_cursor_grabbed(cursor: Single<&CursorOptions>) -> bool { - cursor.grab_mode != CursorGrabMode::None -} #[derive(Component, Debug)] pub struct CameraFreeLook { @@ -106,43 +54,3 @@ pub fn camera_look( // Override the camera transform's rotation. transform.rotation = Quat::from_euler(EulerRot::ZYX, 0.0, look.yaw, look.pitch); } - -#[derive(Component)] -pub struct Crosshair; - -pub fn setup_crosshair(mut commands: Commands, assets: Res) { - commands.spawn(( - Node { - width: percent(100), - height: percent(100), - align_items: AlignItems::Center, - justify_content: JustifyContent::Center, - ..default() - }, - children![( - Crosshair, - Node { - width: px(64), - height: px(64), - ..default() - }, - ImageNode { - image: assets.load("crosshair.png"), - ..default() - }, - // Hidden by default, because cursor shouldn't be grabbed at startup either. - Visibility::Hidden, - )], - )); -} - -pub fn update_crosshair_visibility( - cursor: Single<&CursorOptions, Changed>, - crosshair: Single<&mut Visibility, With>, -) { - let is_grabbed = cursor.grab_mode != CursorGrabMode::None; - let mut crosshair_visibility = crosshair.into_inner(); - *crosshair_visibility = (!is_grabbed || cursor.visible) - .then_some(Visibility::Hidden) - .unwrap_or_default(); -} diff --git a/client/src/input.rs b/client/src/input/client_inputs.rs similarity index 89% rename from client/src/input.rs rename to client/src/input/client_inputs.rs index 3f08e08..ad4b841 100644 --- a/client/src/input.rs +++ b/client/src/input/client_inputs.rs @@ -1,11 +1,14 @@ +//! Handles client-side input using [`lightyear`], updating the +//! [`ActionState`] of the affected entities (such as the `Player`). + use bevy::prelude::*; +use common::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( diff --git a/client/src/input/cursor_grab.rs b/client/src/input/cursor_grab.rs new file mode 100644 index 0000000..ffe3142 --- /dev/null +++ b/client/src/input/cursor_grab.rs @@ -0,0 +1,63 @@ +use bevy::prelude::*; + +use bevy::window::{CursorGrabMode, CursorOptions}; + +use crate::Screen; + +pub fn plugin(app: &mut App) { + app.add_systems( + PreUpdate, + update_cursor_grab.run_if(in_state(Screen::Gameplay)), + ); +} + +fn update_cursor_grab( + mut mouse_button_input: ResMut>, + key_input: Res>, + window: Single<(&mut Window, &mut CursorOptions)>, +) { + let (mut window, mut cursor) = window.into_inner(); + + let is_grabbed = cursor.grab_mode != CursorGrabMode::None; + let request_grab = mouse_button_input.any_just_pressed([MouseButton::Left, MouseButton::Right]); + let request_ungrab = !window.focused || key_input.just_pressed(KeyCode::Escape); + + if !is_grabbed && request_grab && !request_ungrab { + cursor.grab_mode = CursorGrabMode::Locked; + + // To prevent other systems (such as `place_break_blocks`) + // from seeing the mouse button inputs, clear the state here. + mouse_button_input.clear(); + } + + if is_grabbed && request_ungrab && !request_grab { + cursor.grab_mode = CursorGrabMode::None; + } + + if is_grabbed && !request_ungrab { + // Set the cursor position to the center of the window, so that when + // it is ungrabbed, it will reappear there. Because `is_grabbed` is + // not updated on grab, this block is delayed by one frame. + // + // On Wayland, since the cursor is locked into place, this only needs + // to be done once. Unfortunately, for some reason this doesn't work + // in the same frame as setting `grab_mode`, and would log an error. + // + // On X11, the cursor can't be locked into place, only confined to the + // window bounds, so we repeatedly move the cursor back to the center + // while it's grabbed. + // + // On the web, the cursor can be locked, but setting its position is + // not supported at all, so this would instead log a bunch of errors. + let center = window.resolution.size() / 2.; + #[cfg(not(target_family = "wasm"))] // skip on web + window.set_cursor_position(Some(center)); + } + + // Keep cursor visbility in sync with `grab_mode`. + cursor.visible = cursor.grab_mode == CursorGrabMode::None; +} + +pub fn is_cursor_grabbed(cursor: Single<&CursorOptions>) -> bool { + cursor.grab_mode != CursorGrabMode::None +} diff --git a/client/src/input/mod.rs b/client/src/input/mod.rs new file mode 100644 index 0000000..d3fa23a --- /dev/null +++ b/client/src/input/mod.rs @@ -0,0 +1,10 @@ +use bevy::prelude::*; + +mod client_inputs; +mod cursor_grab; + +pub use cursor_grab::is_cursor_grabbed; + +pub fn plugin(app: &mut App) { + app.add_plugins((client_inputs::plugin, cursor_grab::plugin)); +} diff --git a/client/src/main.rs b/client/src/main.rs index baac5c9..8280f32 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -9,16 +9,18 @@ use lightyear::prelude::input::native::InputMarker; use lightyear::prelude::server::Started; mod args; -mod block_assets; +mod assets; mod camera; mod input; -mod loading; mod placement; +mod ui; use args::*; use camera::*; use placement::*; +use input::is_cursor_grabbed; + #[derive(States, Clone, Copy, PartialEq, Eq, Hash, Debug)] pub enum Screen { Loading, @@ -26,6 +28,7 @@ pub enum Screen { } fn main() -> Result { + let args = Args::parse(); let mut app = App::new(); app.add_plugins( @@ -53,32 +56,23 @@ fn main() -> Result { app.add_plugins(( bevy_fix_cursor_unlock_web::FixPointerUnlockPlugin, - ServerPlugin, - ClientPlugin, common::asset_loading::plugin, - block_assets::plugin, - loading::plugin, + common::network::plugin, + assets::plugin, input::plugin, + ui::plugin, )); - app.insert_resource(Args::parse()); + app.insert_resource(args); app.insert_state(Screen::Loading); app.add_systems( OnEnter(Screen::Gameplay), - (setup_crosshair, setup_scene, start_server_or_connect), + (setup_scene, start_server_or_connect), ); - // TODO: Create and configure `SystemSet`s for gameplay, input, etc. - app.add_systems( - Update, - ( - cursor_grab, - update_crosshair_visibility.after(cursor_grab), - camera_look.after(cursor_grab).run_if(is_cursor_grabbed), - ) - .run_if(in_state(Screen::Gameplay)), - ); + // 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. @@ -90,7 +84,7 @@ fn main() -> Result { ); app.add_observer(spawn_initial_blocks); - app.add_observer(handle_predicted_spawn); + app.add_observer(handle_predicted_player_spawn); app.run(); Ok(()) @@ -136,7 +130,7 @@ fn spawn_initial_blocks(_event: On, mut blocks: Blocks) { } /// When the `Predicted` player we control is being spawned, insert necessary components. -fn handle_predicted_spawn(event: On, mut commands: Commands) { +fn handle_predicted_player_spawn(event: On, mut commands: Commands) { let player = event.entity; commands .entity(player) diff --git a/client/src/ui/crosshair.rs b/client/src/ui/crosshair.rs new file mode 100644 index 0000000..9ba8c53 --- /dev/null +++ b/client/src/ui/crosshair.rs @@ -0,0 +1,68 @@ +use bevy::prelude::*; +use common::prelude::*; + +use bevy::window::{CursorGrabMode, CursorOptions}; + +use crate::Screen; + +pub fn plugin(app: &mut App) { + app.load_resource::(); + app.add_systems(OnEnter(Screen::Gameplay), setup_crosshair); + app.add_systems(Update, update_crosshair_visibility); +} + +#[derive(Resource, Asset, Reflect, Clone)] +#[reflect(Resource)] +pub struct CrosshairAssets { + #[dependency] + image: Handle, +} + +impl FromWorld for CrosshairAssets { + fn from_world(world: &mut World) -> Self { + let assets = world.resource::(); + Self { + image: assets.load("crosshair.png"), + } + } +} + +#[derive(Component)] +pub struct Crosshair; + +fn setup_crosshair(mut commands: Commands, assets: Res) { + commands.spawn(( + Node { + width: percent(100), + height: percent(100), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }, + children![( + Crosshair, + Node { + width: px(64), + height: px(64), + ..default() + }, + ImageNode { + image: assets.image.clone(), + ..default() + }, + // Hidden by default, because cursor shouldn't be grabbed at startup either. + Visibility::Hidden, + )], + )); +} + +fn update_crosshair_visibility( + cursor: Single<&CursorOptions, Changed>, + crosshair: Single<&mut Visibility, With>, +) { + let is_grabbed = cursor.grab_mode != CursorGrabMode::None; + let mut crosshair_visibility = crosshair.into_inner(); + *crosshair_visibility = (!is_grabbed || cursor.visible) + .then_some(Visibility::Hidden) + .unwrap_or_default(); +} diff --git a/client/src/loading.rs b/client/src/ui/loading_screen.rs similarity index 100% rename from client/src/loading.rs rename to client/src/ui/loading_screen.rs diff --git a/client/src/ui/mod.rs b/client/src/ui/mod.rs new file mode 100644 index 0000000..b852000 --- /dev/null +++ b/client/src/ui/mod.rs @@ -0,0 +1,8 @@ +use bevy::prelude::*; + +mod crosshair; +mod loading_screen; + +pub fn plugin(app: &mut App) { + app.add_plugins((crosshair::plugin, loading_screen::plugin)); +} diff --git a/common/src/lib.rs b/common/src/lib.rs index 6a57afa..53ab0ac 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -3,13 +3,13 @@ pub mod block; pub mod network; pub mod player; -// 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::network; + pub use crate::network; - pub use super::block::*; - pub use super::player::*; + pub use crate::block::*; + pub use crate::network::protocol::*; + pub use crate::player::*; - pub use super::asset_loading::LoadResource; + // Allows use of the `App::load_resource` extension trait function. + pub use crate::asset_loading::LoadResource; } diff --git a/common/src/network/client.rs b/common/src/network/client.rs index 5c84add..21eecc7 100644 --- a/common/src/network/client.rs +++ b/common/src/network/client.rs @@ -2,23 +2,13 @@ use bevy::prelude::*; use lightyear::prelude::client::*; use lightyear::prelude::*; -pub use super::client_webtransport::ConnectWebTransportCommand; +pub(super) fn plugin(app: &mut App) { + app.add_plugins(ClientPlugins::default()); -pub struct ClientPlugin; + app.add_observer(on_connected); + app.add_observer(on_disconnected); -impl Plugin for ClientPlugin { - fn build(&self, app: &mut App) { - app.add_plugins(ClientPlugins::default()); - - if !app.is_plugin_added::() { - app.add_plugins(super::ProtocolPlugin); - } - - app.add_observer(on_connected); - app.add_observer(on_disconnected); - - app.add_observer(autoconnect_host_client); - } + app.add_observer(autoconnect_host_client); } /// Automatically creates a "host client" to connect to the local server when it is started. diff --git a/common/src/network/mod.rs b/common/src/network/mod.rs index c621989..1120010 100644 --- a/common/src/network/mod.rs +++ b/common/src/network/mod.rs @@ -1,12 +1,19 @@ -mod client; -mod protocol; -mod server; +use bevy::prelude::*; + +pub(crate) mod protocol; +mod client; mod client_webtransport; +mod server; mod server_webtransport; -pub use client::*; -pub use protocol::*; -pub use server::*; +pub use crate::network::client_webtransport::ConnectWebTransportCommand; +pub use crate::network::server::StartLocalServerCommand; +#[cfg(not(target_family = "wasm"))] +pub use crate::network::server_webtransport::StartWebTransportServerCommand; pub const DEFAULT_PORT: u16 = 13580; + +pub fn plugin(app: &mut App) { + app.add_plugins((protocol::plugin, client::plugin, server::plugin)); +} diff --git a/common/src/network/protocol.rs b/common/src/network/protocol.rs index 4c26c96..40b730c 100644 --- a/common/src/network/protocol.rs +++ b/common/src/network/protocol.rs @@ -8,6 +8,21 @@ use serde::{Deserialize, Serialize}; use crate::block::Block; use crate::player::Player; +pub(super) fn plugin(app: &mut App) { + app.add_plugins(InputPlugin::::default()); + + // marker components + app.register_component::(); + app.register_component::(); + + app.register_component::() + .add_prediction() + .add_interpolation_with(interpolate_transform); + + // unified update systems + app.add_systems(FixedUpdate, crate::player::movement); +} + #[derive(Default, Deserialize, Serialize, Reflect, Clone, PartialEq, Debug)] pub struct Inputs { pub movement: Vec2, @@ -19,25 +34,6 @@ 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()); - - // marker components - app.register_component::(); - app.register_component::(); - - app.register_component::() - .add_prediction() - .add_interpolation_with(interpolate_transform); - - // unified update systems - app.add_systems(FixedUpdate, crate::player::movement); - } -} - fn interpolate_transform(start: Transform, other: Transform, t: f32) -> Transform { Transform { translation: start.translation.lerp(other.translation, t), diff --git a/common/src/network/server.rs b/common/src/network/server.rs index 58b26b5..41eeb78 100644 --- a/common/src/network/server.rs +++ b/common/src/network/server.rs @@ -4,29 +4,18 @@ use lightyear::prelude::*; use crate::player::Player; -#[cfg(not(target_family = "wasm"))] -pub use super::server_webtransport::StartWebTransportServerCommand; +pub(super) fn plugin(app: &mut App) { + app.add_plugins(ServerPlugins::default()); -pub struct ServerPlugin; + // TODO: See what happens when we try to start a server, but it + // can't, for example due to the port already being in use. + app.add_observer(on_server_started); + app.add_observer(on_server_stopped); + app.add_observer(handle_client_connected); + app.add_observer(handle_client_disconnected); -impl Plugin for ServerPlugin { - fn build(&self, app: &mut App) { - app.add_plugins(ServerPlugins::default()); - - if !app.is_plugin_added::() { - app.add_plugins(super::ProtocolPlugin); - } - - // TODO: See what happens when we try to start a server, but it - // can't, for example due to the port already being in use. - app.add_observer(on_server_started); - app.add_observer(on_server_stopped); - app.add_observer(handle_client_connected); - app.add_observer(handle_client_disconnected); - - #[cfg(not(target_family = "wasm"))] - app.add_observer(super::server_webtransport::print_certificate_digest); - } + #[cfg(not(target_family = "wasm"))] + app.add_observer(super::server_webtransport::print_certificate_digest); } /// Starts a local-only server that only a host-client can connect to. diff --git a/common/src/player.rs b/common/src/player.rs index 4e24d58..bace3e3 100644 --- a/common/src/player.rs +++ b/common/src/player.rs @@ -4,7 +4,7 @@ use lightyear::prelude::*; use lightyear::prelude::input::native::ActionState; use serde::{Deserialize, Serialize}; -use crate::network::Inputs; +use crate::network::protocol::Inputs; const MOVEMENT_SPEED: f32 = 5.0;