Compare commits

...

2 Commits

  1. 9
      client/src/assets/block.rs
  2. 15
      client/src/assets/mod.rs
  3. 38
      client/src/assets/player.rs
  4. 148
      client/src/camera.rs
  5. 41
      client/src/input.rs
  6. 45
      client/src/input/client_inputs.rs
  7. 63
      client/src/input/cursor_grab.rs
  8. 30
      client/src/input/head_orientation.rs
  9. 15
      client/src/input/mod.rs
  10. 49
      client/src/main.rs
  11. 68
      client/src/ui/crosshair.rs
  12. 0
      client/src/ui/loading_screen.rs
  13. 8
      client/src/ui/mod.rs
  14. 12
      common/src/lib.rs
  15. 12
      common/src/network/client.rs
  16. 19
      common/src/network/mod.rs
  17. 31
      common/src/network/protocol.rs
  18. 13
      common/src/network/server.rs
  19. 77
      common/src/player.rs

@ -2,8 +2,8 @@ use bevy::prelude::*;
use common::prelude::*; use common::prelude::*;
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_observer(insert_block_visuals);
app.load_resource::<BlockAssets>(); app.load_resource::<BlockAssets>();
app.add_observer(insert_block_visuals);
} }
#[derive(Resource, Asset, Reflect, Clone)] #[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( fn insert_block_visuals(
add: On<Add, Block>, event: On<Add, Block>,
mut commands: Commands, mut commands: Commands,
block_assets: Res<BlockAssets>, block_assets: Res<BlockAssets>,
) { ) {
commands.entity(add.entity).insert(( let block = event.entity;
commands.entity(block).insert((
Mesh3d(block_assets.mesh.clone()), Mesh3d(block_assets.mesh.clone()),
MeshMaterial3d(block_assets.material.clone()), MeshMaterial3d(block_assets.material.clone()),
)); ));

@ -0,0 +1,15 @@
//! 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;
mod player;
pub fn plugin(app: &mut App) {
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,148 +0,0 @@
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<ButtonInput<MouseButton>>,
key_input: Res<ButtonInput<KeyCode>>,
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 {
/// 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);
}
#[derive(Component)]
pub struct Crosshair;
pub fn setup_crosshair(mut commands: Commands, assets: Res<AssetServer>) {
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<CursorOptions>>,
crosshair: Single<&mut Visibility, With<Crosshair>>,
) {
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();
}

@ -1,41 +0,0 @@
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<ButtonInput<KeyCode>>,
mut action_state: Single<&mut ActionState<Inputs>, With<InputMarker<Inputs>>>,
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),
}
};
}

@ -0,0 +1,45 @@
//! 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};
pub fn plugin(app: &mut App) {
app.add_systems(
FixedPreUpdate,
buffer_input.in_set(InputSystems::WriteClientInputs),
);
}
fn buffer_input(
keys: Res<ButtonInput<KeyCode>>,
player: Single<(&mut ActionState<Inputs>, &HeadOrientation), With<InputMarker<Inputs>>>,
cursor: Single<&CursorOptions>,
) {
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 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: keys.pressed(KeyCode::Space),
down: keys.pressed(KeyCode::ShiftLeft),
}
}
}

@ -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<ButtonInput<MouseButton>>,
key_input: Res<ButtonInput<KeyCode>>,
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
}

@ -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);
}

@ -0,0 +1,15 @@
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,
head_orientation::plugin,
));
}

@ -9,16 +9,16 @@ use lightyear::prelude::input::native::InputMarker;
use lightyear::prelude::server::Started; use lightyear::prelude::server::Started;
mod args; mod args;
mod block_assets; mod assets;
mod camera;
mod input; mod input;
mod loading;
mod placement; mod placement;
mod ui;
use args::*; use args::*;
use camera::*;
use placement::*; use placement::*;
use input::is_cursor_grabbed;
#[derive(States, Clone, Copy, PartialEq, Eq, Hash, Debug)] #[derive(States, Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum Screen { pub enum Screen {
Loading, Loading,
@ -26,8 +26,12 @@ pub enum Screen {
} }
fn main() -> Result { fn main() -> Result {
let args = Args::parse();
let mut app = App::new(); let mut app = App::new();
app.insert_resource(args);
app.insert_state(Screen::Loading);
app.add_plugins( app.add_plugins(
DefaultPlugins DefaultPlugins
.set(WindowPlugin { .set(WindowPlugin {
@ -53,31 +57,16 @@ fn main() -> Result {
app.add_plugins(( app.add_plugins((
bevy_fix_cursor_unlock_web::FixPointerUnlockPlugin, bevy_fix_cursor_unlock_web::FixPointerUnlockPlugin,
ServerPlugin,
ClientPlugin,
common::asset_loading::plugin, common::asset_loading::plugin,
block_assets::plugin, common::network::plugin,
loading::plugin, assets::plugin,
input::plugin, input::plugin,
ui::plugin,
)); ));
app.insert_resource(Args::parse());
app.insert_state(Screen::Loading);
app.add_systems( app.add_systems(
OnEnter(Screen::Gameplay), 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)),
); );
// `place_break_blocks` requires the camera's `GlobalTransform`. // `place_break_blocks` requires the camera's `GlobalTransform`.
@ -90,7 +79,7 @@ fn main() -> Result {
); );
app.add_observer(spawn_initial_blocks); app.add_observer(spawn_initial_blocks);
app.add_observer(handle_predicted_spawn); app.add_observer(handle_predicted_player_spawn);
app.run(); app.run();
Ok(()) Ok(())
@ -136,12 +125,12 @@ 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_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(),
));
} }

@ -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::<CrosshairAssets>();
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<Image>,
}
impl FromWorld for CrosshairAssets {
fn from_world(world: &mut World) -> Self {
let assets = world.resource::<AssetServer>();
Self {
image: assets.load("crosshair.png"),
}
}
}
#[derive(Component)]
pub struct Crosshair;
fn setup_crosshair(mut commands: Commands, assets: Res<CrosshairAssets>) {
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<CursorOptions>>,
crosshair: Single<&mut Visibility, With<Crosshair>>,
) {
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();
}

@ -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));
}

@ -3,13 +3,13 @@ pub mod block;
pub mod network; pub mod network;
pub mod player; 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 mod prelude {
pub use super::network; pub use crate::network;
pub use super::block::*; pub use crate::block::*;
pub use super::player::*; 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;
} }

@ -2,23 +2,13 @@ use bevy::prelude::*;
use lightyear::prelude::client::*; use lightyear::prelude::client::*;
use lightyear::prelude::*; use lightyear::prelude::*;
pub use super::client_webtransport::ConnectWebTransportCommand; pub(super) fn plugin(app: &mut App) {
pub struct ClientPlugin;
impl Plugin for ClientPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(ClientPlugins::default()); app.add_plugins(ClientPlugins::default());
if !app.is_plugin_added::<super::ProtocolPlugin>() {
app.add_plugins(super::ProtocolPlugin);
}
app.add_observer(on_connected); app.add_observer(on_connected);
app.add_observer(on_disconnected); 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. /// Automatically creates a "host client" to connect to the local server when it is started.

@ -1,12 +1,19 @@
mod client; use bevy::prelude::*;
mod protocol;
mod server; pub(crate) mod protocol;
mod client;
mod client_webtransport; mod client_webtransport;
mod server;
mod server_webtransport; mod server_webtransport;
pub use client::*; pub use crate::network::client_webtransport::ConnectWebTransportCommand;
pub use protocol::*; pub use crate::network::server::StartLocalServerCommand;
pub use server::*; #[cfg(not(target_family = "wasm"))]
pub use crate::network::server_webtransport::StartWebTransportServerCommand;
pub const DEFAULT_PORT: u16 = 13580; pub const DEFAULT_PORT: u16 = 13580;
pub fn plugin(app: &mut App) {
app.add_plugins((protocol::plugin, client::plugin, server::plugin));
}

@ -6,23 +6,9 @@ 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};
#[derive(Default, Deserialize, Serialize, Reflect, Clone, PartialEq, Debug)] pub(super) fn plugin(app: &mut App) {
pub struct Inputs {
pub movement: Vec2,
pub up: bool,
pub down: bool,
}
impl MapEntities for Inputs {
fn map_entities<E: EntityMapper>(&mut self, _entity_mapper: &mut E) {}
}
pub struct ProtocolPlugin;
impl Plugin for ProtocolPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(InputPlugin::<Inputs>::default()); app.add_plugins(InputPlugin::<Inputs>::default());
// marker components // marker components
@ -35,7 +21,18 @@ impl Plugin for ProtocolPlugin {
// unified update systems // unified update systems
app.add_systems(FixedUpdate, crate::player::movement); app.add_systems(FixedUpdate, crate::player::movement);
} }
#[derive(Default, 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<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 {

@ -4,19 +4,9 @@ use lightyear::prelude::*;
use crate::player::Player; use crate::player::Player;
#[cfg(not(target_family = "wasm"))] pub(super) fn plugin(app: &mut App) {
pub use super::server_webtransport::StartWebTransportServerCommand;
pub struct ServerPlugin;
impl Plugin for ServerPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(ServerPlugins::default()); app.add_plugins(ServerPlugins::default());
if !app.is_plugin_added::<super::ProtocolPlugin>() {
app.add_plugins(super::ProtocolPlugin);
}
// TODO: See what happens when we try to start a server, but it // 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. // can't, for example due to the port already being in use.
app.add_observer(on_server_started); app.add_observer(on_server_started);
@ -26,7 +16,6 @@ impl Plugin for ServerPlugin {
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
app.add_observer(super::server_webtransport::print_certificate_digest); app.add_observer(super::server_webtransport::print_certificate_digest);
}
} }
/// Starts a local-only server that only a host-client can connect to. /// Starts a local-only server that only a host-client can connect to.

@ -1,40 +1,95 @@
use std::f32::consts::{PI, TAU};
use bevy::prelude::*; use bevy::prelude::*;
use lightyear::prelude::*; use lightyear::prelude::*;
use lightyear::prelude::input::native::ActionState; use lightyear::prelude::input::native::ActionState;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::network::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