Skip to content

Commit

Permalink
fix rollbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
cBournhonesque committed Jan 18, 2025
1 parent e6eb4ac commit 9c607be
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 56 deletions.
19 changes: 10 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ bevy = { version = "0.15", default-features = false, features = [
"multi_threaded",
"bevy_state",
"serialize",
"bevy_scene",
"bevy_asset",
"bevy_state",
"bevy_color",
"multi_threaded",
"sysinfo_plugin",
Expand All @@ -31,6 +31,8 @@ avian3d = { version = "0.2", default-features = false, features = [
"3d",
"f32",
"parry-f32",
"collider-from-mesh",
"bevy_scene",
"parallel",
"serialize",
] }
Expand Down
19 changes: 11 additions & 8 deletions client/src/player.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use avian3d::prelude::Position;
use avian3d::prelude::{Position, RigidBody};
use bevy::prelude::*;
use bevy::prelude::TransformSystem::TransformPropagate;
use bevy::window::PrimaryWindow;
Expand All @@ -24,19 +24,21 @@ impl Plugin for PlayerPlugin {
// .before(InputSystemSet::BufferClientInputs)
// .run_if(not(is_in_rollback)),
// );
app.add_systems(Update, add_input_map);
app.add_systems(Update, handle_predicted_spawn);
app.add_systems(PostUpdate, player_camera_system.after(TransformPropagate));
}
}


/// Add an InputMap to Predicted players so they can send inputs to the server
fn add_input_map(
/// Handle a newly spawned Predicted player:
/// - adds an InputMap to Predicted so that the user can control the predicted entity
/// - adds RigidBody::Kinematic so that physics are also applied to that entity on the client side
fn handle_predicted_spawn(
mut commands: Commands,
predicted_player: Query<Entity, (With<Controlled>, With<Player>, With<Predicted>, Without<InputMap<PlayerInput>>)>
) {
for entity in predicted_player.iter() {
commands.entity(entity).insert(InputMap::<PlayerInput>::default()
let input_map = InputMap::<PlayerInput>::default()
.with_multiple([
(PlayerInput::MoveForward, KeyCode::KeyW),
(PlayerInput::MoveBackward, KeyCode::KeyS),
Expand All @@ -51,11 +53,12 @@ fn add_input_map(
(PlayerInput::Weapon3, KeyCode::Digit3),
(PlayerInput::Weapon4, KeyCode::Digit4),
(PlayerInput::Weapon5, KeyCode::Digit5),
])
])
.with(PlayerInput::ShootPrimary, MouseButton::Left)
.with(PlayerInput::ShootSecondary, MouseButton::Right)
.with_dual_axis(PlayerInput::Look, MouseMove::default())
);
.with_dual_axis(PlayerInput::Look, MouseMove::default());

commands.entity(entity).insert((input_map, RigidBody::Kinematic));
}
}

Expand Down
5 changes: 5 additions & 0 deletions launcher/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use bevy::asset::AssetPlugin;
use bevy::diagnostic::DiagnosticsPlugin;
use bevy::hierarchy::HierarchyPlugin;
use bevy::prelude::*;
use bevy::scene::ScenePlugin;
use bevy::render::mesh::MeshPlugin;
use bevy::state::app::StatesPlugin;
use lightyear::prelude::server::*;
use crate::settings;
Expand Down Expand Up @@ -34,6 +36,9 @@ impl ServerApp {
file_path: settings::ASSETS_PATH.to_string(),
..default()
},
// the mesh asset is needed for avian collisions
MeshPlugin,
ScenePlugin,
settings::log_plugin(),
StatesPlugin,
HierarchyPlugin,
Expand Down
2 changes: 1 addition & 1 deletion launcher/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ pub(crate) fn window_plugin() -> WindowPlugin {
pub(crate) fn log_plugin() -> LogPlugin {
LogPlugin {
level: Level::INFO,
filter: "wgpu=error,bevy_render=info,bevy_ecs=warn,bevy_time=warn".to_string(),
filter: "wgpu=error,bevy_render=info,bevy_ecs=warn,bevy_time=warn,lightyear::client::prediction=debug".to_string(),
..default()
}
}
Expand Down
1 change: 1 addition & 0 deletions renderer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ server = []
shared = { path = "../shared", default-features = false }
lightyear.workspace = true
bevy-inspector-egui.workspace = true
avian3d.workspace = true
bevy = { workspace = true, features = [
"bevy_asset",
"bevy_render",
Expand Down
15 changes: 14 additions & 1 deletion renderer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod player;
mod weapon;

use bevy::prelude::*;
use lightyear::client::interpolation::VisualInterpolationPlugin;

pub struct RendererPlugin;

Expand All @@ -16,8 +17,20 @@ impl Plugin for RendererPlugin {
..default()
});

#[cfg(feature = "client")]
// we use Position and Rotation as primary source of truth, so no need to sync changes
// from Transform->Pos, just Pos->Transform.
// Also we apply visual interpolation on Transform, but that's just for visuals, we want the real
// value to still come from Position/Rotation
app.insert_resource(avian3d::sync::SyncConfig {
transform_to_position: false,
position_to_transform: true,
..default()
});
app.add_plugins(VisualInterpolationPlugin::<Transform>::default());

// SYSTEMS
// TODO: separate client renderer from server renderer?
// TODO: separate client renderer from server renderer? The features cfg are not enough
// on the server, the camera doesn't follow a player
#[cfg(not(feature = "client"))]
app.add_systems(Startup, init);
Expand Down
13 changes: 9 additions & 4 deletions renderer/src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use bevy::color::palettes::basic::BLUE;
use bevy::core_pipeline::prepass::DepthPrepass;
use bevy::prelude::*;
use bevy::render::camera::Exposure;
use lightyear::prelude::client::Confirmed;
use lightyear::prelude::client::{Confirmed, Predicted, VisualInterpolateStatus};
use lightyear::shared::replication::components::Controlled;
use shared::player::Player;

Expand All @@ -21,15 +21,21 @@ impl Plugin for PlayerPlugin {
/// Add meshes/visuals for spawned players
fn spawn_visuals(
// we do not want to add visuals to confirmed entities on the client
query: Query<(Entity, Has<Controlled>), (Without<Confirmed>, Added<Player>)>,
query: Query<(Entity, Has<Controlled>, Has<Predicted>), (Without<Confirmed>, Added<Player>)>,
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
// mut atomized_materials: ResMut<Assets<AtomizedMaterial>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
query.iter().for_each(|(parent, is_controlled)| {
query.iter().for_each(|(parent, is_controlled, is_predicted)| {
// add visibility
commands.entity(parent).insert(Visibility::default());

// TODO: don't do this in host-server mode!
// add visual interpolation on the predicted entity
if is_predicted {
commands.entity(parent).insert(VisualInterpolateStatus::<Transform>::default());
}
// add lights
// TODO: why do we need it as a child? so we can specify a direction (via Transform) to the light?
commands.entity(parent).with_children(|parent| {
Expand Down Expand Up @@ -80,7 +86,6 @@ fn spawn_visuals(
fov: 90.0_f32.to_radians(),
..default()
}),
Transform::default(),
));
}
});
Expand Down
3 changes: 2 additions & 1 deletion server/src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ fn spawn_player_on_connect(mut commands: Commands, mut events: EventReader<Conne
// ],
// }),
RigidBody::Kinematic,
Collider::sphere(0.5),
// TODO: contacts are not fully deterministic in avian!
// Collider::sphere(0.5),
)
);
}
Expand Down
28 changes: 17 additions & 11 deletions shared/src/network/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ use bevy::prelude::*;
use leafwing_input_manager::{Actionlike, InputControlKind};
use lightyear::prelude::*;
use avian3d::prelude::*;
use lightyear::prelude::client::ComponentSyncMode;
use lightyear::prelude::client::{ComponentSyncMode, LerpFn};
use lightyear::utils::avian3d::{position, rotation};
use crate::player::Player;
use lightyear::utils::bevy::TransformLinearInterpolation;

pub struct ProtocolPlugin;

Expand Down Expand Up @@ -68,17 +69,16 @@ impl Plugin for ProtocolPlugin {
app.register_component::<ExternalImpulse>(ChannelDirection::ServerToClient)
.add_prediction(ComponentSyncMode::Full);

app.register_component::<Transform>(ChannelDirection::ServerToClient)
.add_prediction(ComponentSyncMode::Full);

app.register_component::<ComputedMass>(ChannelDirection::ServerToClient)
.add_prediction(ComponentSyncMode::Full);
// app.register_component::<Transform>(ChannelDirection::ServerToClient)
// .add_prediction(ComponentSyncMode::Full)
// .add_interpolation_fn(<TransformLinearInterpolation as LerpFn<Transform>>::lerp);
// // .add_correction_fn(<TransformLinearInterpolation as LerpFn<Transform>>::lerp);

// Position and Rotation have a `correction_fn` set, which is used to smear rollback errors
// over a few frames, just for the rendering part in postudpate.
//
// They also set `interpolation_fn` which is used by the VisualInterpolationPlugin to smooth
// out rendering between fixedupdate ticks.
// // Position and Rotation have a `correction_fn` set, which is used to smear rollback errors
// // over a few frames, just for the rendering part in postudpate.
// //
// // They also set `interpolation_fn` which is used by the VisualInterpolationPlugin to smooth
// // out rendering between fixedupdate ticks.
app.register_component::<Position>(ChannelDirection::ServerToClient)
.add_prediction(ComponentSyncMode::Full)
.add_interpolation_fn(position::lerp)
Expand All @@ -88,5 +88,11 @@ impl Plugin for ProtocolPlugin {
.add_prediction(ComponentSyncMode::Full)
.add_interpolation_fn(rotation::lerp)
.add_correction_fn(rotation::lerp);

// do not replicate Transform but make sure to register an interpolation function
// for it so that we can do visual interpolation
// (another option would be to replicate transform and not use Position/Rotation at all)
app.add_interpolation::<Transform>(ComponentSyncMode::None);
app.add_interpolation_fn::<Transform>(TransformLinearInterpolation::lerp);
}
}
30 changes: 28 additions & 2 deletions shared/src/physics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,33 @@ pub struct PhysicsPlugin;

impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut App) {
// TODO: make adjustments similar to lightyear examples
app.add_plugins(PhysicsPlugins::default());
// PLUGINS
app.add_plugins(PhysicsPlugins::default()
.build()
// disable sync to add the sync plugin in a different schedule
.disable::<SyncPlugin>()
.disable::<PhysicsInterpolationPlugin>());
// as an optimization, we run the sync plugin in RunFixedMainLoop (outside FixedMainLoop)
// so that in the case of a rollback we don't do the sync again
app.add_plugins(SyncPlugin::new(RunFixedMainLoop));

// RESOURCES

// Position and Rotation are the primary source of truth so no need to
// sync changes from Transform to Position.
// NOTE: this is CRUCIAL to avoid rollbacks! presumably because on the client
// we modify Transform in PostUpdate, which triggers the Sync from transform->position systems in avian
// Maybe those systems cause some numerical instability?
app.insert_resource(avian3d::sync::SyncConfig {
transform_to_position: false,
position_to_transform: true,
..default()
});
// disable sleeping
app.insert_resource(SleepingThreshold {
linear: -0.01,
angular: -0.01,
});
app.insert_resource(Gravity::ZERO);
}
}
Loading

0 comments on commit 9c607be

Please sign in to comment.