From 8013bd0d83684d62a4d0bdeb707bd92e262dbecb Mon Sep 17 00:00:00 2001 From: core Date: Sun, 17 May 2026 23:34:12 -0400 Subject: [PATCH] feat(netcode-rewrite): vastly improve client simulation accuracy --- crates/unified/src/client/interpolation.rs | 128 ++++++++++++++++++ crates/unified/src/client/mod.rs | 6 + .../src/client/planet/incoming_planets.rs | 33 ++++- crates/unified/src/client/rendering/mod.rs | 2 +- crates/unified/src/server/gravity.rs | 51 +------ crates/unified/src/server/orbit/mod.rs | 88 +----------- crates/unified/src/server/part.rs | 7 +- crates/unified/src/server/planets.rs | 7 +- crates/unified/src/shared/gravity.rs | 51 +++++++ crates/unified/src/shared/mod.rs | 2 + crates/unified/src/shared/net.rs | 7 +- crates/unified/src/shared/orbit.rs | 75 ++++++++++ 12 files changed, 310 insertions(+), 147 deletions(-) create mode 100644 crates/unified/src/client/interpolation.rs create mode 100644 crates/unified/src/shared/gravity.rs create mode 100644 crates/unified/src/shared/orbit.rs diff --git a/crates/unified/src/client/interpolation.rs b/crates/unified/src/client/interpolation.rs new file mode 100644 index 0000000000000000000000000000000000000000..c11e4cfdcf3ff29c3864b79b8082c521c7108970 --- /dev/null +++ b/crates/unified/src/client/interpolation.rs @@ -0,0 +1,128 @@ +use std::f64::consts::PI; +use bevy::math::DVec2; +use bevy_replicon::client::confirm_history::ConfirmHistory; +use bevy_replicon::prelude::Replicated; +use bevy_replicon::shared::replicon_tick::RepliconTick; +use crate::prelude::*; +use crate::shared::config::planet::PlanetSpring; + +const CORRECTION_FACTOR: f64 = 0.1; +const SNAP_THRESHOLD: f64 = 500.0; + +#[derive(Component, Default)] +struct LocalState { + position: DVec2, + linvel: DVec2, + angvel: f64, + rotation_cos: f64, + rotation_sin: f64, +} + +#[derive(Component, Default)] +struct CorrectionTarget { + position: DVec2, + linvel: DVec2, + angvel: f64, + rotation_cos: f64, + rotation_sin: f64, + last_tick: Option, +} + +pub fn prediction_plugin(app: &mut App) { + app + .add_observer(init_prediction) + .add_systems(PreUpdate, ( + save_local_state.before(bevy_replicon::client::ClientSystems::Receive), + record_server_correction.after(bevy_replicon::client::ClientSystems::Receive), + )) + .add_systems(FixedUpdate, apply_correction + .after(PhysicsSystems::Prepare) + .before(PhysicsSystems::StepSimulation)); +} + +fn init_prediction( + trigger: On, + query: Query<(&Position, &Rotation, &LinearVelocity, Option<&AngularVelocity>), Without>, + mut commands: Commands, +) { + let entity = trigger.event_target(); + let Ok((pos, rot, linvel, angvel)) = query.get(entity) else { return }; + commands.entity(entity).insert(( + LocalState { + position: pos.0, + linvel: linvel.0, + angvel: angvel.map(|a| a.0).unwrap_or(0.0), + rotation_cos: rot.cos, + rotation_sin: rot.sin, + }, + CorrectionTarget::default(), + )); +} + +fn save_local_state( + mut query: Query<(&Position, &Rotation, &LinearVelocity, &AngularVelocity, &mut LocalState)>, +) { + for (pos, rot, linvel, angvel, mut local) in &mut query { + if pos.0.is_nan() { continue; } + local.position = pos.0; + local.linvel = linvel.0; + local.angvel = angvel.0; + local.rotation_cos = rot.cos; + local.rotation_sin = rot.sin; + } +} + +fn record_server_correction( + mut query: Query<( + &mut Position, &mut Rotation, &mut LinearVelocity, &mut AngularVelocity, + &ConfirmHistory, + &mut CorrectionTarget, &LocalState, + )>, +) { + for (mut pos, mut rot, mut linvel, mut angvel, history, mut target, local) in &mut query { + let tick = history.last_tick(); + if target.last_tick != Some(tick) { + target.last_tick = Some(tick); + target.position = pos.0; + target.linvel = linvel.0; + target.angvel = angvel.0; + target.rotation_cos = rot.cos; + target.rotation_sin = rot.sin; + } + pos.0 = local.position; + rot.cos = local.rotation_cos; + rot.sin = local.rotation_sin; + linvel.0 = local.linvel; + angvel.0 = local.angvel; + } +} + +fn apply_correction( + mut query: Query<(&mut Position, &mut Rotation, &mut LinearVelocity, &mut AngularVelocity, &CorrectionTarget)>, +) { + for (mut pos, mut rot, mut linvel, mut angvel, target) in &mut query { + let Some(_) = target.last_tick else { continue }; + + let pos_err = target.position - pos.0; + if pos_err.length() > SNAP_THRESHOLD { + pos.0 = target.position; + linvel.0 = target.linvel; + angvel.0 = target.angvel; + rot.cos = target.rotation_cos; + rot.sin = target.rotation_sin; + } else { + pos.0 += pos_err * CORRECTION_FACTOR; + let cur_linvel = linvel.0; + linvel.0 += (target.linvel - cur_linvel) * CORRECTION_FACTOR; + let cur_angvel = angvel.0; + angvel.0 += (target.angvel - cur_angvel) * CORRECTION_FACTOR; + + let angle_local = rot.sin.atan2(rot.cos); + let angle_target = target.rotation_sin.atan2(target.rotation_cos); + let diff = ((angle_target - angle_local + PI).rem_euclid(2.0 * PI)) - PI; + let new_angle = angle_local + diff * CORRECTION_FACTOR; + rot.cos = new_angle.cos(); + rot.sin = new_angle.sin(); + } + } +} diff --git a/crates/unified/src/client/mod.rs b/crates/unified/src/client/mod.rs index 562f8cb6d5ed542e2c2dbcb113631a58ab71c587..2020c39e4781c3db0039859d79fb60d6ff578fc6 100644 --- a/crates/unified/src/client/mod.rs +++ b/crates/unified/src/client/mod.rs @@ -20,9 +20,12 @@ use planet::incoming_planets::incoming_planets_plugin; use crate::client::components::Me; use crate::client::ship::attachment::client_attachment_plugin; use crate::shared::ecs::GameplayState; +use crate::shared::gravity::update_gravity; use crate::shared::net::Hi; +use crate::shared::orbit::OrbitPlugin; pub mod colors; +pub mod interpolation; pub mod key_input; pub mod parts; pub mod planet; @@ -46,6 +49,9 @@ impl Plugin for ClientPlugin { app .init_gizmo_group::() .add_plugins(rendering::render_plugin) + .add_plugins(interpolation::prediction_plugin) + .add_plugins(OrbitPlugin) + .add_systems(FixedUpdate, update_gravity.before(PhysicsSystems::Prepare)) .add_plugins(input::input_plugin) .add_plugins(ship::thrusters::client_thrusters_plugin) .add_plugins((incoming_planets_plugin, indicators_plugin)) diff --git a/crates/unified/src/client/planet/incoming_planets.rs b/crates/unified/src/client/planet/incoming_planets.rs index 5295ef8e52688703c3eabfdbe7bc6a7271b4f7ee..b2473e2f951ca329e734a996183afd21da020bab 100644 --- a/crates/unified/src/client/planet/incoming_planets.rs +++ b/crates/unified/src/client/planet/incoming_planets.rs @@ -1,9 +1,12 @@ -use crate::shared::config::planet::{Planet, SpecialSpriteProperties}; +use crate::shared::config::planet::{Planet, PlanetSpring, PlanetSpringJoint, SpecialSpriteProperties}; use crate::prelude::*; use crate::shared::ecs::{MAIN_STAR_LAYERS}; +const PLANET_SPRING_COMPLIANCE: f64 = 0.01; +const PLANET_SPRING_DAMPING: f64 = 0.1; + pub fn incoming_planets_plugin(app: &mut App) { - app.add_systems(Update, (handle_incoming_planets, handle_updated_planets)); + app.add_systems(Update, (handle_incoming_planets, handle_updated_planets, setup_planet_spring_joints)); } fn build_planet_sprite(planet: &Planet, asset_server: &AssetServer) -> Sprite { @@ -40,3 +43,29 @@ fn handle_updated_planets( trace!(?updated_planet, "updated planet"); } } + +#[derive(Component)] +struct SpringJointReady; + +fn setup_planet_spring_joints( + unmatched_springs: Query<(Entity, &PlanetSpring), Without>, + planets: Query<(Entity, &Planet)>, + mut commands: Commands, +) { + for (spring_entity, spring) in unmatched_springs.iter() { + let Some((planet_entity, _)) = planets.iter().find(|(_, p)| p.name == spring.name) else { + continue; + }; + commands.entity(spring_entity).insert(( + RigidBody::Kinematic, + LinearVelocity::default(), + SpringJointReady, + )); + commands.spawn(( + PlanetSpringJoint { name: spring.name.clone() }, + FixedJoint::new(planet_entity, spring_entity) + .with_point_compliance(PLANET_SPRING_COMPLIANCE), + JointDamping { linear: PLANET_SPRING_DAMPING, angular: 1.0 }, + )); + } +} diff --git a/crates/unified/src/client/rendering/mod.rs b/crates/unified/src/client/rendering/mod.rs index b9fdbaf5ee36e52a37b83f86bfafef36aea02bcd..6be05acdcc1a7826dbdbe7113161689f0e31c3b2 100644 --- a/crates/unified/src/client/rendering/mod.rs +++ b/crates/unified/src/client/rendering/mod.rs @@ -3,8 +3,8 @@ use bevy::app::{App, Startup}; use bevy::core_pipeline::tonemapping::DebandDither; use bevy::post_process::bloom::Bloom; use crate::client::components::{MainCamera, Me}; -use crate::client::starguide::components::StarguideGizmos; use crate::shared::ecs::{GameplayState, MAIN_LAYER, STARGUIDE_LAYER}; +use crate::client::starguide::components::StarguideGizmos; use crate::prelude::*; pub fn render_plugin(app: &mut App) { diff --git a/crates/unified/src/server/gravity.rs b/crates/unified/src/server/gravity.rs index f73c1c2e2701a78bda1fa6df676aeafab9c1a4a0..0799c75a61831f9a3345cfe0d055a9914582406d 100644 --- a/crates/unified/src/server/gravity.rs +++ b/crates/unified/src/server/gravity.rs @@ -1,56 +1,7 @@ -use crate::shared::config::planet::Planet; -use crate::shared::ecs::Part; use crate::prelude::*; use crate::server::system_sets::WorldUpdateSet; -use crate::shared::world_config::WorldConfigResource; +use crate::shared::gravity::update_gravity; pub fn newtonian_gravity_plugin(app: &mut App) { app.add_systems(Update, update_gravity.in_set(WorldUpdateSet)); } - -fn update_gravity( - mut part_query: Query<(&Transform, &LinearVelocity, &Mass, &mut ConstantForce), With>, - planet_query: Query<(&Transform, &Mass), With>, - world_config: Res, - time: Res