Skip to content
This repository has been archived by the owner on Nov 10, 2024. It is now read-only.

Commit

Permalink
don't rollback if prediction matches snapshot
Browse files Browse the repository at this point in the history
  • Loading branch information
RJ committed Sep 3, 2023
1 parent fbe84d3 commit 97f20d6
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 15 deletions.
26 changes: 22 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,27 @@
//! Assumes game logic uses bevy's `FixedUpdate` schedule.
//!
//! Current status: under development alongside a multiplayer server-authoritative game.
//! I am "RJ" on bevy's discord, in the networking channel, if you want to discuss.
//!
//! ## Typical scenario this crate is built for
//! ## "Sports Games"
//!
//! This crate is built for so called "sports games" where dynamic entities interact in the same
//! timeframe. That means clients simulate all entities into the future, in the same timeframe as
//! the local player. This necessitates predicting player inputs.
//!
//! This is quite different to traditional Quake style FPS netcode, which is for a mostly static
//! world, with players running around shooting eachother. In Quake style, you are always seeing
//! a delayed version of other players, interpolated between two snapshots. The server does
//! backwards reconcilliation / lag compensation to verify hits.
//!
//! ### Example rollback scenario:
//!
//! In your client/server game:
//!
//! - client is simulating frame 10
//! - server snapshot for frame 6 arrives, including values for an entity's component T
//! - client updates entity's ServerSnapshot<T> value at frame 6 (ie, the past)
//! - Timewarp triggers a rollback to frame 6:
//! - Timewarp triggers a rollback to frame 6 if snapshot != our stored value for frame 6.
//! - - winds back frame counter to 6
//! - copies the server snapshot value to the component
//! - resimulates frames 7,8,9,10 as fast as possible
Expand Down Expand Up @@ -116,7 +128,7 @@
//! ```
//!
//! then timewarp will capture the before and after versions of components when doing a rollback,
//! and put it into a [`TimewarpCorrection`] component for your game to examine.
//! and put it into a [`TimewarpCorrection<Position>`] component for your game to examine.
//! Typically this would be useful for some visual smoothing - you might gradually blend over the
//! error distance with your sprite, even though the underlying physical simulation snapped correct.
//!
Expand Down Expand Up @@ -165,6 +177,9 @@
//! - Littered with a variety of debug logging, set your log level accordingly
//! - Unoptimized: clones components each frame without checking if they've changed.
//! - Doesn't rollback resources or other things, just (registered) component data.
//! - Registered components must impl `PartialEq`
//! - I'm using a patched version of `bevy_xpbd` at the mo, to make `Collider` impl `PartialEq`
//! (PRs sent..)
//!
pub(crate) mod components;
mod error;
Expand Down Expand Up @@ -218,7 +233,10 @@ pub struct TimewarpPlugin {
impl TimewarpPlugin {
pub fn new(rollback_window: FrameNumber, after_set: impl SystemSet) -> Self {
Self {
config: TimewarpConfig { rollback_window },
config: TimewarpConfig {
rollback_window,
..default()
},
after_set: Box::new(after_set),
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ use crate::FrameNumber;
use bevy::prelude::*;
use std::{ops::Range, time::Duration};

#[derive(Resource, Debug, Copy, Clone)]
#[derive(Resource, Debug, Copy, Clone, Default)]
pub struct TimewarpConfig {
/// how many frames of old component values should we buffer?
/// can't roll back any further than this. will depend on network lag and game mechanics.
pub rollback_window: FrameNumber,
/// if set to true, a rollback will be initiated even if
/// the stored predicted value matches the server snapshot.
/// meant as a worst-case scenario for checking performance really.
pub force_rollback_always: bool,
}

/// Updated whenever we perform a rollback
Expand Down
23 changes: 18 additions & 5 deletions src/systems.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,10 @@ pub(crate) fn record_component_history<T: TimewarpComponent>(
if rb.range.end == game_clock.frame() {
if let Some(old_val) = comp_hist.at_frame(game_clock.frame()) {
if *old_val != *comp {
warn!("Generating Correction");
warn!(
"Generating Correction for {entity:?} old:{:?} new{:?}",
old_val, comp
);
if let Some(mut correction) = opt_correction {
correction.before = old_val.clone();
correction.after = comp.clone();
Expand Down Expand Up @@ -194,17 +197,16 @@ pub(crate) fn insert_components_at_prior_frames<T: TimewarpComponent>(
}
// if the entity never had this component type T before, we'll need to insert
// the ComponentHistory and ServerSnapshot components.
// If they already exist, just insert at the correct frame.
// NOTE: don't insert historial value into ComponentHistory, only ServerSnapshot.
// let trigger_rollback_when... copy it to CH, or things break.
if let Some(mut ch) = opt_ch {
ch.insert(icaf.frame, icaf.component.clone(), &entity);
ch.report_birth_at_frame(icaf.frame);
trace!("Inserting component at past frame for existing ComponentHistory");
} else {
let mut ch = ComponentHistory::<T>::with_capacity(
let ch = ComponentHistory::<T>::with_capacity(
timewarp_config.rollback_window as usize,
icaf.frame,
);
ch.insert(icaf.frame, icaf.component.clone(), &entity);
ent_cmd.insert(ch);
trace!("Inserting component at past frame by inserting new ComponentHistory");
}
Expand Down Expand Up @@ -254,12 +256,23 @@ pub(crate) fn trigger_rollback_when_snapshot_added<T: TimewarpComponent>(
continue;
}
tw_status.set_snapped_at(snap_frame);

// insert into comp history, because if we rollback exactly to snap-frame
// the `apply_snapshot_to_component` won't have run, and we need it in there.
let comp_from_snapshot = server_snapshot
.at_frame(snap_frame)
.expect("snap_frame must have a value here");

// check if our historical value for the snap_frame is the same as what snapshot says
// because if they match, we predicted successfully, and there's no need to rollback.
if let Some(stored_comp_val) = comp_hist.at_frame(snap_frame) {
if *stored_comp_val == *comp_from_snapshot {
// a correct prediction, no need to rollback. hooray!
// info!("skipping rollback 🎖️ {stored_comp_val:?}");
continue;
}
}

comp_hist.insert(snap_frame, comp_from_snapshot.clone(), &entity);

debug!("f={:?} SNAPPING and Triggering rollback due to snapshot. {entity:?} snap_frame: {snap_frame}", game_clock.frame());
Expand Down
5 changes: 3 additions & 2 deletions tests/basic_rollback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,14 +204,15 @@ fn basic_rollback() {
// so lets make the server confirm that:
ss_e2.insert(5, Enemy { health: 97 });

tick(&mut app); // frame 8, expecting another rollback
tick(&mut app); // frame 8, potential rollback

// but no - our prediction matches the snapshot so it didn't roll back.
assert_eq!(
app.world
.get_resource::<RollbackStats>()
.unwrap()
.num_rollbacks,
2
1
);

assert_eq!(app.comp_val_at::<Enemy>(e2, 8).unwrap().health, 94);
Expand Down
5 changes: 2 additions & 3 deletions tests/error_corrections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ fn error_correction() {

tick(&mut app); // frame 5, we expect a rollback


assert!(app.world.get::<TimewarpCorrection<Enemy>>(e1).is_some());

assert_eq!(
Expand Down Expand Up @@ -173,14 +172,14 @@ fn error_correction() {
let mut ss_e1 = app.world.get_mut::<ServerSnapshot<Enemy>>(e1).unwrap();
ss_e1.insert(7, Enemy { health: 95 });

tick(&mut app); // frame 10 - rollback
tick(&mut app); // frame 10 - rollback? no. should be bypassed because prediction was right

assert_eq!(
app.world
.get_resource::<RollbackStats>()
.unwrap()
.num_rollbacks,
2
1
);

assert_eq!(app.comp_val_at::<Enemy>(e1, 10).unwrap().health, 92);
Expand Down

0 comments on commit 97f20d6

Please sign in to comment.