Skip to content

Commit

Permalink
Support rolling back resources (#622)
Browse files Browse the repository at this point in the history
* Format avian_3d_character's cargo file

* Don't require ReadyBuffer item to implement PartialEq

* Support rolling back resources

---------

Co-authored-by: Nick Eaton <[email protected]>
  • Loading branch information
nick-e and Nick Eaton authored Sep 4, 2024
1 parent 8b50b69 commit 2cd8cf8
Show file tree
Hide file tree
Showing 8 changed files with 394 additions and 25 deletions.
24 changes: 12 additions & 12 deletions examples/avian_3d_character/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,26 @@ lightyear_examples_common = { path = "../common" }
bevy_screen_diagnostics = "0.6"
leafwing-input-manager = "0.15"
avian3d = { version = "0.1.1", default-features = false, features = [
"3d",
"f32",
"parry-f32",
"parallel",
"serialize",
"3d",
"f32",
"parry-f32",
"parallel",
"serialize",
] }
lightyear = { path = "../../lightyear", features = [
"webtransport",
"websocket",
"leafwing",
"avian3d",
"webtransport",
"websocket",
"leafwing",
"avian3d",
] }
serde = { version = "1.0.188", features = ["derive"] }
anyhow = { version = "1.0.75", features = [] }
tracing = "0.1"
tracing-subscriber = "0.3.17"
bevy = { version = "0.14", features = [
"multi_threaded",
"bevy_state",
"serialize",
"multi_threaded",
"bevy_state",
"serialize",
] }

rand = "0.8.1"
Expand Down
2 changes: 1 addition & 1 deletion examples/avian_3d_character/assets/settings.ron
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MySettings(
server_replication_send_interval: 50,
input_delay_ticks: 4,
input_delay_ticks: 6,
// do not set a limit on the amount of prediction
max_prediction_ticks: 100,
correction_ticks_factor: 2.0,
Expand Down
1 change: 1 addition & 0 deletions lightyear/src/client/prediction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod pre_prediction;
pub mod predicted_history;
pub mod prespawn;
pub(crate) mod resource;
pub mod resource_history;
pub mod rollback;
pub mod spawn;

Expand Down
16 changes: 14 additions & 2 deletions lightyear/src/client/prediction/plugin.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use bevy::prelude::{
not, App, Component, Condition, FixedPostUpdate, IntoSystemConfigs, IntoSystemSetConfigs,
Plugin, PostUpdate, PreUpdate, Res, SystemSet,
Plugin, PostUpdate, PreUpdate, Res, Resource, SystemSet,
};
use bevy::reflect::Reflect;
use bevy::transform::TransformSystem;
Expand Down Expand Up @@ -29,9 +29,10 @@ use crate::shared::sets::{ClientMarker, InternalMainSet};

use super::pre_prediction::PrePredictionPlugin;
use super::predicted_history::{add_component_history, apply_confirmed_update};
use super::resource_history::update_resource_history;
use super::rollback::{
check_rollback, increment_rollback_tick, prepare_rollback, prepare_rollback_non_networked,
prepare_rollback_prespawn, run_rollback, Rollback, RollbackState,
prepare_rollback_prespawn, prepare_rollback_resource, run_rollback, Rollback, RollbackState,
};
use super::spawn::spawn_predicted_entity;

Expand Down Expand Up @@ -199,6 +200,17 @@ pub fn add_non_networked_rollback_systems<C: Component + PartialEq + Clone>(app:
);
}

pub fn add_resource_rollback_systems<R: Resource + Clone>(app: &mut App) {
app.add_systems(
PreUpdate,
prepare_rollback_resource::<R>.in_set(PredictionSet::PrepareRollback),
);
app.add_systems(
FixedPostUpdate,
update_resource_history::<R>.in_set(PredictionSet::UpdateHistory),
);
}

pub fn add_prediction_systems<C: SyncComponent>(app: &mut App, prediction_mode: ComponentSyncMode) {
app.add_systems(
PreUpdate,
Expand Down
262 changes: 262 additions & 0 deletions lightyear/src/client/prediction/resource_history.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
//! There's a lot of overlap with `client::prediction_history` because resources are components in ECS so rollback is going to look similar.
use bevy::prelude::*;

use crate::{
prelude::{Tick, TickManager},
utils::ready_buffer::ReadyBuffer,
};

use super::rollback::Rollback;

/// Stores a past update for a resource
#[derive(Debug, PartialEq, Clone)]
pub(crate) enum ResourceState<R> {
/// the resource just got removed
Removed,
/// the resource got updated
Updated(R),
}

/// To know if we need to do rollback, we need to compare the resource's history with the server's state updates
#[derive(Resource, Debug)]
pub(crate) struct ResourceHistory<R> {
// We will only store the history for the ticks where the resource got updated
pub buffer: ReadyBuffer<Tick, ResourceState<R>>,
}

impl<R> Default for ResourceHistory<R> {
fn default() -> Self {
Self {
buffer: ReadyBuffer::new(),
}
}
}

impl<R> PartialEq for ResourceHistory<R> {
fn eq(&self, other: &Self) -> bool {
let mut self_history: Vec<_> = self.buffer.heap.iter().collect();
let mut other_history: Vec<_> = other.buffer.heap.iter().collect();
self_history.sort_by_key(|item| item.key);
other_history.sort_by_key(|item| item.key);
self_history.eq(&other_history)
}
}

impl<R: Clone> ResourceHistory<R> {
/// Reset the history for this resource
pub(crate) fn clear(&mut self) {
self.buffer = ReadyBuffer::new();
}

/// Add to the buffer that we received an update for the resource at the given tick
pub(crate) fn add_update(&mut self, tick: Tick, resource: R) {
self.buffer.push(tick, ResourceState::Updated(resource));
}

/// Add to the buffer that the resource got removed at the given tick
pub(crate) fn add_remove(&mut self, tick: Tick) {
self.buffer.push(tick, ResourceState::Removed);
}

// TODO: check if this logic is necessary/correct?
/// Clear the history of values strictly older than the specified tick,
/// and return the most recent value that is older or equal to the specified tick.
/// NOTE: That value is written back into the buffer
///
/// CAREFUL:
/// the resource history will only contain the ticks where the resource got updated, and otherwise
/// contains gaps. Therefore, we need to always leave a value in the history buffer so that we can
/// get the values for the future ticks
pub(crate) fn pop_until_tick(&mut self, tick: Tick) -> Option<ResourceState<R>> {
self.buffer.pop_until(&tick).map(|(tick, state)| {
// TODO: this clone is pretty bad and avoidable. Probably switch to a sequence buffer?
self.buffer.push(tick, state.clone());
state
})
}
}

/// This system handles changes and removals of resources
pub(crate) fn update_resource_history<R: Resource + Clone>(
resource: Option<Res<R>>,
mut history: ResMut<ResourceHistory<R>>,
tick_manager: Res<TickManager>,
rollback: Res<Rollback>,
) {
// tick for which we will record the history (either the current client tick or the current rollback tick)
let tick = tick_manager.tick_or_rollback_tick(rollback.as_ref());

if let Some(resource) = resource {
if resource.is_changed() {
history.add_update(tick, resource.clone());
}
// resource does not exist, it might have been just removed
} else {
match history.buffer.peek_max_item() {
Some((_, ResourceState::Removed)) => (),
// if there is no latest item or the latest item isn't a removal then the resource just got removed.
_ => history.add_remove(tick),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::client::RollbackState;
use crate::prelude::AppComponentExt;
use crate::tests::stepper::BevyStepper;
use crate::utils::ready_buffer::ItemWithReadyKey;
use bevy::ecs::system::RunSystemOnce;

#[derive(Resource, Clone, PartialEq, Debug)]
struct TestResource(f32);

/// Test adding and removing updates to the resource history
#[test]
fn test_resource_history() {
let mut resource_history = ResourceHistory::<TestResource>::default();

// check when we try to access a value when the buffer is empty
assert_eq!(resource_history.pop_until_tick(Tick(0)), None);

// check when we try to access an exact tick
resource_history.add_update(Tick(1), TestResource(1.0));
resource_history.add_update(Tick(2), TestResource(2.0));
assert_eq!(
resource_history.pop_until_tick(Tick(2)),
Some(ResourceState::Updated(TestResource(2.0)))
);
// check that we cleared older ticks, and that the most recent value still remains
assert_eq!(resource_history.buffer.len(), 1);
assert!(resource_history.buffer.has_item(&Tick(2)));

// check when we try to access a value in-between ticks
resource_history.add_update(Tick(4), TestResource(4.0));
// we retrieve the most recent value older or equal to Tick(3)
assert_eq!(
resource_history.pop_until_tick(Tick(3)),
Some(ResourceState::Updated(TestResource(2.0)))
);
assert_eq!(resource_history.buffer.len(), 2);
// check that the most recent value got added back to the buffer at the popped tick
assert_eq!(
resource_history.buffer.heap.peek(),
Some(&ItemWithReadyKey {
key: Tick(2),
item: ResourceState::Updated(TestResource(2.0))
})
);
assert!(resource_history.buffer.has_item(&Tick(4)));

// check that nothing happens when we try to access a value before any ticks
assert_eq!(resource_history.pop_until_tick(Tick(0)), None);
assert_eq!(resource_history.buffer.len(), 2);

resource_history.add_remove(Tick(5));
assert_eq!(resource_history.buffer.len(), 3);

resource_history.clear();
assert_eq!(resource_history.buffer.len(), 0);
}

/// Test that the history gets updated correctly
/// 1. Updating the TestResource resource
/// 2. Removing the TestResource resource
/// 3. Updating the TestResource resource during rollback
/// 4. Removing the TestResource resource during rollback
#[test]
fn test_update_history() {
let mut stepper = BevyStepper::default();
stepper.client_app.add_resource_rollback::<TestResource>();

// 1. Updating TestResource resource
stepper
.client_app
.world_mut()
.insert_resource(TestResource(1.0));
stepper.frame_step();
stepper
.client_app
.world_mut()
.resource_mut::<TestResource>()
.0 = 2.0;
stepper.frame_step();
let tick = stepper.client_tick();
assert_eq!(
stepper
.client_app
.world_mut()
.get_resource_mut::<ResourceHistory<TestResource>>()
.expect("Expected resource history to be added")
.pop_until_tick(tick),
Some(ResourceState::Updated(TestResource(2.0))),
"Expected resource value to be updated in resource history"
);

// 2. Removing TestResource
stepper
.client_app
.world_mut()
.remove_resource::<TestResource>();
stepper.frame_step();
let tick = stepper.client_tick();
assert_eq!(
stepper
.client_app
.world_mut()
.get_resource_mut::<ResourceHistory<TestResource>>()
.expect("Expected resource history to be added")
.pop_until_tick(tick),
Some(ResourceState::Removed),
"Expected resource value to be removed in resource history"
);

// 3. Updating TestResource during rollback
let rollback_tick = Tick(10);
stepper
.client_app
.world_mut()
.insert_resource(Rollback::new(RollbackState::ShouldRollback {
current_tick: rollback_tick,
}));
stepper
.client_app
.world_mut()
.insert_resource(TestResource(3.0));
stepper
.client_app
.world_mut()
.run_system_once(update_resource_history::<TestResource>);
assert_eq!(
stepper
.client_app
.world_mut()
.get_resource_mut::<ResourceHistory<TestResource>>()
.expect("Expected resource history to be added")
.pop_until_tick(rollback_tick),
Some(ResourceState::Updated(TestResource(3.0))),
"Expected resource value to be updated in resource history"
);

// 4. Removing TestResource during rollback
stepper
.client_app
.world_mut()
.remove_resource::<TestResource>();
stepper
.client_app
.world_mut()
.run_system_once(update_resource_history::<TestResource>);
assert_eq!(
stepper
.client_app
.world_mut()
.get_resource_mut::<ResourceHistory<TestResource>>()
.expect("Expected resource history to be added")
.pop_until_tick(rollback_tick),
Some(ResourceState::Removed),
"Expected resource value to be removed from resource history"
);
}
}
Loading

0 comments on commit 2cd8cf8

Please sign in to comment.