Replicate player entity with input

- Camera is added to player on spawn
- Input is being processed server-side
main
copygirl 1 month ago
parent 04c63abee2
commit 90b255ed3c
  1. 2
      Cargo.toml
  2. 68
      client/src/camera.rs
  3. 41
      client/src/input.rs
  4. 44
      client/src/main.rs
  5. 12
      common/src/lib.rs
  6. 19
      common/src/network/protocol.rs
  7. 21
      common/src/network/server.rs
  8. 38
      common/src/player.rs

@ -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" ]

@ -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<Time<Real>>,
key_input: Res<ButtonInput<KeyCode>>,
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;

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

@ -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<Add, Started>, 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<Args>, mut commands: Commands) -> Result {
@ -139,3 +125,23 @@ fn start_server_or_connect(args: Res<Args>, 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<Add, Started>, 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<Add, Controlled>, mut commands: Commands) {
let player = event.entity;
commands
.entity(player)
// Handle inputs on this entity.
.insert(InputMarker::<Inputs>::default())
// Add a camera that can be freely rotated.
.with_child((Camera3d::default(), CameraFreeLook::default()));
}

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

@ -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<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.register_component::<Transform>();
app.register_component::<Player>();
app.register_component::<Block>();
}
}

@ -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::<Inputs>::default(),
ControlledBy {
owner: client,
lifetime: Lifetime::SessionBased,
},
));
}
fn handle_client_disconnected(event: On<Remove, Connected>, clients: Query<&LinkOf>) {
@ -76,8 +90,5 @@ fn handle_client_disconnected(event: On<Remove, Connected>, 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.
}

@ -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<Time<Fixed>>,
mut players: Query<(&mut Transform, &ActionState<Inputs>)>,
) {
for (mut transform, inputs) in players.iter_mut() {
shared_movement(&mut transform, *time, inputs);
}
}
pub fn shared_movement(transform: &mut Transform, time: Time<Fixed>, 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;
}
Loading…
Cancel
Save