use super::predict::Predicted;
use super::replicate::NetTransform;
use crate::Transform;
use bevy_ecs::prelude::*;
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::f32::consts::{PI, TAU};
use std::time::Duration;
const MAX_SAMPLES: usize = 64;
#[derive(Clone, Debug)]
pub struct Interpolation {
delay: Duration,
samples: BTreeMap<Entity, VecDeque<(Duration, Transform)>>,
}
impl Interpolation {
pub fn new(delay: Duration) -> Self {
Self {
delay,
samples: BTreeMap::new(),
}
}
pub fn delay(&self) -> Duration {
self.delay
}
pub fn set_delay(&mut self, delay: Duration) {
self.delay = delay;
}
pub fn record(&mut self, world: &mut World, time: Duration) {
let mut query = world
.query_filtered::<(Entity, &NetTransform), Without<Predicted>>();
let mut alive = BTreeSet::new();
for (entity, net) in query.iter(world) {
let samples = self.samples.entry(entity).or_default();
if samples.back().is_some_and(|(last, _)| *last >= time) {
samples.pop_back(); }
if samples.len() == MAX_SAMPLES {
samples.pop_front();
}
samples.push_back((time, net.0));
alive.insert(entity);
}
self.samples.retain(|entity, _| alive.contains(entity));
}
pub fn present(&mut self, world: &mut World, now: Duration) {
let at = now.saturating_sub(self.delay);
for (entity, samples) in &mut self.samples {
while samples.get(1).is_some_and(|(time, _)| *time <= at) {
samples.pop_front();
}
let Some(&(from_time, from)) = samples.front() else {
continue;
};
let transform = match samples.get(1) {
Some(&(to_time, to)) if at > from_time => {
let t = (at - from_time).as_secs_f32()
/ (to_time - from_time).as_secs_f32();
blend(&from, &to, t)
}
_ => from,
};
if let Ok(mut entity) = world.get_entity_mut(*entity) {
entity.insert(transform);
}
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Track {
samples: VecDeque<(u64, [f32; 3])>,
}
impl Track {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, tick: u64, position: [f32; 3]) {
match self.samples.binary_search_by_key(&tick, |(tick, _)| *tick) {
Ok(index) => self.samples[index].1 = position,
Err(0) if self.samples.len() == MAX_SAMPLES => {}
Err(index) => {
self.samples.insert(index, (tick, position));
if self.samples.len() > MAX_SAMPLES {
self.samples.pop_front();
}
}
}
}
pub fn newest(&self) -> Option<u64> {
self.samples.back().map(|(tick, _)| *tick)
}
pub fn at(&self, tick: f64) -> Option<[f32; 3]> {
let after = self.samples.partition_point(|(at, _)| *at as f64 <= tick);
let Some(&(to_tick, to)) = self.samples.get(after) else {
return self.samples.back().map(|(_, position)| *position);
};
let Some(&(from_tick, from)) = after
.checked_sub(1)
.and_then(|before| self.samples.get(before))
else {
return Some(to);
};
let t =
((tick - from_tick as f64) / (to_tick - from_tick) as f64) as f32;
Some(std::array::from_fn(|i| from[i] + (to[i] - from[i]) * t))
}
}
fn blend(from: &Transform, to: &Transform, t: f32) -> Transform {
let lerp = |a: [f32; 3], b: [f32; 3], angle: bool| {
std::array::from_fn(|i| {
let mut d = b[i] - a[i];
if angle {
d = (d + PI).rem_euclid(TAU) - PI;
}
a[i] + d * t
})
};
let mut out = *to;
out.position = lerp(from.position, to.position, false);
out.rotation = lerp(from.rotation, to.rotation, true);
out.scale = lerp(from.scale, to.scale, false);
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::net::replicate::{Replica, Replicated, Replication, TRANSFORM};
use crate::net::HOST;
use crate::runtime::{SceneComponentRegistry, SceneId};
#[test]
fn remote_objects_show_where_they_were_one_delay_ago() {
let mut host = World::new();
let mut client = World::new();
client.insert_resource(SceneComponentRegistry::default());
host.insert_resource(SceneComponentRegistry::default());
let mut replication = Replication::new();
replication.component(TRANSFORM);
let mut replica = Replica::new(&replication).interpolated();
let mut smooth = Interpolation::new(Duration::from_millis(100));
let raft = host
.spawn((Replicated, SceneId::new(), Transform::new([0.0; 3])))
.id();
let ms = Duration::from_millis;
for i in 0..4u8 {
host.get_mut::<Transform>(raft).unwrap().position[0] = f32::from(i);
host.get_mut::<Transform>(raft).unwrap().rotation[1] =
if i % 2 == 0 { 3.1 } else { -3.1 };
if let Some(delta) = replication.delta(&mut host).unwrap() {
replica.apply(&mut client, HOST, &delta).unwrap().unwrap();
}
smooth.record(&mut client, ms(50 * u64::from(i)));
}
let shown = |client: &mut World| {
*client
.query_filtered::<&Transform, With<Replicated>>()
.single(client)
.unwrap()
};
let net = |client: &mut World| {
client.query::<&NetTransform>().single(client).unwrap().0
};
assert_eq!(net(&mut client).position[0], 3.0, "the host's latest");
smooth.present(&mut client, ms(50));
assert_eq!(shown(&mut client).position[0], 0.0, "held before 0 ms");
smooth.present(&mut client, ms(125));
let halfway = shown(&mut client);
assert!((halfway.position[0] - 0.5).abs() < 1e-5);
assert!(
halfway.rotation[1].abs() > 3.1,
"turns the short way past pi, not through zero"
);
smooth.present(&mut client, ms(500));
assert_eq!(shown(&mut client).position[0], 3.0, "held at the newest");
smooth.set_delay(ms(475));
smooth.present(&mut client, ms(500));
assert_eq!(smooth.delay(), ms(475));
assert_eq!(shown(&mut client).position[0], 3.0, "never goes back");
}
#[test]
fn a_track_blends_plain_tick_samples_that_arrive_out_of_order() {
let mut track = Track::new();
assert_eq!(track.at(5.0), None);
track.push(20, [2.0, 0.0, 0.0]);
track.push(10, [1.0, 0.0, 0.0]); track.push(30, [9.0, 0.0, 0.0]);
track.push(30, [3.0, 0.0, 6.0]); assert_eq!(track.newest(), Some(30));
assert_eq!(track.at(5.0), Some([1.0, 0.0, 0.0]), "holds the oldest");
assert_eq!(track.at(15.0), Some([1.5, 0.0, 0.0]));
assert_eq!(track.at(27.5), Some([2.75, 0.0, 4.5]));
assert_eq!(track.at(99.0), Some([3.0, 0.0, 6.0]), "no guessing");
for tick in 31..200 {
track.push(tick, [0.0; 3]);
}
assert_eq!(track.samples.len(), MAX_SAMPLES);
track.push(5, [7.0; 3]);
assert_eq!(track.samples.front().unwrap().0, 200 - 64, "too old");
}
#[test]
fn the_predicted_hero_is_not_interpolated() {
let mut world = World::new();
let at = |x| Transform::new([x, 0.0, 0.0]);
let hero = world.spawn((NetTransform(at(1.0)), at(9.0))).id();
let other = world.spawn((NetTransform(at(1.0)), at(9.0))).id();
crate::net::predict::predict_only(&mut world, other);
crate::net::predict::predict_only(&mut world, hero);
assert!(world.get::<Predicted>(other).is_none(), "only one hero");
let mut smooth = Interpolation::new(Duration::ZERO);
smooth.record(&mut world, Duration::ZERO);
smooth.present(&mut world, Duration::ZERO);
assert_eq!(world.get::<Transform>(hero).unwrap().position[0], 9.0);
assert_eq!(world.get::<Transform>(other).unwrap().position[0], 1.0);
}
}