use bevy::prelude::*;
use lightyear::prelude::{HistoryBuffer, TickManager};
#[cfg(all(feature = "2d", not(feature = "3d")))]
use avian2d::{math::Vector, prelude::*};
#[cfg(all(feature = "3d", not(feature = "2d")))]
use avian3d::{math::Vector, prelude::*};
#[derive(Resource)]
pub struct LagCompensationPlugin;
#[derive(Resource)]
pub struct LagCompensationConfig {
pub max_collider_history_ticks: u8,
}
impl Default for LagCompensationConfig {
fn default() -> Self {
Self {
max_collider_history_ticks: 35,
}
}
}
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub enum LagCompensationSet {
UpdateHistory,
Collisions,
}
#[derive(Component)]
pub struct AabbEnvelopeHolder;
pub type LagCompensationHistory = HistoryBuffer<(Position, Rotation, ColliderAabb)>;
impl Plugin for LagCompensationPlugin {
fn build(&self, app: &mut App) {
app.register_type::<LagCompensationHistory>();
app.init_resource::<LagCompensationConfig>();
app.add_observer(spawn_broad_phase_aabb_envelope);
app.add_systems(
PhysicsSchedule,
(update_collision_layers, update_collider_history)
.in_set(LagCompensationSet::UpdateHistory),
);
app.configure_sets(
PhysicsSchedule,
(
PhysicsStepSet::Solver,
LagCompensationSet::UpdateHistory.ambiguous_with(PhysicsStepSet::ReportContacts),
PhysicsStepSet::SpatialQuery,
LagCompensationSet::Collisions,
)
.chain(),
);
app.configure_sets(
FixedPostUpdate,
LagCompensationSet::Collisions.after(PhysicsSet::Sync),
);
}
}
fn spawn_broad_phase_aabb_envelope(
trigger: Trigger<OnAdd, LagCompensationHistory>,
query: Query<Option<&CollisionLayers>>,
mut commands: Commands,
) {
debug!("spawning broad-phase collider from aabb!");
commands.entity(trigger.target()).with_children(|builder| {
let mut child_commands = builder.spawn((
#[cfg(all(feature = "2d", not(feature = "3d")))]
Collider::rectangle(1.0, 1.0),
#[cfg(all(feature = "3d", not(feature = "2d")))]
Collider::cuboid(1.0, 1.0, 1.0),
Position::default(),
Rotation::default(),
AabbEnvelopeHolder,
));
if let Ok(Some(collision_layers)) = query.get(trigger.target()) {
child_commands.insert(collision_layers.clone());
}
});
}
fn update_collision_layers(
mut child_query: Query<&mut CollisionLayers, With<AabbEnvelopeHolder>>,
mut parent_query: Query<(&mut CollisionLayers, &Children), Without<AabbEnvelopeHolder>>,
) {
parent_query.iter_mut().for_each(|(layers, children)| {
if layers.is_changed() || !layers.is_added() {
for child in children.iter() {
if let Ok(mut child_layers) = child_query.get_mut(*child) {
*child_layers = *layers;
}
}
}
});
}
fn update_collider_history(
tick_manager: Res<TickManager>,
config: Res<LagCompensationConfig>,
mut parent_query: Query<
(
&Position,
&Rotation,
&ColliderAabb,
&mut LagCompensationHistory,
),
Without<AabbEnvelopeHolder>,
>,
mut children_query: Query<(&Parent, &mut Collider, &mut Position), With<AabbEnvelopeHolder>>,
) {
let tick = tick_manager.tick();
children_query
.iter_mut()
.for_each(|(parent, mut collider, mut position)| {
let (parent_position, parent_rotation, parent_aabb, mut history) =
parent_query.get_mut(parent.get()).unwrap();
history.add_update(
tick,
(
parent_position.clone(),
parent_rotation.clone(),
parent_aabb.clone(),
),
);
history.clear_until_tick(tick - (config.max_collider_history_ticks as u16));
let (min, max) = history.into_iter().fold(
(Vector::MAX, Vector::MIN),
|(min, max), (_, (_, _, aabb))| (min.min(aabb.min), max.max(aabb.max)),
);
let aabb_envelope = ColliderAabb::from_min_max(min, max);
#[cfg(all(feature = "2d", not(feature = "3d")))]
let new_collider = Collider::rectangle(max.x - min.x, max.y - min.y);
#[cfg(all(feature = "3d", not(feature = "2d")))]
let new_collider = Collider::cuboid(max.x - min.x, max.y - min.y, max.z - min.z);
*collider = new_collider;
*position = Position(aabb_envelope.center());
trace!(
?tick,
?history,
?aabb_envelope,
"update collider history and aabb envlope"
);
});
}