Sync client-to-server block changes

main
copygirl 3 weeks ago
parent b67e9191ee
commit 7c235d6d61
  1. 2
      client/src/assets/block.rs
  2. 49
      client/src/input/client_inputs.rs
  3. 14
      client/src/main.rs
  4. 46
      client/src/placement.rs
  5. 83
      common/src/block.rs
  6. 5
      common/src/network/client_webtransport.rs
  7. 11
      common/src/network/protocol.rs
  8. 31
      common/src/player.rs

@ -27,8 +27,8 @@ impl FromWorld for BlockAssets {
fn insert_block_visuals( fn insert_block_visuals(
event: On<Add, Block>, event: On<Add, Block>,
mut commands: Commands,
block_assets: Res<BlockAssets>, block_assets: Res<BlockAssets>,
mut commands: Commands,
) { ) {
let block = event.entity; let block = event.entity;
commands.entity(block).insert(( commands.entity(block).insert((

@ -11,7 +11,7 @@ use bevy::window::{CursorGrabMode, CursorOptions};
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_systems( app.add_systems(
FixedPreUpdate, FixedPreUpdate,
buffer_input.in_set(InputSystems::WriteClientInputs), (buffer_input, buffer_action).in_set(InputSystems::WriteClientInputs),
); );
} }
@ -43,3 +43,50 @@ fn buffer_input(
} }
} }
} }
// TODO: Use picking system instead of manually raycasting.
pub fn buffer_action(
buttons: Res<ButtonInput<MouseButton>>,
mut player: Single<&mut ActionState<Action>, With<InputMarker<Action>>>,
cursor: Single<&CursorOptions>,
window: Single<(&Window, &CursorOptions)>,
camera: Single<(&GlobalTransform, &Camera)>,
mut ray_cast: MeshRayCast,
blocks: Blocks,
) {
player.0 = Action::None;
if cursor.grab_mode == CursorGrabMode::None {
return;
}
if !buttons.any_just_pressed([MouseButton::Right, MouseButton::Left]) {
return;
}
let (window, cursor) = window.into_inner();
let (cam_transform, camera) = camera.into_inner();
let ray = if cursor.grab_mode == CursorGrabMode::Locked {
Ray3d::new(cam_transform.translation(), cam_transform.forward())
} else if let Some(cursor_pos) = window.cursor_position() {
camera.viewport_to_world(cam_transform, cursor_pos).unwrap()
} else {
return; // cursor outside window area
};
let settings = MeshRayCastSettings::default();
let Some((block, hit)) = ray_cast.cast_ray(ray, &settings).first() else {
return; // ray didn't hit anything
};
let Some(block_pos) = blocks.position(*block) else {
return; // entity hit is not a block
};
player.0 = if buttons.just_pressed(MouseButton::Right) {
// FIXME: This only works for axis-aligned normals.
let offset = hit.normal.normalize().round().as_ivec3();
Action::PlaceBlock(block_pos + offset)
} else {
Action::BreakBlock(block_pos)
};
}

@ -11,13 +11,9 @@ use lightyear::prelude::server::Started;
mod args; mod args;
mod assets; mod assets;
mod input; mod input;
mod placement;
mod ui; mod ui;
use args::*; use args::*;
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 {
@ -67,15 +63,6 @@ fn main() -> Result {
(setup_scene, start_server_or_connect), (setup_scene, start_server_or_connect),
); );
// `place_break_blocks` requires the camera's `GlobalTransform`.
// For a most up-to-date value, run it after that's been updated.
app.add_systems(
PostUpdate,
place_break_blocks
.after(TransformSystems::Propagate)
.run_if(in_state(Screen::Gameplay).and(is_cursor_grabbed)),
);
app.add_observer(spawn_initial_blocks); app.add_observer(spawn_initial_blocks);
app.add_observer(handle_predicted_player_spawn); app.add_observer(handle_predicted_player_spawn);
@ -129,6 +116,7 @@ fn handle_predicted_player_spawn(event: On<Add, Predicted>, mut commands: Comman
commands.entity(player).insert(( commands.entity(player).insert((
// Handle inputs on this entity. // Handle inputs on this entity.
InputMarker::<Inputs>::default(), InputMarker::<Inputs>::default(),
InputMarker::<Action>::default(),
// TODO: Attach camera to player head eventually. // TODO: Attach camera to player head eventually.
Camera3d::default(), Camera3d::default(),
)); ));

@ -1,46 +0,0 @@
use bevy::prelude::*;
use common::prelude::*;
use bevy::window::{CursorGrabMode, CursorOptions};
// TODO: Use picking system instead of manually raycasting.
pub fn place_break_blocks(
mut commands: Commands,
mut blocks: Blocks,
mut ray_cast: MeshRayCast,
mouse_button_input: Res<ButtonInput<MouseButton>>,
window: Single<(&Window, &CursorOptions)>,
camera: Single<(&GlobalTransform, &Camera)>,
) {
let (window, cursor) = window.into_inner();
let (cam_transform, camera) = camera.into_inner();
let ray = if cursor.grab_mode == CursorGrabMode::Locked {
Ray3d::new(cam_transform.translation(), cam_transform.forward())
} else if let Some(cursor_pos) = window.cursor_position() {
camera.viewport_to_world(cam_transform, cursor_pos).unwrap()
} else {
return; // cursor outside window area
};
let settings = &MeshRayCastSettings::default();
let Some((block, hit)) = ray_cast.cast_ray(ray, settings).first() else {
return; // ray didn't hit anything
};
let Some(block_pos) = blocks.position(*block) else {
return; // entity hit is not a block
};
if mouse_button_input.just_pressed(MouseButton::Left) {
// Destroy the block clicked.
commands.entity(*block).despawn();
} else if mouse_button_input.just_pressed(MouseButton::Right) {
// Create a new block next to the one that was just clicked.
// FIXME: This only works for axis-aligned normals.
let offset = hit.normal.normalize().round().as_ivec3();
blocks.spawn(block_pos + offset);
}
}

@ -2,29 +2,88 @@ use bevy::prelude::*;
use lightyear::prelude::*; use lightyear::prelude::*;
use bevy::ecs::system::SystemParam; use bevy::ecs::system::SystemParam;
use bevy::platform::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Component, PartialEq, Deserialize, Serialize)] pub(crate) fn plugin(app: &mut App) {
pub struct Block; app.init_resource::<BlockMap>();
app.add_observer(block_added);
app.add_observer(block_removed);
}
#[derive(Component, Deserialize, Serialize, PartialEq)]
pub struct Block(pub IVec3);
#[derive(Resource, Default)]
pub struct BlockMap(HashMap<IVec3, Entity>);
#[derive(SystemParam)] #[derive(SystemParam)]
pub struct Blocks<'w, 's> { pub struct Blocks<'w, 's> {
map: Res<'w, BlockMap>,
blocks: Query<'w, 's, &'static Block>,
commands: Commands<'w, 's>, commands: Commands<'w, 's>,
blocks: Query<'w, 's, &'static Transform, With<Block>>,
} }
impl Blocks<'_, '_> { impl Blocks<'_, '_> {
pub fn spawn(&mut self, pos: IVec3) { /// Gets the block [`Entity`] at the given position, if any.
self.commands.spawn(( pub fn get(&self, pos: IVec3) -> Option<Entity> {
Block, self.map.0.get(&pos).copied()
Transform::from_translation(pos.as_vec3() + Vec3::ONE / 2.), }
Replicate::to_clients(NetworkTarget::All),
)); /// Gets an [`EntityCommands`] for the block at the given position, if any.
pub fn entity(&mut self, pos: IVec3) -> Option<EntityCommands> {
self.get(pos).map(|block| self.commands.entity(block))
}
/// Spawns a block at the given position.
pub fn spawn(&mut self, pos: IVec3) -> EntityCommands {
self.commands.spawn(Block(pos))
}
/// Tries to spawn a block at the given position, as long as there isn't one already.
/// If there already is a block, its [`Entity`] is returned as the `Err` variant.
pub fn try_spawn(&mut self, pos: IVec3) -> Result<EntityCommands, Entity> {
if let Some(existing) = self.get(pos) {
Err(existing)
} else {
Ok(self.spawn(pos))
}
} }
/// Gets the position of a block entity, or `None` if not a block. /// Despawns the block at the given position, returning if successful.
pub fn despawn(&mut self, pos: IVec3) -> bool {
self.entity(pos).map(|mut block| block.despawn()).is_some()
}
/// Gets the position of the given block [`Entity`], if it exists and is a [`Block`].
pub fn position(&self, entity: Entity) -> Option<IVec3> { pub fn position(&self, entity: Entity) -> Option<IVec3> {
let transform = self.blocks.get(entity).ok(); self.blocks.get(entity).ok().map(|block| block.0)
transform.map(|t| t.translation.floor().as_ivec3()) }
}
fn block_added(
event: On<Add, Block>,
blocks: Query<&Block>,
mut map: ResMut<BlockMap>,
server: Option<Single<&Server>>,
mut commands: Commands,
) {
let block = event.entity;
let pos = blocks.get(block).unwrap().0;
if map.0.insert(pos, block).is_some() {
// TODO: This IS going to happen occasionally.
warn!("Duplicate block at pos {pos}");
} }
let mut entity = commands.entity(block);
entity.insert(Transform::from_translation(pos.as_vec3() + Vec3::ONE / 2.));
if server.is_some() {
entity.insert(Replicate::to_clients(NetworkTarget::All));
}
}
fn block_removed(event: On<Remove, Block>, blocks: Query<&Block>, mut map: ResMut<BlockMap>) {
let block = event.entity;
let pos = blocks.get(block).unwrap().0;
map.0.remove(&pos);
} }

@ -28,9 +28,12 @@ impl Command for ConnectWebTransportCommand {
.spawn(( .spawn((
Client::default(), Client::default(),
Name::from("Client"), Name::from("Client"),
// configuration
ReplicationReceiver::default(),
PredictionManager::default(),
// connection
LocalAddr(client_addr), LocalAddr(client_addr),
PeerAddr(self.server_addr), PeerAddr(self.server_addr),
ReplicationReceiver::default(),
WebTransportClientIo { certificate_digest }, WebTransportClientIo { certificate_digest },
RawClient, RawClient,
)) ))

@ -9,7 +9,9 @@ use crate::block::Block;
use crate::player::{HeadOrientation, Player}; use crate::player::{HeadOrientation, Player};
pub(super) fn plugin(app: &mut App) { pub(super) fn plugin(app: &mut App) {
app.add_plugins(crate::block::plugin);
app.add_plugins(InputPlugin::<Inputs>::default()); app.add_plugins(InputPlugin::<Inputs>::default());
app.add_plugins(InputPlugin::<Action>::default());
// marker components // marker components
app.register_component::<Player>(); app.register_component::<Player>();
@ -21,6 +23,7 @@ pub(super) fn plugin(app: &mut App) {
// unified update systems // unified update systems
app.add_systems(FixedUpdate, crate::player::movement); app.add_systems(FixedUpdate, crate::player::movement);
app.add_systems(FixedUpdate, crate::player::placement);
} }
#[derive(Default, MapEntities, Deserialize, Serialize, Reflect, Clone, PartialEq, Debug)] #[derive(Default, MapEntities, Deserialize, Serialize, Reflect, Clone, PartialEq, Debug)]
@ -31,6 +34,14 @@ pub struct Inputs {
pub down: bool, pub down: bool,
} }
#[derive(Default, MapEntities, Deserialize, Serialize, Reflect, Clone, PartialEq, Debug)]
pub enum Action {
#[default]
None,
PlaceBlock(IVec3),
BreakBlock(IVec3),
}
fn interpolate_transform(start: Transform, other: Transform, t: f32) -> Transform { fn interpolate_transform(start: Transform, other: Transform, t: f32) -> Transform {
Transform { Transform {
translation: start.translation.lerp(other.translation, t), translation: start.translation.lerp(other.translation, t),

@ -6,7 +6,8 @@ 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::protocol::Inputs; use crate::block::Blocks;
use crate::network::protocol::{Action, Inputs};
const MOVEMENT_SPEED: f32 = 5.0; const MOVEMENT_SPEED: f32 = 5.0;
const PITCH_LIMIT: f32 = TAU / 4.0; const PITCH_LIMIT: f32 = TAU / 4.0;
@ -93,3 +94,31 @@ pub fn movement(
transform.rotation = orientation.to_quat(); transform.rotation = orientation.to_quat();
} }
} }
pub fn placement(
players: Query<
&mut ActionState<Action>,
(With<Player>, Or<(With<ControlledBy>, With<Predicted>)>),
>,
mut blocks: Blocks,
) {
for mut action in players {
match **action {
Action::None => {
// do nothing
}
Action::PlaceBlock(pos) => {
if let Ok(mut block) = blocks.try_spawn(pos) {
// TODO: Use `default_with_salt` with the player's `client_id`?
block.insert(PreSpawned::default());
}
}
Action::BreakBlock(pos) => {
blocks.despawn(pos);
}
}
// HACK: Ensure an action will only run once?
// Probably won't be relevant once we move to BEI.
action.0 = Action::None;
}
}

Loading…
Cancel
Save