From 2d903d45992569d98aefa1b1ea32c5e5c3a9bb20 Mon Sep 17 00:00:00 2001 From: Hennadii Chernyshchyk Date: Sat, 22 Feb 2025 14:58:56 +0200 Subject: [PATCH 01/14] Store connected clients and their data as entities --- CHANGELOG.md | 18 + Cargo.toml | 1 - .../examples/simple_box.rs | 110 +++-- .../examples/tic_tac_toe.rs | 130 ++++-- bevy_replicon_example_backend/src/client.rs | 16 +- bevy_replicon_example_backend/src/server.rs | 47 +- .../tests/backend.rs | 7 +- src/client/diagnostics.rs | 8 +- src/core.rs | 74 ++-- src/core/connected_clients.rs | 131 ------ src/core/event/client_event.rs | 19 +- src/core/event/client_trigger.rs | 12 +- src/core/event/server_event.rs | 104 +++-- src/core/event/server_trigger.rs | 6 +- src/core/replication.rs | 2 +- src/core/replication/client_ticks.rs | 161 +++++++ src/core/replication/replicated_clients.rs | 400 ------------------ .../replication_registry/test_fns.rs | 2 +- src/core/replicon_client.rs | 106 +---- src/core/replicon_server.rs | 29 +- src/lib.rs | 62 ++- src/server.rs | 339 +++++++-------- src/server/client_entity_map.rs | 42 +- .../client_visibility.rs | 87 ++-- src/server/event.rs | 12 +- src/server/replication_messages.rs | 43 -- .../replication_messages/mutate_message.rs | 17 +- .../replication_messages/serialized_data.rs | 21 +- .../replication_messages/update_message.rs | 19 +- src/test_app.rs | 70 ++- tests/connection.rs | 69 +-- tests/despawn.rs | 4 +- tests/insertion.rs | 43 +- tests/mutations.rs | 119 ++++-- tests/removal.rs | 26 +- tests/server_event.rs | 53 ++- tests/spawn.rs | 28 +- tests/stats.rs | 22 +- tests/visibility.rs | 91 ++-- 39 files changed, 1026 insertions(+), 1524 deletions(-) delete mode 100644 src/core/connected_clients.rs create mode 100644 src/core/replication/client_ticks.rs delete mode 100644 src/core/replication/replicated_clients.rs rename src/{core/replication/replicated_clients => server}/client_visibility.rs (89%) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7fbf1e8..fc8dcac8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Derive `Debug` for `FnsId`. - Derive `Deref` and `DerefMut` to underlying event in `ToClients` and `FromClient`. +- Derive `PartialEq` for `RepliconClientStatus`. ### Changed +- Connected clients are now represented as entities with `ConnectedClient` components. Backends are responsible for spawning and despawning entities with this component. +- Statistics for connected clients now accessible via `ClientStats` component. +- Replicated entities now represented by connected clients with `ReplicatedClient` component. +- To access visibility, use `ClientVisibility` component on replicated entities. +- `ServerEntityMap` resource now a component on replicated entities. It now accepts entity to entity mappings directly instead of `ClientId` to `ClientMapping`. +- Replace statistic methods on `RepliconClient` with `RepliconClient::stats()` method that returns `ClientStats` struct. +- Move `VisibilityPolicy` to `server` module. +- Use `TestClientEntity` instead of `ClientId` resource on clients in `ServerTestAppExt` to identify client entity. +- Rename `FromClient::client_id` into `FromClient::client_entity`. - Replace `bincode` with `postcard`. It has more suitable variable integer encoding and potentially unlocks `no_std` support. If you use custom ser/de functions, replace `DefaultOptions::new().serialize_into(message, event)` with `postcard_utils::to_extend_mut(event, message)` and `DefaultOptions::new().deserialize_from(cursor)` with `postcard_utils::from_buf(message)`. - All serde methods now use `postcard::Result` instead of `bincode::Result`. - All deserialization methods now accept `Bytes` instead of `std::io::Cursor` because deserialization from `std::io::Read` requires a temporary buffer. `Bytes` already provide cursor-like functionality. The crate now re-exported under `bevy_replicon::bytes`. @@ -26,6 +36,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Local re-trigger for listen server mode. +### Removed + +- `ClientId`. Replicon doesn't need to know what backends use as client identifiers and now just uses `Entity` to refer to a connected client everywhere. Use `Entity::PLACEHOLDER` to refer to a server. +- `StartReplication` trigger. Just insert `ReplicatedClient` to enable replication. +- `ConnectedClients` and `ReplicatedClients` resources. Use components on connected clients instead. +- `ClientConnected` and `ClientDisconnected` triggers. Just observe for `Trigger` or `Trigger`. To get disconnect reason, obtain it from the ued backend. +- `ServerSet::TriggerConnectionEvents` variant. We no longer use events for connections. + ## [0.30.1] - 2025-02-07 ### Fixed diff --git a/Cargo.toml b/Cargo.toml index b4f23457..706ed738 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,6 @@ members = ["bevy_replicon_example_backend"] [dependencies] bevy = { version = "0.15", default-features = false, features = ["serialize"] } -thiserror = "2.0" typeid = "1.0" bytes = "1.10" serde = "1.0" diff --git a/bevy_replicon_example_backend/examples/simple_box.rs b/bevy_replicon_example_backend/examples/simple_box.rs index 2cfbb162..14129d87 100644 --- a/bevy_replicon_example_backend/examples/simple_box.rs +++ b/bevy_replicon_example_backend/examples/simple_box.rs @@ -1,7 +1,10 @@ //! A simple demo to showcase how player could send inputs to move a box and server replicates position back. //! Also demonstrates the single-player and how sever also could be a player. -use std::io; +use std::{ + hash::{DefaultHasher, Hash, Hasher}, + io, +}; use bevy::{ color::palettes::css::GREEN, @@ -35,7 +38,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) @@ -49,7 +52,12 @@ fn read_cli(mut commands: Commands, cli: Res) -> io::Result<()> { match *cli { Cli::SinglePlayer => { info!("starting single-player game"); - commands.spawn((BoxPlayer(ClientId::SERVER), BoxColor(GREEN.into()))); + commands.spawn(( + PlayerBox { + color: GREEN.into(), + }, + BoxOwner(Entity::PLACEHOLDER), + )); } Cli::Server { port } => { info!("starting server at port {port}"); @@ -63,15 +71,20 @@ fn read_cli(mut commands: Commands, cli: Res) -> io::Result<()> { }, TextColor::WHITE, )); - commands.spawn((BoxPlayer(ClientId::SERVER), BoxColor(GREEN.into()))); + commands.spawn(( + PlayerBox { + color: GREEN.into(), + }, + BoxOwner(Entity::PLACEHOLDER), + )); } Cli::Client { port } => { info!("connecting to port {port}"); let client = ExampleClient::new(port)?; - let client_id = client.id()?; + let addr = client.local_addr()?; commands.insert_resource(client); commands.spawn(( - Text(format!("Client: {client_id:?}")), + Text(format!("Client: {addr}")), TextFont { font_size: 30.0, ..default() @@ -89,24 +102,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(); } @@ -139,26 +165,28 @@ fn read_input(mut commands: Commands, input: Res>) { fn apply_movement( trigger: Trigger>, time: Res