Skip to content

Commit 812fc06

Browse files
authored
Store connected clients and their data as entities (#423)
This PR replaces `ConnectedEntities`, `ReplicatedEntities`, `ClientEntityMap`, and `Messages` resources with entities and data stored as components. Connected clients are now represented by entities with a `ConnectedEntity` component. Connected entities become replicated after the insertion of `ReplicatedClient` (which replaces the `StartReplication` trigger). It's just a marker now, I've split the original `ReplicatedClient` into multiple components. Visibility and statistics are now accessible via the `ClientVisibility` and `ClientStats` components on replicated entities. The `ClientEntityMap` is now a component on replicated entities that stores mappings for them. `ClientTicks` is no longer accessible to users, but it isn't needed for the public API - even prediction crates don't use it. `UpdateMessage` and `MutateMessages` are now crate-private components, just to speed up iteration. Using `zip` is slower, but if everything is on an entity, the performance is similar to what we had before this PR. It's still a bit slower, but I believe the improved ergonomics are worth it. Since connected clients now have a unique identifier (`Entity`). This also gives us both fast iteration and fast lookup (closes #326). Users will mostly interact with entities, and plugins can use `ClientId` if they need a persistent identifier. The server ID is now `SERVER` constant which equal to `Entity::PLACEHOLDER`. I also removed the `ClientConnected` and `ClientDisconnected` events. Users can observe for `Trigger<OnAdd, ConnectedClient>` or `Trigger<OnRemove, ConnectedClient>`. `DisconnectReason` is also gone. We don't care about it in Replicon and can't provide a nice representation for it anyway. Users can access proper information from the used backend. There is no need to duplicate it for us.
1 parent f7e90f6 commit 812fc06

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1151
-1580
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Derive `Debug` for `FnsId`.
1313
- Derive `Deref` and `DerefMut` to underlying event in `ToClients` and `FromClient`.
14+
- Derive `PartialEq` for `RepliconClientStatus`.
1415

1516
### Changed
1617

18+
- Connected clients are now represented as entities with `ConnectedClient` components. Backends are responsible for spawning and despawning entities with this component. `ClientId` is accessible from `ConnectedClient::id` in case you need an identifier that is persistent across reconnects.
19+
- Statistics for connected clients now accessible via `ClientStats` component.
20+
- Replicated entities now represented by connected clients with `ReplicatedClient` component.
21+
- To access visibility, use `ClientVisibility` component on replicated entities.
22+
- `ServerEntityMap` resource now a component on replicated entities. It now accepts entity to entity mappings directly instead of `ClientId` to `ClientMapping`.
23+
- Replace statistic methods on `RepliconClient` with `RepliconClient::stats()` method that returns `ClientStats` struct.
24+
- Move `VisibilityPolicy` to `server` module.
25+
- Move `ClientId` to `connected_client` module and remove from `prelude`.
26+
- Use `TestClientEntity` instead of `ClientId` resource on clients in `ServerTestAppExt` to identify client entity.
27+
- Rename `FromClient::client_id` into `FromClient::client_entity`.
1728
- 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)`.
1829
- All serde methods now use `postcard::Result` instead of `bincode::Result`.
1930
- 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 +37,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2637

2738
- Local re-trigger for listen server mode.
2839

40+
### Removed
41+
42+
- `ClientId` from `prelude`. Most operations now done using `Entity` as identifier. But it could be useful
43+
- `StartReplication` trigger. Just insert `ReplicatedClient` to enable replication.
44+
- `ConnectedClients` and `ReplicatedClients` resources. Use components on connected clients instead.
45+
- `ClientConnected` and `ClientDisconnected` triggers. Just observe for `Trigger<OnAdd, ConnectedClient>` or `Trigger<OnRemove, ConnectedClient>`. To get disconnect reason, obtain it from the ued backend.
46+
- `ServerSet::TriggerConnectionEvents` variant. We no longer use events for connections.
47+
2948
## [0.30.1] - 2025-02-07
3049

3150
### Fixed

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ members = ["bevy_replicon_example_backend"]
2929

3030
[dependencies]
3131
bevy = { version = "0.15", default-features = false, features = ["serialize"] }
32-
thiserror = "2.0"
3332
typeid = "1.0"
3433
bytes = "1.10"
3534
serde = "1.0"

bevy_replicon_example_backend/examples/simple_box.rs

Lines changed: 75 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
//! A simple demo to showcase how player could send inputs to move a box and server replicates position back.
22
//! Also demonstrates the single-player and how sever also could be a player.
33
4-
use std::io;
4+
use std::{
5+
hash::{DefaultHasher, Hash, Hasher},
6+
io,
7+
};
58

69
use bevy::{
710
color::palettes::css::GREEN,
@@ -35,7 +38,7 @@ struct SimpleBoxPlugin;
3538
impl Plugin for SimpleBoxPlugin {
3639
fn build(&self, app: &mut App) {
3740
app.replicate::<BoxPosition>()
38-
.replicate::<BoxColor>()
41+
.replicate::<PlayerBox>()
3942
.add_client_trigger::<MoveBox>(ChannelKind::Ordered)
4043
.add_observer(spawn_clients)
4144
.add_observer(despawn_clients)
@@ -49,7 +52,12 @@ fn read_cli(mut commands: Commands, cli: Res<Cli>) -> io::Result<()> {
4952
match *cli {
5053
Cli::SinglePlayer => {
5154
info!("starting single-player game");
52-
commands.spawn((BoxPlayer(ClientId::SERVER), BoxColor(GREEN.into())));
55+
commands.spawn((
56+
PlayerBox {
57+
color: GREEN.into(),
58+
},
59+
BoxOwner(SERVER),
60+
));
5361
}
5462
Cli::Server { port } => {
5563
info!("starting server at port {port}");
@@ -63,15 +71,20 @@ fn read_cli(mut commands: Commands, cli: Res<Cli>) -> io::Result<()> {
6371
},
6472
TextColor::WHITE,
6573
));
66-
commands.spawn((BoxPlayer(ClientId::SERVER), BoxColor(GREEN.into())));
74+
commands.spawn((
75+
PlayerBox {
76+
color: GREEN.into(),
77+
},
78+
BoxOwner(SERVER),
79+
));
6780
}
6881
Cli::Client { port } => {
6982
info!("connecting to port {port}");
7083
let client = ExampleClient::new(port)?;
71-
let client_id = client.id()?;
84+
let addr = client.local_addr()?;
7285
commands.insert_resource(client);
7386
commands.spawn((
74-
Text(format!("Client: {client_id:?}")),
87+
Text(format!("Client: {addr}")),
7588
TextFont {
7689
font_size: 30.0,
7790
..default()
@@ -89,24 +102,37 @@ fn spawn_camera(mut commands: Commands) {
89102
}
90103

91104
/// Spawns a new box whenever a client connects.
92-
fn spawn_clients(trigger: Trigger<ClientConnected>, mut commands: Commands) {
93-
// Generate pseudo random color from client id.
94-
let r = ((trigger.client_id.get() % 23) as f32) / 23.0;
95-
let g = ((trigger.client_id.get() % 27) as f32) / 27.0;
96-
let b = ((trigger.client_id.get() % 39) as f32) / 39.0;
97-
info!("spawning box for `{:?}`", trigger.client_id);
98-
commands.spawn((BoxPlayer(trigger.client_id), BoxColor(Color::srgb(r, g, b))));
105+
fn spawn_clients(trigger: Trigger<OnAdd, ConnectedClient>, mut commands: Commands) {
106+
// Hash index to generate visually distinctive color.
107+
let mut hasher = DefaultHasher::new();
108+
trigger.entity().index().hash(&mut hasher);
109+
let hash = hasher.finish();
110+
111+
// Use the lower 24 bits.
112+
// Divide by 255 to convert bytes into 0..1 floats.
113+
let r = ((hash >> 16) & 0xFF) as f32 / 255.0;
114+
let g = ((hash >> 8) & 0xFF) as f32 / 255.0;
115+
let b = (hash & 0xFF) as f32 / 255.0;
116+
117+
// Generate pseudo random color from client entity.
118+
info!("spawning box for `{}`", trigger.entity());
119+
commands.spawn((
120+
PlayerBox {
121+
color: Color::srgb(r, g, b),
122+
},
123+
BoxOwner(trigger.entity()),
124+
));
99125
}
100126

101127
/// Despawns a box whenever a client disconnects.
102128
fn despawn_clients(
103-
trigger: Trigger<ClientDisconnected>,
129+
trigger: Trigger<OnRemove, ConnectedClient>,
104130
mut commands: Commands,
105-
boxes: Query<(Entity, &BoxPlayer)>,
131+
boxes: Query<(Entity, &BoxOwner)>,
106132
) {
107133
let (entity, _) = boxes
108134
.iter()
109-
.find(|(_, &player)| *player == trigger.client_id)
135+
.find(|(_, &owner)| *owner == trigger.entity())
110136
.expect("all clients should have entities");
111137
commands.entity(entity).despawn();
112138
}
@@ -139,26 +165,28 @@ fn read_input(mut commands: Commands, input: Res<ButtonInput<KeyCode>>) {
139165
fn apply_movement(
140166
trigger: Trigger<FromClient<MoveBox>>,
141167
time: Res<Time>,
142-
mut boxes: Query<(&BoxPlayer, &mut BoxPosition)>,
168+
mut boxes: Query<(&BoxOwner, &mut BoxPosition)>,
143169
) {
144170
const MOVE_SPEED: f32 = 300.0;
145-
info!("received movement from `{:?}`", trigger.client_id);
146-
for (player, mut position) in &mut boxes {
147-
// Find the sender entity. We don't include the entity as a trigger target to save traffic, since the server knows
148-
// which entity to apply the input to. We could have a resource that maps connected clients to controlled entities,
149-
// but we didn't implement it for the sake of simplicity.
150-
if trigger.client_id == **player {
151-
**position += *trigger.event * time.delta_secs() * MOVE_SPEED;
152-
}
153-
}
171+
info!("received movement from `{}`", trigger.client_entity);
172+
173+
// Find the sender entity. We don't include the entity as a trigger target to save traffic, since the server knows
174+
// which entity to apply the input to. We could have a resource that maps connected clients to controlled entities,
175+
// but we didn't implement it for the sake of simplicity.
176+
let (_, mut position) = boxes
177+
.iter_mut()
178+
.find(|(&owner, _)| *owner == trigger.client_entity)
179+
.unwrap_or_else(|| panic!("`{}` should be connected", trigger.client_entity));
180+
181+
**position += *trigger.event * time.delta_secs() * MOVE_SPEED;
154182
}
155183

156-
fn draw_boxes(mut gizmos: Gizmos, boxes: Query<(&BoxPosition, &BoxColor)>) {
157-
for (position, color) in &boxes {
184+
fn draw_boxes(mut gizmos: Gizmos, boxes: Query<(&BoxPosition, &PlayerBox)>) {
185+
for (position, player) in &boxes {
158186
gizmos.rect(
159187
Vec3::new(position.x, position.y, 0.0),
160188
Vec2::ONE * 50.0,
161-
**color,
189+
player.color,
162190
);
163191
}
164192
}
@@ -188,18 +216,30 @@ impl Default for Cli {
188216
}
189217
}
190218

191-
/// Identifies which player controls the box.
219+
/// Player-controlled box.
192220
///
193221
/// We want to replicate all boxes, so we just set [`Replicated`] as a required component.
194-
#[derive(Component, Clone, Copy, Deref, Serialize, Deserialize)]
195-
#[require(BoxPosition, BoxColor, Replicated)]
196-
struct BoxPlayer(ClientId);
222+
#[derive(Component, Deref, Deserialize, Serialize, Default)]
223+
#[require(BoxPosition, Replicated)]
224+
struct PlayerBox {
225+
/// Color to visually distinguish boxes.
226+
color: Color,
227+
}
197228

229+
/// Position of a player-controlled box.
230+
///
231+
/// This is a separate component from [`PlayerBox`] because, when the position
232+
/// changes, we only want to send this component (and it changes often!).
198233
#[derive(Component, Deserialize, Serialize, Deref, DerefMut, Default)]
199234
struct BoxPosition(Vec2);
200235

201-
#[derive(Component, Deref, Deserialize, Serialize, Default)]
202-
struct BoxColor(Color);
236+
/// Identifies which player controls the box.
237+
///
238+
/// Points to client entity. Used to apply movement to the correct box.
239+
///
240+
/// It's not replicated and present only on server or singleplayer.
241+
#[derive(Component, Clone, Copy, Deref)]
242+
struct BoxOwner(Entity);
203243

204244
/// A movement event for the controlled box.
205245
#[derive(Deserialize, Deref, Event, Serialize)]

0 commit comments

Comments
 (0)