#[cfg(all(feature = "2d", not(feature = "3d")))]
use avian2d::{math::Vector, prelude::*};
#[cfg(all(feature = "3d", not(feature = "2d")))]
use avian3d::{math::Vector, prelude::*};
use bevy_app::prelude::*;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::prelude::*;
use bevy_ecs::{
hierarchy::{ChildOf, Children},
schedule::{IntoScheduleConfigs, SystemSet},
};
use lightyear_core::history_buffer::HistoryBuffer;
use lightyear_core::prelude::{LocalTimeline, Tick};
use lightyear_link::prelude::Server;
#[allow(unused_imports)]
use tracing::{debug, info, trace};
#[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,
}
}
}
#[deprecated(note = "Use LagCompensationSystems instead")]
pub type LagCompensationSet = LagCompensationSystems;
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub enum LagCompensationSystems {
UpdateHistory,
Collisions,
}
#[derive(Component)]
pub struct AabbEnvelopeHolder;
#[derive(Component, Debug, Default, Deref, DerefMut)]
pub struct LagCompensationHistory(HistoryBuffer<(Position, Rotation, ColliderAabb)>);
impl<'a> IntoIterator for &'a LagCompensationHistory {
type Item = (Tick, &'a (Position, Rotation, ColliderAabb));
type IntoIter =
<&'a HistoryBuffer<(Position, Rotation, ColliderAabb)> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
(&self.0).into_iter()
}
}
impl Plugin for LagCompensationPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<LagCompensationConfig>();
app.add_observer(spawn_broad_phase_aabb_envelope);
app.add_systems(
PhysicsSchedule,
(update_collision_layers, update_collider_history)
.in_set(LagCompensationSystems::UpdateHistory),
);
app.configure_sets(
PhysicsSchedule,
(
PhysicsStepSystems::Solver,
LagCompensationSystems::UpdateHistory.ambiguous_with(PhysicsStepSystems::Sleeping),
PhysicsStepSystems::Finalize,
)
.chain(),
);
app.configure_sets(
FixedPostUpdate,
LagCompensationSystems::Collisions.after(SpatialQuerySystems),
);
}
}
fn spawn_broad_phase_aabb_envelope(
trigger: On<Add, LagCompensationHistory>,
query: Query<Option<&CollisionLayers>>,
mut commands: Commands,
) {
debug!("spawning broad-phase collider from aabb!");
commands.entity(trigger.entity).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.entity) {
child_commands.insert(*collision_layers);
}
});
}
fn update_collision_layers(
child_query: Query<Entity, With<AabbEnvelopeHolder>>,
parent_query: Query<
(Entity, &CollisionLayers, &Children),
(Without<AabbEnvelopeHolder>, Changed<CollisionLayers>),
>,
mut commands: Commands,
) {
parent_query.iter().for_each(|(parent, layers, children)| {
for child in children.iter() {
if child_query.get(child).is_ok() {
commands.entity(child).insert(*layers);
trace!(
?child,
?parent,
"Adding layers {layers:?} on lag compensation child collider"
);
}
}
});
}
fn update_collider_history(
timeline: Res<LocalTimeline>,
server: Single<(), With<Server>>,
config: Res<LagCompensationConfig>,
mut parent_query: Query<
(
&Position,
&Rotation,
&ColliderAabb,
&mut LagCompensationHistory,
),
Without<AabbEnvelopeHolder>,
>,
mut children_query: Query<(&ChildOf, &mut Collider, &mut Position), With<AabbEnvelopeHolder>>,
) {
let tick = timeline.tick();
children_query
.iter_mut()
.for_each(|(child_of, mut collider, mut position)| {
let Ok((parent_position, parent_rotation, parent_aabb, mut history)) =
parent_query.get_mut(child_of.parent())
else {
debug!("The lag compensation collider is not yet spawned! For one of the entities");
return;
};
history.add_update(tick, (*parent_position, *parent_rotation, *parent_aabb));
history.clear_until_tick(tick - (config.max_collider_history_ticks as u32));
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 envelope"
);
});
}
#[cfg(test)]
mod tests {
use super::*;
use bevy_ecs::world::World;
#[test]
fn lag_compensation_history_can_be_inserted_on_multiple_entities() {
let mut world = World::new();
let first = world.spawn(LagCompensationHistory::default()).id();
let second = world.spawn(LagCompensationHistory::default()).id();
assert!(world.entity(first).contains::<LagCompensationHistory>());
assert!(world.entity(second).contains::<LagCompensationHistory>());
}
}