Basic networking using `lightyear` crate

main
copygirl 2 days ago
parent ff8578fd82
commit bb66d28f06
  1. 3056
      Cargo.lock
  2. 12
      Cargo.toml
  3. 8
      client/Cargo.toml
  4. 4
      client/src/camera.rs
  5. 73
      client/src/main.rs
  6. 4
      common/Cargo.toml
  7. 9
      common/src/block.rs
  8. 1
      common/src/lib.rs
  9. 63
      common/src/network/client.rs
  10. 9
      common/src/network/mod.rs
  11. 13
      common/src/network/protocol.rs
  12. 91
      common/src/network/server.rs

3056
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
[workspace]
resolver = "3"
members = ["client", "common"]
members = [ "client", "common" ]
# Enable a small amount of optimization in the dev profile.
[profile.dev]
@ -11,4 +11,12 @@ opt-level = 1
opt-level = 3
[workspace.dependencies]
bevy = "0.17"
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" ]

@ -4,8 +4,10 @@ version = "0.1.0"
edition = "2024"
[dependencies]
common = { package = "bevy-bloxel-classic-common", path = "../common" }
common = { path = "../common", package = "bevy-bloxel-classic-common" }
bevy = { workspace = true }
bevy.workspace = true
lightyear.workspace = true
serde.workspace = true
bevy_fix_cursor_unlock_web = "0.2"
bevy_fix_cursor_unlock_web = "0.2.0"

@ -175,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),
@ -192,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.

@ -1,5 +1,7 @@
use bevy::prelude::*;
use bevy::window::WindowResolution;
mod block;
mod camera;
mod placement;
@ -8,41 +10,54 @@ use block::*;
use camera::*;
use placement::*;
#[rustfmt::skip]
fn main() {
#[rustfmt::skip]
App::new()
.add_plugins(DefaultPlugins)
// Fixes issue on web where the cursor isn't ungrabbed properly.
.add_plugins(bevy_fix_cursor_unlock_web::FixPointerUnlockPlugin)
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()
}));
.add_systems(Startup, setup_crosshair)
.add_systems(Startup, setup_blocks)
.add_systems(Startup, setup_scene.after(setup_blocks))
// Fixes issue on web where the cursor isn't ungrabbed properly.
app.add_plugins(bevy_fix_cursor_unlock_web::FixPointerUnlockPlugin);
.add_systems(Update, cursor_grab)
.add_systems(Update, update_crosshair_visibility.after(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))
app.add_systems(Startup, setup_crosshair);
app.add_systems(Startup, setup_blocks);
app.add_systems(Startup, setup_scene.after(setup_blocks));
// `place_break_blocks` requires the camera's `GlobalTransform`.
// For a most up-to-date value, run it after that's been updated.
.add_systems(PostUpdate, place_break_blocks.after(TransformSystems::Propagate))
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));
.add_observer(add_block_visuals)
// `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();
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((
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,
@ -51,6 +66,14 @@ fn setup_scene(mut commands: Commands, mut blocks: Blocks) {
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 {

@ -4,4 +4,6 @@ version = "0.1.0"
edition = "2024"
[dependencies]
bevy = { workspace = true }
bevy.workspace = true
lightyear.workspace = true
serde.workspace = true

@ -1,7 +1,10 @@
use bevy::ecs::system::SystemParam;
use bevy::prelude::*;
use lightyear::prelude::*;
use bevy::ecs::system::SystemParam;
use serde::{Deserialize, Serialize};
#[derive(Component)]
#[derive(Component, PartialEq, Deserialize, Serialize)]
pub struct Block;
#[derive(SystemParam)]
@ -15,6 +18,8 @@ impl Blocks<'_, '_> {
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),
));
}

@ -1 +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(())
}
Loading…
Cancel
Save