Skip to content

Commit 41c82c4

Browse files
committed
Use Bevy state API for client and server states
This allows users to utilize things like `StateScoped` and run systems more efficiently using `OnEnter` or `OnExit` without evaluating conditions every frame. As a result, we now require `StatesPlugin` to be present. It's included by default in `DefaultPlugins`, but with `MinimalPlugins` you have to add it manually. In tests, I had to add `StatesPlugin` everywhere. This also changes the behavior a little bit: - On disconnect, we still apply all received messages and disconnect only after that, because the `StateTransitions` schedule runs right after `PreUpdate`. But I think this behavior is actually better. - We no longer check if `RepliconClient` or `RepliconServer` can actually accept messages. I currently preserved the original behavior where we reset them after exiting `Connected` or `Running`. But it might be worth considering doing a reset on exiting `Disconnected` or `Stopped` to ensure a clean start, like we did with events. I would appreciate opinions on this. Fix formatting Fix tests
1 parent e1586e2 commit 41c82c4

Some content is hidden

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

44 files changed

+481
-615
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414

1515
### Changed
1616

17+
- Rename `RepliconClientStatus` to `ClientState` and `RepliconServerStatus` to `ServerState`. They are now regular Bevy states. As result, we now require `StatesPlugin` to be added. It's present by default in `DefaultPlugins`, but with `MinimalPlugins` you have to add it manually.
1718
- `AppRuleExt::replicate_with` now accepts `IntoReplicationRule` trait that allows to define rules with multiple components.
1819
- Rename `GroupReplication` into `BundleReplication`.
1920
- Rename `AppRuleExt::replicate_group` into `AppRuleExt::replicate_bundle`.
@@ -30,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3031

3132
- `WriteCtx::commands`. You can now insert and remove components directly through `DeferredEntity`.
3233
- Deprecated methods.
34+
- Methods in `RepliconServer` and `RepliconClient` that updated the connection state. Use Bevy's state API with `ServerState` and `ClientState` instead.
35+
- All provided run conditions. Just use `in_state` or `OnEnter`/`OnExit` with `ServerState` and `ClientState` instead.
3336

3437
## [0.33.0] - 2025-04-27
3538

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ all-features = true
2828
members = ["bevy_replicon_example_backend"]
2929

3030
[dependencies]
31-
bevy = { version = "0.16.0", default-features = false }
31+
bevy = { version = "0.16.0", default-features = false, features = [
32+
"bevy_state",
33+
] }
3234
log = "0.4" # Directly depend on `log` like other `no_std` Bevy crates, since `bevy_log` currently requires `std`.
3335
petgraph = { version = "0.8", default-features = false, features = [
3436
"stable_graph",

benches/related_entities.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use bevy::prelude::*;
1+
use bevy::{prelude::*, state::app::StatesPlugin};
22
use bevy_replicon::prelude::*;
33
use criterion::{Criterion, criterion_group, criterion_main};
44

@@ -11,24 +11,25 @@ fn hierarchy_spawning(c: &mut Criterion) {
1111

1212
group.bench_function("regular", |b| {
1313
let mut app = App::new();
14-
app.add_plugins((MinimalPlugins, RepliconPlugins));
14+
app.add_plugins((MinimalPlugins, StatesPlugin, RepliconPlugins));
1515

1616
b.iter(|| spawn_then_despawn(&mut app));
1717
});
1818
group.bench_function("related_without_server", |b| {
1919
let mut app = App::new();
20-
app.add_plugins((MinimalPlugins, RepliconPlugins))
20+
app.add_plugins((MinimalPlugins, StatesPlugin, RepliconPlugins))
2121
.sync_related_entities::<ChildOf>();
2222

2323
b.iter(|| spawn_then_despawn(&mut app));
2424
});
2525
group.bench_function("related", |b| {
2626
let mut app = App::new();
27-
app.add_plugins((MinimalPlugins, RepliconPlugins))
27+
app.add_plugins((MinimalPlugins, StatesPlugin, RepliconPlugins))
2828
.sync_related_entities::<ChildOf>();
2929

30-
let mut server = app.world_mut().resource_mut::<RepliconServer>();
31-
server.set_running(true);
30+
app.world_mut()
31+
.resource_mut::<NextState<ServerState>>()
32+
.set(ServerState::Running);
3233

3334
b.iter(|| spawn_then_despawn(&mut app));
3435
});
@@ -45,15 +46,15 @@ fn hierarchy_changes(c: &mut Criterion) {
4546

4647
group.bench_function("regular", |b| {
4748
let mut app = App::new();
48-
app.add_plugins((MinimalPlugins, RepliconPlugins));
49+
app.add_plugins((MinimalPlugins, StatesPlugin, RepliconPlugins));
4950

5051
spawn_hierarchy(app.world_mut());
5152

5253
b.iter(|| trigger_hierarchy_change(&mut app));
5354
});
5455
group.bench_function("related_without_server", |b| {
5556
let mut app = App::new();
56-
app.add_plugins((MinimalPlugins, RepliconPlugins))
57+
app.add_plugins((MinimalPlugins, StatesPlugin, RepliconPlugins))
5758
.sync_related_entities::<ChildOf>();
5859

5960
spawn_hierarchy(app.world_mut());
@@ -62,13 +63,14 @@ fn hierarchy_changes(c: &mut Criterion) {
6263
});
6364
group.bench_function("related", |b| {
6465
let mut app = App::new();
65-
app.add_plugins((MinimalPlugins, RepliconPlugins))
66+
app.add_plugins((MinimalPlugins, StatesPlugin, RepliconPlugins))
6667
.sync_related_entities::<ChildOf>();
6768

6869
spawn_hierarchy(app.world_mut());
6970

70-
let mut server = app.world_mut().resource_mut::<RepliconServer>();
71-
server.set_running(true);
71+
app.world_mut()
72+
.resource_mut::<NextState<ServerState>>()
73+
.set(ServerState::Running);
7274

7375
b.iter(|| trigger_hierarchy_change(&mut app));
7476
});

benches/replication.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use core::time::Duration;
22

3-
use bevy::{ecs::component::Mutable, platform::time::Instant, prelude::*};
3+
use bevy::{
4+
ecs::component::Mutable, platform::time::Instant, prelude::*, state::app::StatesPlugin,
5+
};
46
use bevy_replicon::{prelude::*, test_app::ServerTestAppExt};
57
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
68
use serde::{Deserialize, Serialize, de::DeserializeOwned};
@@ -184,6 +186,7 @@ fn create_app<C: BenchmarkComponent>() -> App {
184186
let mut app = App::new();
185187
app.add_plugins((
186188
MinimalPlugins,
189+
StatesPlugin,
187190
RepliconPlugins.set(ServerPlugin {
188191
tick_policy: TickPolicy::EveryFrame,
189192
..Default::default()

bevy_replicon_example_backend/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ fastrand = "2.3"
2828
[dev-dependencies]
2929
bevy = { version = "0.16.0", default-features = false, features = [
3030
"bevy_gizmos",
31-
"bevy_state",
3231
"bevy_text",
3332
"bevy_ui_picking_backend",
3433
"bevy_ui",

bevy_replicon_example_backend/examples/tic_tac_toe.rs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,19 +55,17 @@ fn main() {
5555
.add_systems(OnEnter(GameState::Winner), show_winner_text)
5656
.add_systems(OnEnter(GameState::Tie), show_tie_text)
5757
.add_systems(OnEnter(GameState::Disconnected), stop_networking)
58+
.add_systems(OnEnter(ClientState::Connected), client_start)
59+
.add_systems(OnEnter(ClientState::Connecting), show_connecting_text)
60+
.add_systems(OnExit(ClientState::Connected), disconnect_by_server)
61+
.add_systems(OnEnter(ServerState::Running), show_waiting_client_text)
5862
.add_systems(
5963
Update,
6064
(
61-
show_connecting_text.run_if(resource_added::<ExampleClient>),
62-
show_waiting_client_text.run_if(resource_added::<ExampleServer>),
63-
client_start.run_if(client_just_connected),
64-
(
65-
disconnect_by_server.run_if(client_just_disconnected),
66-
update_buttons_background.run_if(local_player_turn),
67-
show_turn_symbol.run_if(resource_changed::<TurnSymbol>),
68-
)
69-
.run_if(in_state(GameState::InGame)),
70-
),
65+
update_buttons_background.run_if(local_player_turn),
66+
show_turn_symbol.run_if(resource_changed::<TurnSymbol>),
67+
)
68+
.run_if(in_state(GameState::InGame)),
7169
)
7270
.run();
7371
}

bevy_replicon_example_backend/src/client.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ impl Plugin for RepliconExampleClientPlugin {
3636
}
3737
}
3838

39-
fn set_disconnected(mut replicon_client: ResMut<RepliconClient>) {
40-
replicon_client.set_status(RepliconClientStatus::Disconnected);
39+
fn set_disconnected(mut state: ResMut<NextState<ClientState>>) {
40+
state.set(ClientState::Disconnected);
4141
}
4242

43-
fn set_connected(mut replicon_client: ResMut<RepliconClient>) {
44-
replicon_client.set_status(RepliconClientStatus::Connected);
43+
fn set_connected(mut state: ResMut<NextState<ClientState>>) {
44+
state.set(ClientState::Connected);
4545
}
4646

4747
fn receive_packets(

bevy_replicon_example_backend/src/server.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ impl Plugin for RepliconExampleServerPlugin {
3636
}
3737
}
3838

39-
fn set_stopped(mut server: ResMut<RepliconServer>) {
40-
server.set_running(false);
39+
fn set_stopped(mut state: ResMut<NextState<ServerState>>) {
40+
state.set(ServerState::Stopped);
4141
}
4242

43-
fn set_running(mut server: ResMut<RepliconServer>) {
44-
server.set_running(true);
43+
fn set_running(mut state: ResMut<NextState<ServerState>>) {
44+
state.set(ServerState::Running);
4545
}
4646

4747
fn receive_packets(

bevy_replicon_example_backend/tests/backend.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::io;
22

3-
use bevy::prelude::*;
3+
use bevy::{prelude::*, state::app::StatesPlugin};
44
use bevy_replicon::prelude::*;
55
use bevy_replicon_example_backend::{ExampleClient, ExampleServer, RepliconExampleBackendPlugins};
66
use serde::{Deserialize, Serialize};
@@ -12,6 +12,7 @@ fn connect_disconnect() {
1212
for app in [&mut server_app, &mut client_app] {
1313
app.add_plugins((
1414
MinimalPlugins,
15+
StatesPlugin,
1516
RepliconPlugins.set(ServerPlugin {
1617
tick_policy: TickPolicy::EveryFrame,
1718
..Default::default()
@@ -22,13 +23,14 @@ fn connect_disconnect() {
2223

2324
setup(&mut server_app, &mut client_app).unwrap();
2425

25-
assert!(server_app.world().resource::<RepliconServer>().is_running());
26+
let server_state = server_app.world().resource::<State<ServerState>>();
27+
assert_eq!(*server_state, ServerState::Running);
2628

2729
let mut clients = server_app.world_mut().query::<&ConnectedClient>();
2830
assert_eq!(clients.iter(server_app.world()).len(), 1);
2931

30-
let replicon_client = client_app.world().resource::<RepliconClient>();
31-
assert!(replicon_client.is_connected());
32+
let client_state = client_app.world().resource::<State<ClientState>>();
33+
assert_eq!(*client_state, ClientState::Connected);
3234

3335
let renet_client = client_app.world().resource::<ExampleClient>();
3436
assert!(renet_client.is_connected());
@@ -40,14 +42,15 @@ fn connect_disconnect() {
4042

4143
assert_eq!(clients.iter(server_app.world()).len(), 0);
4244

43-
let replicon_client = client_app.world().resource::<RepliconClient>();
44-
assert!(replicon_client.is_disconnected());
45+
let client_state = client_app.world().resource::<State<ClientState>>();
46+
assert_eq!(*client_state, ClientState::Disconnected);
4547

4648
server_app.world_mut().remove_resource::<ExampleServer>();
4749

4850
server_app.update();
4951

50-
assert!(!server_app.world().resource::<RepliconServer>().is_running());
52+
let server_state = server_app.world().resource::<State<ServerState>>();
53+
assert_eq!(*server_state, ServerState::Running);
5154
}
5255

5356
#[test]
@@ -57,6 +60,7 @@ fn replication() {
5760
for app in [&mut server_app, &mut client_app] {
5861
app.add_plugins((
5962
MinimalPlugins,
63+
StatesPlugin,
6064
RepliconPlugins.set(ServerPlugin {
6165
tick_policy: TickPolicy::EveryFrame,
6266
..Default::default()
@@ -83,6 +87,7 @@ fn server_event() {
8387
for app in [&mut server_app, &mut client_app] {
8488
app.add_plugins((
8589
MinimalPlugins,
90+
StatesPlugin,
8691
RepliconPlugins.set(ServerPlugin {
8792
tick_policy: TickPolicy::EveryFrame,
8893
..Default::default()
@@ -114,6 +119,7 @@ fn client_event() {
114119
for app in [&mut server_app, &mut client_app] {
115120
app.add_plugins((
116121
MinimalPlugins,
122+
StatesPlugin,
117123
RepliconPlugins.set(ServerPlugin {
118124
tick_policy: TickPolicy::EveryFrame,
119125
..Default::default()

src/client.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ use postcard::experimental::max_size::MaxSize;
1111

1212
use crate::shared::{
1313
backend::{
14+
ClientState,
1415
replicon_channels::{ReplicationChannel, RepliconChannels},
1516
replicon_client::RepliconClient,
1617
},
17-
common_conditions::{client_connected, client_just_connected, client_just_disconnected},
1818
entity_serde, postcard_utils,
1919
replication::{
2020
Replicated,
@@ -51,10 +51,6 @@ impl Plugin for ClientPlugin {
5151
PreUpdate,
5252
(
5353
ClientSet::ReceivePackets,
54-
(
55-
ClientSet::ResetEvents.run_if(client_just_connected),
56-
ClientSet::Reset.run_if(client_just_disconnected),
57-
),
5854
ClientSet::Receive,
5955
ClientSet::Diagnostics,
6056
)
@@ -65,13 +61,16 @@ impl Plugin for ClientPlugin {
6561
(ClientSet::Send, ClientSet::SendPackets).chain(),
6662
)
6763
.add_systems(Startup, setup_channels)
64+
.add_systems(
65+
OnExit(ClientState::Connected),
66+
reset.in_set(ClientSet::Reset),
67+
)
6868
.add_systems(
6969
PreUpdate,
7070
receive_replication
7171
.in_set(ClientSet::Receive)
72-
.run_if(client_connected),
73-
)
74-
.add_systems(PreUpdate, reset.in_set(ClientSet::Reset));
72+
.run_if(in_state(ClientState::Connected)),
73+
);
7574
}
7675

7776
fn finish(&self, app: &mut App) {
@@ -156,11 +155,13 @@ pub(super) fn receive_replication(
156155
}
157156

158157
fn reset(
158+
mut client: ResMut<RepliconClient>,
159159
mut update_tick: ResMut<ServerUpdateTick>,
160160
mut entity_map: ResMut<ServerEntityMap>,
161161
mut buffered_mutations: ResMut<BufferedMutations>,
162162
stats: Option<ResMut<ClientReplicationStats>>,
163163
) {
164+
client.clear();
164165
*update_tick = Default::default();
165166
entity_map.clear();
166167
buffered_mutations.clear();
@@ -727,7 +728,7 @@ pub enum ClientSet {
727728
SendPackets,
728729
/// Systems that reset queued server events.
729730
///
730-
/// Runs in [`PreUpdate`] immediately after the client connects to ensure client sessions have a fresh start.
731+
/// Runs in [`OnEnter`] with [`ClientState::Connected`] to ensure client sessions have a fresh start.
731732
///
732733
/// This is a separate set from [`ClientSet::Reset`] because the reset requirements for events are different
733734
/// from the replicon client internals.
@@ -736,7 +737,7 @@ pub enum ClientSet {
736737
ResetEvents,
737738
/// Systems that reset the client.
738739
///
739-
/// Runs in [`PreUpdate`] when the client just disconnected.
740+
/// Runs in [`OnExit`] with [`ClientState::Disconnected`] (when the client just disconnected).
740741
///
741742
/// You may want to disable this set if you want to preserve client replication state across reconnects.
742743
/// In that case, you need to manually repair the client state (or use something like

0 commit comments

Comments
 (0)