Compare commits
2 Commits
138a218fc5
...
1458958ba2
| Author | SHA1 | Date |
|---|---|---|
|
|
1458958ba2 | 3 weeks ago |
|
|
df8cc62ece | 4 weeks ago |
19 changed files with 422 additions and 303 deletions
@ -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, |
||||||
|
)); |
||||||
|
} |
||||||
@ -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)); |
||||||
|
} |
||||||
@ -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)); |
||||||
|
} |
||||||
|
|||||||
@ -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…
Reference in new issue