A crates/unified/src/client/interpolation.rs => crates/unified/src/client/interpolation.rs +128 -0
@@ 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<RepliconTick>,
+}
+
+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<Add, Replicated>,
+ query: Query<(&Position, &Rotation, &LinearVelocity, Option<&AngularVelocity>), Without<PlanetSpring>>,
+ 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();
+ }
+ }
+}
M crates/unified/src/client/mod.rs => crates/unified/src/client/mod.rs +6 -0
@@ 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::<StarguideGizmos>()
.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))
M crates/unified/src/client/planet/incoming_planets.rs => crates/unified/src/client/planet/incoming_planets.rs +31 -2
@@ 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<SpringJointReady>>,
+ 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 },
+ ));
+ }
+}
M crates/unified/src/client/rendering/mod.rs => crates/unified/src/client/rendering/mod.rs +1 -1
@@ 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) {
M crates/unified/src/server/gravity.rs => crates/unified/src/server/gravity.rs +1 -50
@@ 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<Part>>,
- planet_query: Query<(&Transform, &Mass), With<Planet>>,
- world_config: Res<WorldConfigResource>,
- time: Res<Time>,
-) {
- let Some(world_config) = &world_config.config else {
- return;
- };
-
- for (part_transform, part_velocity, part_mass, mut forces) in &mut part_query {
- *forces = ConstantForce::new(0.0, 0.0);
-
- let part_mass = part_mass.0 as f64;
- let part_translation = part_transform.translation;
-
- for (planet_transform, planet_mass) in &planet_query {
- let planet_mass = planet_mass.0 as f64;
- let planet_translation = planet_transform.translation;
-
- let distance = planet_translation.distance(part_translation) as f64;
-
- let mut x = 0.0;
- let mut total_f = 0.0;
- let mut v = part_velocity.0;
- let dt = time.delta_secs() as f64 / world_config.world.gravity_iterations as f64;
- for i in 0..world_config.world.gravity_iterations {
- let f =
- world_config.world.gravity * ((part_mass * planet_mass) / ((distance - x)*(distance - x)));
- let dx = dt*(v.project_onto((planet_translation-part_translation).truncate().as_dvec2())).length()
- - 1.0/2.0*(f/part_mass)*(dt*dt);
- x += dx;
- v += f/part_mass*dt;
- if i == 0 || i == world_config.world.gravity_iterations-1 {
- total_f += f;
- } else {
- total_f += 2.0*f;
- }
- }
- let force = (dt/2.0*(total_f))/time.delta_secs() as f64;
- let direction = (planet_translation - part_translation).normalize().as_dvec3() * force;
- forces.x += direction.x;
- forces.y += direction.y;
- }
- }
-}
M crates/unified/src/server/orbit/mod.rs => crates/unified/src/server/orbit/mod.rs +1 -87
@@ 1,87 1,1 @@
-use std::collections::HashMap;
-use std::f64::consts::PI;
-use avian2d::math::TAU;
-use avian2d::prelude::{LinearVelocity, Mass};
-use bevy::prelude::{Plugin, Transform};
-use bevy::time::Time;
-use crate::shared::config::planet::{Planet, PlanetSpring};
-use crate::prelude::{App, Query, Res, Update, Without};
-use crate::shared::world_config::WorldConfigResource;
-
-const KEPLER_MAX_ITERATIONS: u32 = 100;
-const KEPLER_CONVERGENCE_THRESHOLD: f64 = 1e-10;
-
-pub struct OrbitPlugin;
-impl Plugin for OrbitPlugin {
- fn build(&self, app: &mut App) {
- app.add_systems(Update, update_orbits);
- }
-}
-
-fn update_orbits(
- mut planets: Query<(&Planet, &Transform, &mut LinearVelocity), Without<PlanetSpring>>,
- planets_2: Query<(&Planet, &Transform, &Mass), Without<PlanetSpring>>,
- mut planet_springs: Query<(&PlanetSpring, &mut Transform, &mut LinearVelocity), Without<Planet>>,
- world_config: Res<WorldConfigResource>,
- time: Res<Time>
-) {
- let Some(ref world_config) = world_config.config else {
- return;
- };
- let parent_velocities = planets.iter().map(|u| (u.0.name.clone(), u.2.clone())).collect::<HashMap<String, LinearVelocity>>();
- for (planet, _, _vel) in planets.iter_mut() {
- let Some(orbit_data) = &planet.orbit else { continue; };
- // find parent
- let Some(parent) = planets_2.iter().find(|u| u.0.name == orbit_data.orbiting) else { continue; };
-
- let (parent_data, parent_transform, parent_mass) = parent;
- // Orbital elements:
- // a = semi-major axis
- // e = eccentricity
- // t = orbital period
- // m = mean anomaly
- // e_k = eccentric anomaly (solution to Kepler's equation)
- // nu = true anomaly
- // r = radial distance
- let a = (planet.default_position.x as f64 - parent_data.default_position.x as f64) / (1.0 - orbit_data.eccentricity);
- let e = orbit_data.eccentricity;
- let t = 2.0*PI*((a*a*a)/(world_config.world.gravity*(**parent_mass as f64))).sqrt();
-
- let time = time.elapsed_secs_f64();
-
- // calculate position of the planet
- let m = (TAU / t) * time;
- let e_k = iterative_kepler(m, e);
- let nu = 2.0_f64 * ((1.0 + e).sqrt() * (e_k / 2.0).sin())
- .atan2((1.0 - e).sqrt() * (e_k / 2.0).cos());
- let r = a * (1.0 - e * e_k.cos());
-
- let x = r * nu.cos();
- let y = r * nu.sin();
-
- // find the spring
- let Some(mut planet_spring) = planet_springs.iter_mut().find(|u| u.0.name == planet.name) else { continue; };
- planet_spring.1.translation.x = x as f32 + parent_transform.translation.x;
- planet_spring.1.translation.y = y as f32 + parent_transform.translation.y;
-
- let Some(parent_velocity) = parent_velocities.get(&orbit_data.orbiting) else { continue; };
- let de_dt = (TAU / t) / (1.0 - e * e_k.cos());
- let b_factor = (1.0 - e * e).sqrt();
- let vx = -a * e_k.sin() * de_dt;
- let vy = a * b_factor * e_k.cos() * de_dt;
- planet_spring.2.x = vx + parent_velocity.x;
- planet_spring.2.y = vy + parent_velocity.y;
- }
-}
-
-fn iterative_kepler(m: f64, e: f64) -> f64 {
- let mut output = m;
- for _ in 0..KEPLER_MAX_ITERATIONS {
- let d = (m - output + e * output.sin()) / (1.0 - e * output.cos());
- output += d;
- if d.abs() < KEPLER_CONVERGENCE_THRESHOLD {
- break;
- }
- }
- output
-}
+pub use crate::shared::orbit::OrbitPlugin;
M crates/unified/src/server/part.rs => crates/unified/src/server/part.rs +6 -1
@@ 133,7 133,11 @@ fn calculate_bundle(config: &PartConfig, handle: &Handle<PartConfig>) -> impl Bu
};
let part_handle = PartHandle(handle.clone());
let collider = Collider::rectangle(config.physics.width, config.physics.height);
- let mass = Mass(config.physics.mass as f32);
+ let m = config.physics.mass as f32;
+ let w = config.physics.width as f32;
+ let h = config.physics.height as f32;
+ let mass = Mass(m);
+ let inertia = AngularInertia(m * (w * w + h * h) / 12.0); // I'm not entirely sure why this is required but things break without it, TODO(core) investigate
let temperature = Temperature(298.0); // note that this is 25 degrees C
let radiator = Radiator {
emissivity: config.part.emissivity,
@@ 145,6 149,7 @@ fn calculate_bundle(config: &PartConfig, handle: &Handle<PartConfig>) -> impl Bu
part_handle,
collider,
mass,
+ inertia,
temperature,
radiator,
)
M crates/unified/src/server/planets.rs => crates/unified/src/server/planets.rs +6 -1
@@ 1,5 1,6 @@
use crate::server::components::PlanetSensor;
use bevy::{asset::Handle, math::DVec3};
+use bevy_replicon::prelude::Replicated;
use crate::prelude::*;
use crate::shared::config::planet::{PlanetSpring, PlanetSpringJoint};
use crate::shared::config::planet::{Planet, PlanetBundle, PlanetConfigCollection};
@@ 53,13 54,16 @@ pub fn update_planets(
let planet_config = assets.get(*id).unwrap();
for planet in &planet_config.planets {
let planet_position = planet.default_position;
+ let r = planet.radius as f32;
+ let m = planet.mass;
let mut planet_entity = commands
.spawn((PlanetBundle {
planet: planet.clone(),
transform: Transform::from_translation(planet.default_position),
collider: Collider::circle(planet.radius),
- mass: Mass(planet.mass)
+ mass: Mass(m),
},
+ AngularInertia(m * r * r / 2.0), // Same deal here: not a clue why this is required, TODO(core) investigate
SleepingDisabled
));
planet_entity.with_child((
@@ 87,6 91,7 @@ pub fn update_planets(
},
Transform::from_translation(planet.default_position),
RigidBody::Kinematic,
+ Replicated,
)).id();
commands.spawn((
PlanetSpringJoint {
A crates/unified/src/shared/gravity.rs => crates/unified/src/shared/gravity.rs +51 -0
@@ 0,0 1,51 @@
+use crate::shared::config::planet::Planet;
+use crate::shared::ecs::Part;
+use crate::prelude::*;
+use crate::shared::world_config::WorldConfigResource;
+
+pub fn update_gravity(
+ mut part_query: Query<(&Transform, &LinearVelocity, &Mass, &mut ConstantForce), With<Part>>,
+ planet_query: Query<(&Transform, &Mass), With<Planet>>,
+ world_config: Res<WorldConfigResource>,
+ time: Res<Time>,
+) {
+ let Some(world_config) = &world_config.config else {
+ return;
+ };
+
+ for (part_transform, part_velocity, part_mass, mut forces) in &mut part_query {
+ *forces = ConstantForce::new(0.0, 0.0);
+
+ let part_mass = part_mass.0 as f64;
+ let part_translation = part_transform.translation;
+
+ for (planet_transform, planet_mass) in &planet_query {
+ let planet_mass = planet_mass.0 as f64;
+ let planet_translation = planet_transform.translation;
+
+ let distance = planet_translation.distance(part_translation) as f64;
+
+ let mut x = 0.0;
+ let mut total_f = 0.0;
+ let mut v = part_velocity.0;
+ let dt = time.delta_secs() as f64 / world_config.world.gravity_iterations as f64;
+ for i in 0..world_config.world.gravity_iterations {
+ let f =
+ world_config.world.gravity * ((part_mass * planet_mass) / ((distance - x)*(distance - x)));
+ let dx = dt*(v.project_onto((planet_translation-part_translation).truncate().as_dvec2())).length()
+ - 1.0/2.0*(f/part_mass)*(dt*dt);
+ x += dx;
+ v += f/part_mass*dt;
+ if i == 0 || i == world_config.world.gravity_iterations-1 {
+ total_f += f;
+ } else {
+ total_f += 2.0*f;
+ }
+ }
+ let force = (dt/2.0*(total_f))/time.delta_secs() as f64;
+ let direction = (planet_translation - part_translation).normalize().as_dvec3() * force;
+ forces.x += direction.x;
+ forces.y += direction.y;
+ }
+ }
+}
M crates/unified/src/shared/mod.rs => crates/unified/src/shared/mod.rs +2 -0
@@ 1,5 1,7 @@
pub mod attachment;
pub mod config;
+pub mod gravity;
+pub mod orbit;
pub mod physics;
pub mod thrust;
pub mod world_config;
M crates/unified/src/shared/net.rs => crates/unified/src/shared/net.rs +2 -5
@@ 1,4 1,4 @@
-use avian2d::prelude::{AngularInertia, AngularVelocity, CenterOfMass, Collider, LinearVelocity, Mass, Position, RigidBody, Rotation};
+use avian2d::prelude::{AngularInertia, AngularVelocity, LinearVelocity, Mass, Position, Rotation};
use bevy::ecs::entity::MapEntities;
use bevy::prelude::*;
use crate::prelude::{App, Message};
@@ 16,15 16,12 @@ pub fn register_replication(app: &mut App) {
.replicate::<Transform>()
.replicate::<GlobalTransform>()
- .replicate::<RigidBody>()
- .replicate::<Collider>()
.replicate::<Position>()
.replicate::<Rotation>()
.replicate::<LinearVelocity>()
.replicate::<AngularVelocity>()
- .replicate::<Mass>()
.replicate::<AngularInertia>()
- .replicate::<CenterOfMass>()
+ .replicate::<Mass>()
.replicate::<Part>()
.replicate::<Planet>()
A crates/unified/src/shared/orbit.rs => crates/unified/src/shared/orbit.rs +75 -0
@@ 0,0 1,75 @@
+use std::collections::HashMap;
+use std::f64::consts::PI;
+use avian2d::math::TAU;
+use avian2d::prelude::{LinearVelocity, Mass};
+use bevy::prelude::{App, Plugin, Transform, Update};
+use bevy::time::Time;
+use crate::shared::config::planet::{Planet, PlanetSpring};
+use crate::prelude::{Query, Res, Without};
+use crate::shared::world_config::WorldConfigResource;
+
+pub struct OrbitPlugin;
+impl Plugin for OrbitPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_systems(Update, update_orbits);
+ }
+}
+
+pub fn update_orbits(
+ mut planets: Query<(&Planet, &Transform, &mut LinearVelocity), Without<PlanetSpring>>,
+ planets_2: Query<(&Planet, &Transform, &Mass), Without<PlanetSpring>>,
+ mut planet_springs: Query<(&PlanetSpring, &mut Transform, &mut LinearVelocity), Without<Planet>>,
+ world_config: Res<WorldConfigResource>,
+ time: Res<Time>
+) {
+ let Some(ref world_config) = world_config.config else {
+ return;
+ };
+ let parent_velocities = planets.iter().map(|u| (u.0.name.clone(), u.2.clone())).collect::<HashMap<String, LinearVelocity>>();
+ for (planet, _, _vel) in planets.iter_mut() {
+ let Some(orbit_data) = &planet.orbit else { continue; };
+ let Some(parent) = planets_2.iter().find(|u| u.0.name == orbit_data.orbiting) else { continue; };
+
+ let (parent_data, parent_transform, parent_mass) = parent;
+ let a = (planet.default_position.x as f64 - parent_data.default_position.x as f64) / (1.0 - orbit_data.eccentricity);
+ let e = orbit_data.eccentricity;
+ let t = 2.0*PI*((a*a*a)/(world_config.world.gravity*(**parent_mass as f64))).sqrt();
+
+ let time = time.elapsed_secs_f64();
+
+ let m = (TAU / t) * time;
+ let e_k = iterative_kepler(m, e);
+ let nu = 2.0_f64 * ((1.0 + e).sqrt() * (e_k / 2.0).sin())
+ .atan2((1.0 - e).sqrt() * (e_k / 2.0).cos());
+ let r = a * (1.0 - e * e_k.cos());
+
+ let x = r * nu.cos();
+ let y = r * nu.sin();
+
+ let Some(mut planet_spring) = planet_springs.iter_mut().find(|u| u.0.name == planet.name) else { continue; };
+ planet_spring.1.translation.x = x as f32 + parent_transform.translation.x;
+ planet_spring.1.translation.y = y as f32 + parent_transform.translation.y;
+
+ let Some(parent_velocity) = parent_velocities.get(&orbit_data.orbiting) else { continue; };
+ let de_dt = (TAU / t) / (1.0 - e * e_k.cos());
+ let b_factor = (1.0 - e * e).sqrt();
+ let vx = -a * e_k.sin() * de_dt;
+ let vy = a * b_factor * e_k.cos() * de_dt;
+ planet_spring.2.x = vx + parent_velocity.x;
+ planet_spring.2.y = vy + parent_velocity.y;
+ }
+}
+
+fn iterative_kepler(m: f64, e: f64) -> f64 {
+ const MAX_ITERATIONS: u32 = 100;
+ const CONVERGENCE_THRESHOLD: f64 = 1e-10;
+ let mut output = m;
+ for _ in 0..MAX_ITERATIONS {
+ let d = (m - output + e * output.sin()) / (1.0 - e * output.cos());
+ output += d;
+ if d.abs() < CONVERGENCE_THRESHOLD {
+ break;
+ }
+ }
+ output
+}