From 10afa15d4a9a5bf9df7f161eccc6b8e2f9116a7f Mon Sep 17 00:00:00 2001 From: core Date: Tue, 19 May 2026 22:31:02 -0400 Subject: [PATCH] feat(netcode-rewrite): remove interpolation --- crates/unified/src/client/interpolation.rs | 330 --------------------- crates/unified/src/client/mod.rs | 2 - crates/unified/src/server/mod.rs | 33 ++- crates/unified/src/shared/net.rs | 3 +- 4 files changed, 23 insertions(+), 345 deletions(-) delete mode 100644 crates/unified/src/client/interpolation.rs diff --git a/crates/unified/src/client/interpolation.rs b/crates/unified/src/client/interpolation.rs deleted file mode 100644 index e8cba98a136c0f17d4fbfb7ece647d6b5a8aab00..0000000000000000000000000000000000000000 --- a/crates/unified/src/client/interpolation.rs +++ /dev/null @@ -1,330 +0,0 @@ -use std::collections::HashSet; -use bevy::math::DVec2; -use bevy::transform::TransformSystems; -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::{Planet, PlanetSpring}; -use crate::shared::world_config::WorldConfigResource; - -const SKIP_THRESHOLD: f64 = 10.0; - -#[derive(Component, Default)] -struct LocalState { - position: DVec2, - linvel: DVec2, - angvel: f64, - rotation_cos: f64, - rotation_sin: f64, -} - -#[derive(Component, Default)] -struct CorrectionTarget { - needs_correction: bool, - position: DVec2, - linvel: DVec2, - angvel: f64, - rotation_cos: f64, - rotation_sin: f64, - last_tick: Option, -} - -#[derive(Resource, Default)] -struct LocalTick(u64); - -#[derive(Resource)] -struct LagEstimate { - horizon: f64, - last_local_tick: u64, -} - -impl Default for LagEstimate { - fn default() -> Self { - Self { horizon: 4.0, last_local_tick: 0 } - } -} - -#[derive(Resource, Default)] -struct PlanetPositions(Vec<(DVec2, f64)>); - -/// Public so future thrust-prediction systems can replay buffered inputs at the correct tick. -#[derive(Resource, Default)] -pub struct Resimulating { - pub active: bool, - /// The absolute `LocalTick` value corresponding to the current resimulation step. - pub resim_tick: u64, -} - -struct PhysicsSnapshot { - position: DVec2, - rotation_cos: f64, - rotation_sin: f64, - linvel: DVec2, - angvel: f64, -} - -impl PhysicsSnapshot { - fn from_target(t: &CorrectionTarget) -> Self { - Self { - position: t.position, - rotation_cos: t.rotation_cos, - rotation_sin: t.rotation_sin, - linvel: t.linvel, - angvel: t.angvel, - } - } - - fn from_components( - p: &Position, - r: &Rotation, - lv: &LinearVelocity, - av: &AngularVelocity, - ) -> Self { - Self { - position: p.0, - rotation_cos: r.cos, - rotation_sin: r.sin, - linvel: lv.0, - angvel: av.0, - } - } - - fn apply(&self, world: &mut World, entity: Entity) { - let mut e = world.entity_mut(entity); - if let Some(mut p) = e.get_mut::() { p.0 = self.position; } - if let Some(mut r) = e.get_mut::() { - r.cos = self.rotation_cos; - r.sin = self.rotation_sin; - } - if let Some(mut lv) = e.get_mut::() { lv.0 = self.linvel; } - if let Some(mut av) = e.get_mut::() { av.0 = self.angvel; } - } -} - -pub fn prediction_plugin(app: &mut App) { - app - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .add_observer(init_prediction) - .add_systems(FixedFirst, tick_local_clock) - .add_systems(PreUpdate, ( - ( - save_local_state, - collect_planet_positions, - ).before(bevy_replicon::client::ClientSystems::Receive), - record_server_correction.after(bevy_replicon::client::ClientSystems::Receive), - )) - .add_systems(PostUpdate, - perform_resimulation.before(TransformSystems::Propagate)); -} - -fn tick_local_clock(mut tick: ResMut) { - tick.0 += 1; -} - -fn init_prediction( - trigger: On, - query: Query< - (&Position, &Rotation, &LinearVelocity, Option<&AngularVelocity>), - (Without, 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 collect_planet_positions( - mut resource: ResMut, - planets: Query<(&Position, &Mass), (With, Without)>, -) { - resource.0 = planets.iter().map(|(p, m)| (p.0, m.0 as f64)).collect(); -} - -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, - )>, - planet_positions: Res, - world_config: Res, - local_tick: Res, - mut lag: ResMut, -) { - let Some(cfg) = &world_config.config else { - for (mut pos, mut rot, mut linvel, mut angvel, _, _, local) in &mut query { - pos.0 = local.position; - rot.cos = local.rotation_cos; - rot.sin = local.rotation_sin; - linvel.0 = local.linvel; - angvel.0 = local.angvel; - } - return; - }; - - let planet_snapshot = &planet_positions.0; - - 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) { - pos.0 = local.position; - rot.cos = local.rotation_cos; - rot.sin = local.rotation_sin; - linvel.0 = local.linvel; - angvel.0 = local.angvel; - continue; - } - - let elapsed = (local_tick.0.saturating_sub(lag.last_local_tick)).clamp(1, 20) as f64; - lag.horizon = lag.horizon * 0.9 + elapsed * 0.1; - lag.last_local_tick = local_tick.0; - target.last_tick = Some(tick); - - let server_pos = pos.0; - let server_vel = linvel.0; - - let n = lag.horizon.round() as u64; - let dt = 1.0 / crate::shared::plugins::TICK_RATE; - let (ext_pos, _) = extrapolate( - server_pos, server_vel, &planet_snapshot, n, dt, cfg.world.gravity, - ); - - let error = (ext_pos - local.position).length(); - if error < SKIP_THRESHOLD { - target.needs_correction = false; - } else { - target.needs_correction = true; - target.position = server_pos; - target.linvel = server_vel; - 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 extrapolate( - mut pos: DVec2, - mut vel: DVec2, - planets: &[(DVec2, f64)], - n: u64, - dt: f64, - gravity: f64, -) -> (DVec2, DVec2) { - for _ in 0..n { - let mut accel = DVec2::ZERO; - for &(planet_pos, planet_mass) in planets { - let diff = planet_pos - pos; - let dist_sq = diff.length_squared().max(1.0); - accel += diff.normalize() * gravity * planet_mass / dist_sq; - } - vel += accel * dt; - pos += vel * dt; - } - (pos, vel) -} - -fn perform_resimulation(world: &mut World) { - let corrections: Vec<(Entity, PhysicsSnapshot)> = { - let mut q = world.query::<(Entity, &CorrectionTarget)>(); - q.iter(world) - .filter(|(_, t)| t.needs_correction) - .map(|(e, t)| (e, PhysicsSnapshot::from_target(t))) - .collect() - }; - if corrections.is_empty() { return; } - - let saved: Vec<(Entity, PhysicsSnapshot)> = { - let mut q = world.query::<( - Entity, &Position, &Rotation, &LinearVelocity, &AngularVelocity, - )>(); - q.iter(world) - .map(|(e, p, r, lv, av)| (e, PhysicsSnapshot::from_components(p, r, lv, av))) - .collect() - }; - - // Save Transforms separately; avian2d's Writeback overwrites Transform in each FixedUpdate - // step, so non-corrected entities would visually jump if we don't restore it. - let saved_transforms: Vec<(Entity, Vec3, Quat)> = { - let mut q = world.query::<(Entity, &Transform)>(); - q.iter(world) - .map(|(e, t)| (e, t.translation, t.rotation)) - .collect() - }; - - let corrected_set: HashSet = corrections.iter().map(|(e, _)| *e).collect(); - - for (entity, snapshot) in &corrections { - snapshot.apply(world, *entity); - } - - let n = world.resource::().horizon.round() as u64; - let base_tick = world.resource::().0; - world.resource_mut::().active = true; - for step in 0..n { - world.resource_mut::().resim_tick = base_tick + step; - world.run_schedule(FixedUpdate); - } - world.resource_mut::().active = false; - - for (entity, snapshot) in &saved { - if !corrected_set.contains(entity) { - snapshot.apply(world, *entity); - } - } - - // Restore transforms for non-corrected entities so the visual doesn't jump. - for (entity, translation, rotation) in &saved_transforms { - if !corrected_set.contains(entity) { - let mut e = world.entity_mut(*entity); - if let Some(mut t) = e.get_mut::() { - t.translation = *translation; - t.rotation = *rotation; - } - } - } - - for (entity, _) in &corrections { - if let Some(mut t) = world.entity_mut(*entity).get_mut::() { - t.needs_correction = false; - } - } -} diff --git a/crates/unified/src/client/mod.rs b/crates/unified/src/client/mod.rs index 2020c39e4781c3db0039859d79fb60d6ff578fc6..f23446ba634d16ff947c264b14a4f2a7b278e7a5 100644 --- a/crates/unified/src/client/mod.rs +++ b/crates/unified/src/client/mod.rs @@ -25,7 +25,6 @@ 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; @@ -49,7 +48,6 @@ 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) diff --git a/crates/unified/src/server/mod.rs b/crates/unified/src/server/mod.rs index ece8b3658107eff0e24bc50cc851572d33219faf..da80e07f33667a67c06abaca7f1e2effc49bd89a 100644 --- a/crates/unified/src/server/mod.rs +++ b/crates/unified/src/server/mod.rs @@ -22,6 +22,7 @@ use aeronet_transport::lane::LaneKind; use aeronet_transport::Transport; use aeronet_websocket::server::{ServerConfig, WebSocketServer}; use bevy_replicon::prelude::Replicated; +use bevy_replicon::server::AuthorizedClient; use crate::server::craft::craft_plugin; use crate::server::damping::damping_plugin; use crate::server::drill::drill_plugin; @@ -65,7 +66,8 @@ impl Plugin for ServerPlugin { .queue(WebSocketServer::open(ServerConfig::builder() .with_bind_address(bind) .with_no_encryption())); - }); + }) + .add_systems(Update, handle_authorized); } } @@ -96,19 +98,26 @@ fn on_connected( return; }; info!(?client, ?server, "client connected"); +} - let player = commands - .spawn((Replicated, ConnectedGameEntity { - network_entity: client, - })) - .id(); +fn handle_authorized( + newly_authorized_clients: Query>, + mut commands: Commands +) { + for client in newly_authorized_clients.iter() { + let player = commands + .spawn((Replicated, ConnectedGameEntity { + network_entity: client, + })) + .id(); - commands.entity(client).insert(( - Replicated, - ConnectedNetworkEntity { - game_entity: player, - }, - )); + commands.entity(client).insert(( + Replicated, + ConnectedNetworkEntity { + game_entity: player, + }, + )); + } } fn on_disconnected( trigger: On, diff --git a/crates/unified/src/shared/net.rs b/crates/unified/src/shared/net.rs index c61ff9e6098ecd1ad7c5dc24a17ea4f903cd7674..9f904db703461ae8d1cc14b5e34c4a6015c53a28 100644 --- a/crates/unified/src/shared/net.rs +++ b/crates/unified/src/shared/net.rs @@ -1,4 +1,4 @@ -use avian2d::prelude::{AngularInertia, AngularVelocity, LinearVelocity, Mass, Position, Rotation}; +use avian2d::prelude::{AngularInertia, AngularVelocity, Collider, LinearVelocity, Mass, Position, Rotation}; use bevy::ecs::entity::MapEntities; use bevy::prelude::*; use crate::prelude::{App, Message}; @@ -22,6 +22,7 @@ pub fn register_replication(app: &mut App) { .replicate::() .replicate::() .replicate::() + .replicate::() .replicate::() .replicate::()