From f231736218f389e7db6de33a6a34009cd7e920d8 Mon Sep 17 00:00:00 2001 From: Hennadii Chernyshchyk Date: Sun, 2 Mar 2025 02:10:16 +0200 Subject: [PATCH] Migrate to connected clients as entities --- Cargo.toml | 4 +- examples/simple_box.rs | 104 ++++++++++++++++++++++---------- examples/tic_tac_toe.rs | 129 ++++++++++++++++++++++++---------------- src/client.rs | 22 +++---- src/lib.rs | 2 +- src/server.rs | 126 +++++++++++++++++++++++---------------- tests/netcode.rs | 7 +-- 7 files changed, 237 insertions(+), 157 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d98d824..958a511 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,9 +25,10 @@ rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] all-features = true [dependencies] -bevy_replicon = { version = "0.30", default-features = false } +bevy_replicon = { git = "https://github.com/projectharmonia/bevy_replicon", branch = "connected-entities", default-features = false } bevy_renet = { version = "1.0", default-features = false } bevy = { version = "0.15", default-features = false } +serde = "1.0" [dev-dependencies] bevy = { version = "0.15", default-features = false, features = [ @@ -40,7 +41,6 @@ bevy = { version = "0.15", default-features = false, features = [ "default_font", ] } clap = { version = "4.1", features = ["derive"] } -serde = "1.0" [features] default = ["client", "server", "renet_netcode"] diff --git a/examples/simple_box.rs b/examples/simple_box.rs index 9415c1b..8eae053 100644 --- a/examples/simple_box.rs +++ b/examples/simple_box.rs @@ -3,6 +3,7 @@ use std::{ error::Error, + hash::{DefaultHasher, Hash, Hasher}, net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}, time::SystemTime, }; @@ -46,7 +47,7 @@ struct SimpleBoxPlugin; impl Plugin for SimpleBoxPlugin { fn build(&self, app: &mut App) { app.replicate::() - .replicate::() + .replicate::() .add_client_trigger::(ChannelKind::Ordered) .add_observer(spawn_clients) .add_observer(despawn_clients) @@ -66,7 +67,12 @@ fn read_cli( match *cli { Cli::SinglePlayer => { info!("starting single-player game"); - commands.spawn((BoxPlayer(ClientId::SERVER), BoxColor(GREEN.into()))); + commands.spawn(( + PlayerBox { + color: GREEN.into(), + }, + BoxOwner(SERVER), + )); } Cli::Server { port } => { info!("starting server at port {port}"); @@ -101,7 +107,12 @@ fn read_cli( }, TextColor::WHITE, )); - commands.spawn((BoxPlayer(ClientId::SERVER), BoxColor(GREEN.into()))); + commands.spawn(( + PlayerBox { + color: GREEN.into(), + }, + BoxOwner(SERVER), + )); } Cli::Client { port, ip } => { info!("connecting to {ip}:{port}"); @@ -130,7 +141,7 @@ fn read_cli( commands.insert_resource(transport); commands.spawn(( - Text(format!("Client: {client_id:?}")), + Text(format!("Client: {client_id}")), TextFont { font_size: 30.0, ..default() @@ -148,24 +159,37 @@ fn spawn_camera(mut commands: Commands) { } /// Spawns a new box whenever a client connects. -fn spawn_clients(trigger: Trigger, mut commands: Commands) { - // Generate pseudo random color from client id. - let r = ((trigger.client_id.get() % 23) as f32) / 23.0; - let g = ((trigger.client_id.get() % 27) as f32) / 27.0; - let b = ((trigger.client_id.get() % 39) as f32) / 39.0; - info!("spawning box for `{:?}`", trigger.client_id); - commands.spawn((BoxPlayer(trigger.client_id), BoxColor(Color::srgb(r, g, b)))); +fn spawn_clients(trigger: Trigger, mut commands: Commands) { + // Hash index to generate visually distinctive color. + let mut hasher = DefaultHasher::new(); + trigger.entity().index().hash(&mut hasher); + let hash = hasher.finish(); + + // Use the lower 24 bits. + // Divide by 255 to convert bytes into 0..1 floats. + let r = ((hash >> 16) & 0xFF) as f32 / 255.0; + let g = ((hash >> 8) & 0xFF) as f32 / 255.0; + let b = (hash & 0xFF) as f32 / 255.0; + + // Generate pseudo random color from client entity. + info!("spawning box for `{}`", trigger.entity()); + commands.spawn(( + PlayerBox { + color: Color::srgb(r, g, b), + }, + BoxOwner(trigger.entity()), + )); } /// Despawns a box whenever a client disconnects. fn despawn_clients( - trigger: Trigger, + trigger: Trigger, mut commands: Commands, - boxes: Query<(Entity, &BoxPlayer)>, + boxes: Query<(Entity, &BoxOwner)>, ) { let (entity, _) = boxes .iter() - .find(|(_, &player)| *player == trigger.client_id) + .find(|(_, &owner)| *owner == trigger.entity()) .expect("all clients should have entities"); commands.entity(entity).despawn(); } @@ -198,26 +222,28 @@ fn read_input(mut commands: Commands, input: Res>) { fn apply_movement( trigger: Trigger>, time: Res