Compare commits

..

4 Commits

Author SHA1 Message Date
copygirl bb66d28f06 Basic networking using `lightyear` crate 3 days ago
copygirl ff8578fd82 Add `common` package with blocks module 7 days ago
copygirl b6ac56aba4 Convert to using workspace 7 days ago
copygirl 10e5e3523d Fix and refactor cursor grabbing 7 days ago
  1. 3
      .cargo/config.toml
  2. 3069
      Cargo.lock
  3. 19
      Cargo.toml
  4. 13
      client/Cargo.toml
  5. 31
      client/src/block.rs
  6. 59
      client/src/camera.rs
  7. 83
      client/src/main.rs
  8. 0
      client/src/placement.rs
  9. 9
      common/Cargo.toml
  10. 31
      common/src/block.rs
  11. 2
      common/src/lib.rs
  12. 63
      common/src/network/client.rs
  13. 9
      common/src/network/mod.rs
  14. 13
      common/src/network/protocol.rs
  15. 91
      common/src/network/server.rs
  16. 47
      src/block.rs
  17. 55
      src/main.rs

@ -0,0 +1,3 @@
[env]
# Makes Bevy look for the `assets/` directory in the right place.
BEVY_ASSET_ROOT = { value = ".", relative = true }

3069
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,7 +1,6 @@
[package]
name = "bevy-bloxel-classic"
version = "0.1.0"
edition = "2024"
[workspace]
resolver = "3"
members = [ "client", "common" ]
# Enable a small amount of optimization in the dev profile.
[profile.dev]
@ -11,5 +10,13 @@ opt-level = 1
[profile.dev.package."*"]
opt-level = 3
[dependencies]
bevy = "0.17"
[workspace.dependencies]
bevy = { version = "0.17.2", features = [ "serialize" ] }
# lightyear = { version = "0.25.3", features = [ "netcode", "webtransport" ] }
serde = "1.0.228"
# TODO: Once lightyear releases a version with `raw_connection` support, switch to that.
[workspace.dependencies.lightyear]
git = "https://github.com/cBournhonesque/lightyear.git"
rev = "5559dd47a014040f570516983ace2c9e9a25ac89"
features = [ "raw_connection", "webtransport" ]

@ -0,0 +1,13 @@
[package]
name = "bevy-bloxel-classic"
version = "0.1.0"
edition = "2024"
[dependencies]
common = { path = "../common", package = "bevy-bloxel-classic-common" }
bevy.workspace = true
lightyear.workspace = true
serde.workspace = true
bevy_fix_cursor_unlock_web = "0.2.0"

@ -0,0 +1,31 @@
use bevy::prelude::*;
pub use common::block::*;
#[derive(Resource)]
pub struct BlockResources {
mesh: Handle<Mesh>,
material: Handle<StandardMaterial>,
}
pub fn setup_blocks(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.insert_resource(BlockResources {
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
material: materials.add(Color::srgb_u8(124, 144, 255)),
});
}
pub fn add_block_visuals(
add: On<Add, Block>,
mut commands: Commands,
resources: Res<BlockResources>,
) {
commands.entity(add.entity).insert((
Mesh3d(resources.mesh.clone()),
MeshMaterial3d(resources.material.clone()),
));
}

@ -8,47 +8,51 @@ pub fn cursor_grab(
mut mouse_button_input: ResMut<ButtonInput<MouseButton>>,
key_input: Res<ButtonInput<KeyCode>>,
window: Single<(&mut Window, &mut CursorOptions)>,
crosshair: Single<&mut Visibility, With<Crosshair>>,
mut request_center_cursor: Local<bool>,
) {
let (mut window, mut cursor) = window.into_inner();
let mut crosshair_visibility = crosshair.into_inner();
let is_grabbed = cursor.grab_mode == CursorGrabMode::Locked;
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 {
*crosshair_visibility = Visibility::Inherited;
cursor.grab_mode = CursorGrabMode::Locked;
cursor.visible = false;
// HACK: It appears setting the cursor position in the same frame we're
// locking it is not possible, so we're delaying it by a frame.
*request_center_cursor = true;
// 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_center_cursor {
// Set the cursor position to the middle of the window,
// so when it is ungrabbed again it'll reappear there.
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_arch = "wasm32"))] // skip on web
window.set_cursor_position(Some(center));
*request_center_cursor = false; // Only do this once.
}
if is_grabbed && request_ungrab && !request_grab {
*crosshair_visibility = Visibility::Hidden;
cursor.grab_mode = CursorGrabMode::None;
cursor.visible = true;
}
// 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::Locked
cursor.grab_mode != CursorGrabMode::None
}
#[derive(Component, Debug)]
@ -171,7 +175,7 @@ pub fn noclip_controller(
#[derive(Component)]
pub struct Crosshair;
pub fn setup_crosshair(mut commands: Commands, asset_server: Res<AssetServer>) {
pub fn setup_crosshair(mut commands: Commands, assets: Res<AssetServer>) {
commands.spawn((
Node {
width: percent(100),
@ -188,7 +192,7 @@ pub fn setup_crosshair(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
ImageNode {
image: asset_server.load("crosshair.png"),
image: assets.load("crosshair.png"),
..default()
},
// Hidden by default, because cursor shouldn't be grabbed at startup either.
@ -196,3 +200,14 @@ pub fn setup_crosshair(mut commands: Commands, asset_server: Res<AssetServer>) {
)],
));
}
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();
}

@ -0,0 +1,83 @@
use bevy::prelude::*;
use bevy::window::WindowResolution;
mod block;
mod camera;
mod placement;
use block::*;
use camera::*;
use placement::*;
#[rustfmt::skip]
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "bevy-bloxel-classic".into(),
// Steam Deck: DPI appears pretty high, causing everything to be scaled up.
// Setting `scale_factor_override` prevents this from happening.
resolution: WindowResolution::new(1280, 720).with_scale_factor_override(1.0),
// WEB: Fit canvas to parent element, so `Window` resizes automatically.
fit_canvas_to_parent: true,
// WEB: Don't override default event handling like browser hotkeys while focused.
prevent_default_event_handling: false,
..default()
}),
..default()
}));
// Fixes issue on web where the cursor isn't ungrabbed properly.
app.add_plugins(bevy_fix_cursor_unlock_web::FixPointerUnlockPlugin);
app.add_systems(Startup, setup_crosshair);
app.add_systems(Startup, setup_blocks);
app.add_systems(Startup, setup_scene.after(setup_blocks));
app.add_systems(Update, cursor_grab);
app.add_systems(Update, update_crosshair_visibility.after(cursor_grab));
app.add_systems(Update, camera_look.after(cursor_grab).run_if(is_cursor_grabbed));
app.add_systems(Update, noclip_controller.after(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.
app.add_systems(PostUpdate, place_break_blocks.after(TransformSystems::Propagate));
app.add_observer(add_block_visuals);
// FIXME: Don't hardcode this!
#[cfg(not(target_arch = "wasm32"))]
app.add_plugins(common::network::ServerPlugin);
#[cfg(target_arch = "wasm32")]
app.add_plugins(common::network::ClientPlugin);
app.run();
}
fn setup_scene(mut commands: Commands, mut blocks: Blocks) {
// light
commands.spawn((
PointLight {
shadows_enabled: true,
..default()
},
Transform::from_xyz(4.0, 8.0, 4.0),
));
// camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
CameraFreeLook::default(),
CameraNoClip::default(),
));
// blocks
for x in -8..8 {
for z in -8..8 {
blocks.spawn(IVec3::new(x, 0, z));
}
}
}

@ -0,0 +1,9 @@
[package]
name = "bevy-bloxel-classic-common"
version = "0.1.0"
edition = "2024"
[dependencies]
bevy.workspace = true
lightyear.workspace = true
serde.workspace = true

@ -0,0 +1,31 @@
use bevy::prelude::*;
use lightyear::prelude::*;
use bevy::ecs::system::SystemParam;
use serde::{Deserialize, Serialize};
#[derive(Component, PartialEq, Deserialize, Serialize)]
pub struct Block;
#[derive(SystemParam)]
pub struct Blocks<'w, 's> {
commands: Commands<'w, 's>,
blocks: Query<'w, 's, &'static Transform, With<Block>>,
}
impl Blocks<'_, '_> {
pub fn spawn(&mut self, pos: IVec3) {
self.commands.spawn((
Block,
Transform::from_translation(pos.as_vec3() + Vec3::ONE / 2.),
// Currently prints some warnings when no `Server` is active, but this is fine.
Replicate::to_clients(NetworkTarget::All),
));
}
/// Gets the position of a block entity, or `None` if not a block.
pub fn position(&self, entity: Entity) -> Option<IVec3> {
let transform = self.blocks.get(entity).ok();
transform.map(|t| t.translation.floor().as_ivec3())
}
}

@ -0,0 +1,2 @@
pub mod block;
pub mod network;

@ -0,0 +1,63 @@
use std::net::{Ipv4Addr, SocketAddr};
use bevy::prelude::*;
use lightyear::prelude::client::*;
use lightyear::prelude::*;
// FIXME: Don't hardcode this!
pub const DIGEST: &'static str = "";
pub struct ClientPlugin;
impl Plugin for ClientPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(ClientPlugins::default());
// This maybe should be added by `ClientPlugins` but it currently isn't.
// (Unless we're using `NetcodeClientPlugin`, which would've added it.)
if !app.is_plugin_added::<lightyear::connection::client::ConnectionPlugin>() {
app.add_plugins(lightyear::connection::client::ConnectionPlugin);
}
if !app.is_plugin_added::<super::ProtocolPlugin>() {
app.add_plugins(super::ProtocolPlugin);
}
app.add_systems(Startup, connect_to_server);
app.add_observer(on_connecting);
app.add_observer(on_connected);
app.add_observer(on_disconnected);
}
}
fn connect_to_server(mut commands: Commands) {
let client_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0);
let server_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), super::DEFAULT_PORT);
let certificate_digest = DIGEST.to_string();
commands
.spawn((
Name::from("Client"),
LocalAddr(client_addr),
PeerAddr(server_addr),
ReplicationReceiver::default(),
WebTransportClientIo { certificate_digest },
RawClient,
))
.trigger(|entity| LinkStart { entity });
}
fn on_connecting(event: On<Add, Connecting>) {
let client = event.entity;
info!("Client '{client}' connecting ...");
}
fn on_connected(event: On<Add, Connected>) {
let client = event.entity;
info!("Client '{client}' connected!");
}
fn on_disconnected(event: On<Remove, Connected>) {
let client = event.entity;
info!("Client '{client}' disconnected!");
}

@ -0,0 +1,9 @@
mod client;
mod protocol;
mod server;
pub use client::*;
pub use protocol::*;
pub use server::*;
pub const DEFAULT_PORT: u16 = 13580;

@ -0,0 +1,13 @@
use bevy::prelude::*;
use lightyear::prelude::*;
use crate::block::Block;
pub struct ProtocolPlugin;
impl Plugin for ProtocolPlugin {
fn build(&self, app: &mut App) {
app.register_component::<Transform>();
app.register_component::<Block>();
}
}

@ -0,0 +1,91 @@
use std::net::{Ipv4Addr, SocketAddr};
use bevy::prelude::*;
use lightyear::prelude::server::*;
use lightyear::prelude::*;
pub struct ServerPlugin;
impl Plugin for ServerPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(ServerPlugins::default());
// These maybe should be added by `ServerPlugins` but currently aren't.
// (Unless we're using `NetcodeServerPlugin`, which would've added it.)
if !app.is_plugin_added::<lightyear::connection::client::ConnectionPlugin>() {
app.add_plugins(lightyear::connection::client::ConnectionPlugin);
}
if !app.is_plugin_added::<lightyear::connection::server::ConnectionPlugin>() {
app.add_plugins(lightyear::connection::server::ConnectionPlugin);
}
if !app.is_plugin_added::<super::ProtocolPlugin>() {
app.add_plugins(super::ProtocolPlugin);
}
app.add_systems(Startup, start_server);
// 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);
}
}
fn start_server(mut commands: Commands) -> Result {
let server_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), super::DEFAULT_PORT);
let certificate = Identity::self_signed(["localhost", "127.0.0.1", "::1"])?;
commands
.spawn((
Name::from("Server"),
LocalAddr(server_addr),
WebTransportServerIo { certificate },
RawServer,
))
.trigger(|entity| LinkStart { entity });
Ok(())
}
fn on_server_started(event: On<Add, Started>, servers: Query<&WebTransportServerIo>) -> Result {
let server = event.entity;
info!("Server '{server}' started!");
let certificate = &servers.get(server)?.certificate;
let certificate_hash = certificate.certificate_chain().as_slice()[0].hash();
let certificate_digest = certificate_hash.to_string().replace(':', "");
info!("== Certificate Digest ==");
info!(" Clients use this to securely connect to the server.");
info!(" {certificate_digest}");
Ok(())
}
fn on_server_stopped(event: On<Add, Stopped>) {
let server = event.entity;
info!("Server '{server}' stopped!");
}
fn handle_client_connected(
event: On<Add, Connected>,
clients: Query<&LinkOf>,
mut commands: Commands,
) -> Result {
let client = event.entity;
let server = clients.get(client)?.server;
info!("Client '{client}' connected to server '{server}'");
commands.entity(client).insert(ReplicationSender::default());
Ok(())
}
fn handle_client_disconnected(event: On<Add, Disconnected>, clients: Query<&LinkOf>) -> Result {
let client = event.entity;
let server = clients.get(client)?.server;
info!("Client '{client}' disconnected from server '{server}'");
Ok(())
}

@ -1,47 +0,0 @@
use bevy::ecs::system::SystemParam;
use bevy::prelude::*;
#[derive(Component)]
pub struct Block;
#[derive(SystemParam)]
pub struct Blocks<'w, 's> {
commands: Commands<'w, 's>,
block_resources: Res<'w, BlockResources>,
blocks: Query<'w, 's, &'static Transform, With<Block>>,
}
impl Blocks<'_, '_> {
pub fn spawn(&mut self, pos: IVec3) {
self.commands.spawn((
Block,
Mesh3d(self.block_resources.mesh.clone()),
MeshMaterial3d(self.block_resources.material.clone()),
Transform::from_translation(pos.as_vec3() + Vec3::ONE / 2.),
));
}
/// Gets the position of a block entity, or `None`
/// if the given entity is not alive or not a block.
pub fn position(&self, entity: Entity) -> Option<IVec3> {
let transform = self.blocks.get(entity).ok();
transform.map(|t| t.translation.floor().as_ivec3())
}
}
#[derive(Resource)]
pub struct BlockResources {
mesh: Handle<Mesh>,
material: Handle<StandardMaterial>,
}
pub fn setup_blocks(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.insert_resource(BlockResources {
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
material: materials.add(Color::srgb_u8(124, 144, 255)),
});
}

@ -1,55 +0,0 @@
use bevy::prelude::*;
mod block;
mod camera;
mod placement;
use block::*;
use camera::*;
use placement::*;
fn main() {
#[rustfmt::skip]
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup_crosshair)
.add_systems(Startup, setup_blocks)
.add_systems(Startup, setup_scene.after(setup_blocks))
.add_systems(Update, cursor_grab)
.add_systems(Update, camera_look.after(cursor_grab).run_if(is_cursor_grabbed))
.add_systems(Update, noclip_controller.after(camera_look).run_if(is_cursor_grabbed))
// This system requires the camera's `GlobalTransform`, so for
// a most up-to-date value, run it right after it's been updated.
.add_systems(PostUpdate, place_break_blocks.after(TransformSystems::Propagate))
.run();
}
fn setup_scene(mut commands: Commands, mut blocks: Blocks) {
// light
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
CameraFreeLook::default(),
CameraNoClip::default(),
));
// camera
commands.spawn((
PointLight {
shadows_enabled: true,
..default()
},
Transform::from_xyz(4.0, 8.0, 4.0),
));
// blocks
for x in -8..8 {
for z in -8..8 {
blocks.spawn(IVec3::new(x, 0, z));
}
}
}
Loading…
Cancel
Save