Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve prediction api #9

Merged
merged 4 commits into from
May 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ you will have to implement it yourself.
```rust
use bevy_replicon_snap_macros::{Interpolate};

#[derive(Component, Deserialize, Serialize, Interpolate, Clone)]
#[derive(Component, Deserialize, Serialize, Interpolate, Clone)]
struct PlayerPosition(Vec2);
```

Expand All @@ -84,15 +84,45 @@ interpolated.
```rust
commands.spawn((
PlayerPosition(Vec2::ZERO),
Replication,
Replicated,
Interpolated,
...
));
```

### Client-Side Prediction

Coming soon.. In the meantime check the "predicted" example!
To use client side prediction you need to implement the `Predict` trait for any component and event combination to specify
how a event would mutate a component. This library will then use this implementation to generate respevtive server and client systems
that take care of predicting changes on client-side and correcting them should the server result be different.

```rust
impl Predict<MoveDirection> for PlayerPosition {
fn apply_event(&mut self, event: &MoveDirection, delta_time: f32) {
const MOVE_SPEED: f32 = 300.0;
self.0 += event.0 * delta_time * MOVE_SPEED;
}
}
```

Additionally you need to register the Event as a predicted event aswell as the event and component combination:

```rust
app
.add_client_predicted_event::<MoveDirection>(ChannelKind::Ordered)
.predict_event_for_component::<MoveDirection, PlayerPosition>()
```

Finally, make sure the entities that should be predicted have the `OwnerPredicted` component:

```rust
commands.spawn((
PlayerPosition(Vec2::ZERO),
Replicated,
OwnerPredicted,
...
));
```

## Alternatives

Expand Down
3 changes: 2 additions & 1 deletion examples/interpolated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ use bevy_replicon_renet::{
RenetChannelsExt, RepliconRenetPlugins,
};
use bevy_replicon_snap::{
interpolation::Interpolated, AppInterpolationExt, NetworkOwner, SnapshotInterpolationPlugin,
interpolation::{AppInterpolationExt, Interpolated},
NetworkOwner, SnapshotInterpolationPlugin,
};
use bevy_replicon_snap_macros::Interpolate;
use clap::Parser;
Expand Down
75 changes: 10 additions & 65 deletions examples/owner_predicted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ use std::{
};

use bevy::{prelude::*, winit::UpdateMode::Continuous, winit::WinitSettings};
use bevy_replicon::{
client::confirmed::{self, Confirmed},
prelude::*,
};
use bevy_replicon::prelude::*;
use bevy_replicon_renet::{
renet::{
transport::{
Expand All @@ -23,9 +20,10 @@ use bevy_replicon_renet::{
RenetChannelsExt, RepliconRenetPlugins,
};
use bevy_replicon_snap::{
interpolation::Interpolated, interpolation::SnapshotBuffer, prediction::OwnerPredicted,
prediction::Predicted, prediction::PredictedEventHistory, AppInterpolationExt, NetworkOwner,
SnapshotInterpolationPlugin,
interpolation::AppInterpolationExt,
prediction::OwnerPredicted,
prediction::{AppPredictionExt, Predict},
NetworkOwner, SnapshotInterpolationPlugin,
};
use bevy_replicon_snap_macros::Interpolate;
use clap::Parser;
Expand Down Expand Up @@ -65,15 +63,14 @@ impl Plugin for SimpleBoxPlugin {
app.replicate_interpolated::<PlayerPosition>()
.replicate::<PlayerColor>()
.add_client_predicted_event::<MoveDirection>(ChannelKind::Ordered)
.predict_event_for_component::<MoveDirection, PlayerPosition>()
.add_systems(
Startup,
(Self::cli_system.map(Result::unwrap), Self::init_system),
)
.add_systems(
Update,
(
Self::movement_system.run_if(has_authority), // Runs only on the server or a single player.
Self::predicted_movement_system.run_if(resource_exists::<RenetClient>), // Runs only on clients.
Self::server_event_system.run_if(resource_exists::<RenetServer>), // Runs only on the server.
(Self::draw_boxes_system, Self::input_system),
),
Expand Down Expand Up @@ -230,64 +227,12 @@ impl SimpleBoxPlugin {
move_events.send(MoveDirection(direction.normalize_or_zero()));
}
}
}

/// Mutates [`PlayerPosition`] based on [`MoveCommandEvents`].
/// Server implementation
fn movement_system(
time: Res<Time>,
mut move_events: EventReader<FromClient<MoveDirection>>,
mut players: Query<(&NetworkOwner, &mut PlayerPosition), Without<Predicted>>,
) {
for FromClient { client_id, event } in move_events.read() {
for (player, mut position) in &mut players {
if client_id.get() == player.0 {
Self::apply_move_command(&mut *position, event, time.delta_seconds())
}
}
}
}

// Client prediction implementation
fn predicted_movement_system(
mut q_predicted_players: Query<
(
Entity,
&mut PlayerPosition,
&SnapshotBuffer<PlayerPosition>,
&Confirmed,
),
(With<Predicted>, Without<Interpolated>),
>,
mut local_events: EventReader<MoveDirection>,
mut event_history: ResMut<PredictedEventHistory<MoveDirection>>,
time: Res<Time>,
) {
// Apply all pending inputs to latest snapshot
for (e, mut position, snapshot_buffer, confirmed) in q_predicted_players.iter_mut() {
// Append the latest input event
for event in local_events.read() {
event_history.insert(
event.clone(),
confirmed.last_tick().get(),
time.delta_seconds(),
);
}

let mut corrected_position = snapshot_buffer.latest_snapshot().0;
for event_snapshot in event_history.predict(snapshot_buffer.latest_snapshot_tick()) {
Self::apply_move_command(
&mut corrected_position,
&event_snapshot.value,
event_snapshot.delta_time,
);
}
position.0 = corrected_position;
}
}

fn apply_move_command(position: &mut Vec2, event: &MoveDirection, delta_time: f32) {
impl Predict<MoveDirection> for PlayerPosition {
fn apply_event(&mut self, event: &MoveDirection, delta_time: f32) {
const MOVE_SPEED: f32 = 300.0;
*position += event.0 * delta_time * MOVE_SPEED;
self.0 += event.0 * delta_time * MOVE_SPEED;
}
}

Expand Down
111 changes: 106 additions & 5 deletions src/interpolation.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
use std::collections::VecDeque;
use std::{collections::VecDeque, io::Cursor};

use bevy::{
app::{App, PreUpdate},
ecs::{
component::Component,
query::{With, Without},
system::{Query, Res, Resource},
entity::Entity,
query::{Added, Or, With, Without},
schedule::{common_conditions::resource_exists, IntoSystemConfigs},
system::{Commands, Query, Res, Resource},
world::EntityMut,
},
reflect::Reflect,
time::Time,
utils::default,
};
use serde::{Deserialize, Serialize};
use bevy_replicon::{
bincode,
core::{
command_markers::{AppMarkerExt, MarkerConfig},
replication_fns::{
ctx::{RemoveCtx, WriteCtx},
rule_fns::RuleFns,
},
replication_rules::AppRuleExt,
},
};
use bevy_replicon_renet::renet::RenetClient;
use serde::{de::DeserializeOwned, Deserialize, Serialize};

use crate::prediction::Predicted;
use crate::{
prediction::{owner_prediction_init_system, predicted_snapshot_system, Predicted},
InterpolationSet,
};

pub trait Interpolate {
fn interpolate(&self, other: Self, t: f32) -> Self;
Expand All @@ -38,6 +58,9 @@ pub struct SnapshotInterpolationConfig {
pub max_tick_rate: u16,
}

#[derive(Component)]
pub struct RecordSnapshotsMarker;

impl<T: Component + Interpolate + Clone> SnapshotBuffer<T> {
pub fn new() -> Self {
Self {
Expand Down Expand Up @@ -92,3 +115,81 @@ pub fn snapshot_interpolation_system<T: Component + Interpolate + Clone>(
snapshot_buffer.time_since_last_snapshot += time.delta_seconds();
}
}

/// Add a marker to all components requiring a snapshot buffer
pub fn snapshot_buffer_init_system<T: Component + Interpolate + Clone>(
q_new: Query<(Entity, &T), Or<(Added<Predicted>, Added<Interpolated>)>>,
mut commands: Commands,
) {
for (e, _v) in q_new.iter() {
commands.entity(e).insert(RecordSnapshotsMarker);
}
}

pub fn write_snap_component<C: Clone + Interpolate + Component + DeserializeOwned>(
ctx: &mut WriteCtx,
rule_fns: &RuleFns<C>,
entity: &mut EntityMut,
cursor: &mut Cursor<&[u8]>,
) -> bincode::Result<()> {
let component: C = rule_fns.deserialize(ctx, cursor)?;
if let Some(mut buffer) = entity.get_mut::<SnapshotBuffer<C>>() {
buffer.insert(component, ctx.message_tick.get());
} else {
let mut buffer = SnapshotBuffer::new();
buffer.insert(component, ctx.message_tick.get());
ctx.commands.entity(entity.id()).insert(buffer);
}

Ok(())
}

fn remove_snap_component<C: Clone + Interpolate + Component + DeserializeOwned>(
ctx: &mut RemoveCtx,
entity: &mut EntityMut,
) {
ctx.commands
.entity(entity.id())
.remove::<SnapshotBuffer<C>>()
.remove::<C>();
}
pub trait AppInterpolationExt {
/// Register a component to be replicated and interpolated between server updates
/// Requires the component to implement the Interpolate trait
fn replicate_interpolated<C>(&mut self) -> &mut Self
where
C: Component + Interpolate + Clone + Serialize + DeserializeOwned;
}

impl AppInterpolationExt for App {
fn replicate_interpolated<T>(&mut self) -> &mut Self
where
T: Component + Interpolate + Clone + Serialize + DeserializeOwned,
{
self.add_systems(
PreUpdate,
(snapshot_buffer_init_system::<T>.after(owner_prediction_init_system))
.in_set(InterpolationSet::Init)
.run_if(resource_exists::<RenetClient>),
);
self.add_systems(
PreUpdate,
(
snapshot_interpolation_system::<T>,
predicted_snapshot_system::<T>,
)
.chain()
.in_set(InterpolationSet::Interpolate)
.run_if(resource_exists::<RenetClient>),
)
.replicate::<T>()
.register_marker_with::<RecordSnapshotsMarker>(MarkerConfig {
need_history: true,
..default()
})
.set_marker_fns::<RecordSnapshotsMarker, T>(
write_snap_component::<T>,
remove_snap_component::<T>,
)
}
}
Loading
Loading