use std::fmt::Debug;
use crate::correction::Correction;
use crate::input_buffer::ClientInputBuffer;
use crate::prediction::PredictedEntity;
use crate::smoothing::{linear, Easing, ErrorSmoother};
use crate::types::SequenceNumber;
#[derive(Debug, Clone, Copy)]
pub struct PlayerConfig {
pub input_buffer: usize,
pub smoothing_secs: f32,
pub easing: Easing,
}
impl Default for PlayerConfig {
fn default() -> Self {
Self {
input_buffer: 256,
smoothing_secs: 0.1,
easing: linear,
}
}
}
pub struct PredictedPlayer<State: Clone + Debug, Input: Clone + Debug, Ctx = ()> {
predicted: PredictedEntity<State, Input>,
inputs: ClientInputBuffer<Input, State>,
smoother: ErrorSmoother<State>,
next_seq: SequenceNumber,
apply: fn(&mut State, &Input, &Ctx),
lerp: fn(&State, &State, f32) -> State,
ctx: Ctx,
active: bool,
on_first: Option<fn(&Input, &Ctx)>,
}
impl<State: Clone + Debug, Input: Clone + Debug, Ctx: Default> PredictedPlayer<State, Input, Ctx> {
pub fn new(
initial: State,
config: PlayerConfig,
apply: fn(&mut State, &Input, &Ctx),
lerp: fn(&State, &State, f32) -> State,
) -> Self {
Self {
predicted: PredictedEntity::new(initial),
inputs: ClientInputBuffer::new(config.input_buffer),
smoother: ErrorSmoother::new(config.smoothing_secs).with_easing(config.easing),
next_seq: 0,
apply,
lerp,
ctx: Ctx::default(),
active: true,
on_first: None,
}
}
}
impl<State: Clone + Debug, Input: Clone + Debug, Ctx> PredictedPlayer<State, Input, Ctx> {
pub fn set_context(&mut self, ctx: Ctx) {
self.ctx = ctx;
}
pub fn context(&self) -> &Ctx {
&self.ctx
}
pub fn set_active(&mut self, active: bool) {
self.active = active;
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn teleport(&mut self, state: State) {
self.predicted.current_predicted_state = state.clone();
self.predicted.last_authoritative_state = state;
self.inputs.clear();
self.smoother.reset();
}
pub fn on_first(mut self, hook: fn(&Input, &Ctx)) -> Self {
self.on_first = Some(hook);
self
}
pub fn input(&mut self, input: Input) -> SequenceNumber {
self.next_seq += 1;
let seq = self.next_seq;
if !self.active {
return seq;
}
let (apply, ctx) = (self.apply, &self.ctx);
self
.predicted
.apply_local_input_and_predict(&input, seq, &mut self.inputs, &|s: &mut State, i: &Input| apply(s, i, ctx));
if let Some(hook) = self.on_first {
hook(&input, ctx);
}
seq
}
pub fn reconcile(&mut self, authoritative: State, acked_seq: SequenceNumber) -> Correction<State> {
let seen = self.render();
if self.active {
let (apply, ctx) = (self.apply, &self.ctx);
self.predicted.reconcile_with_server_state(authoritative, acked_seq, &mut self.inputs, &|s: &mut State, i: &Input| {
apply(s, i, ctx)
});
} else {
self.predicted.current_predicted_state = authoritative.clone();
self.predicted.last_authoritative_state = authoritative;
self.predicted.last_server_acknowledged_input_seq = acked_seq;
self.inputs.clear();
}
self.smoother.begin_from(seen.clone());
Correction {
seen,
settled: self.predicted.current_predicted_state.clone(),
}
}
pub fn advance(&mut self, dt_secs: f32) {
self.smoother.advance(dt_secs);
}
pub fn render(&self) -> State {
self.smoother.sample(&self.predicted.current_predicted_state, self.lerp)
}
pub fn logical(&self) -> &State {
&self.predicted.current_predicted_state
}
pub fn authoritative(&self) -> &State {
&self.predicted.last_authoritative_state
}
pub fn latest_seq(&self) -> SequenceNumber {
self.next_seq
}
pub fn acked_seq(&self) -> SequenceNumber {
self.predicted.last_server_acknowledged_input_seq
}
pub fn unacked_count(&self) -> usize {
self.inputs.get_unacknowledged_inputs(self.acked_seq()).count()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Debug, PartialEq)]
struct P(f32);
fn apply(p: &mut P, i: &f32, _ctx: &()) {
p.0 += *i;
}
fn lerp(a: &P, b: &P, t: f32) -> P {
P(a.0 + (b.0 - a.0) * t)
}
fn player(smoothing_secs: f32) -> PredictedPlayer<P, f32> {
PredictedPlayer::new(
P(0.0),
PlayerConfig {
input_buffer: 64,
smoothing_secs,
..PlayerConfig::default()
},
apply,
lerp,
)
}
#[test]
fn predicting_moves_the_logical_state() {
let mut me = player(0.0);
me.input(1.0);
me.input(1.0);
assert_eq!(me.logical().0, 2.0);
}
#[test]
fn reconciliation_replays_unacknowledged_inputs() {
let mut me = player(0.0);
let s1 = me.input(1.0); me.input(1.0);
me.reconcile(P(1.0), s1);
assert_eq!(me.logical().0, 2.0);
assert_eq!(me.unacked_count(), 1);
}
#[test]
fn a_correction_eases_the_render_but_not_the_logical() {
let mut me = player(0.1);
let s = me.input(10.0);
me.reconcile(P(0.0), s);
assert_eq!(me.logical().0, 0.0, "logical snaps to authority immediately");
assert!((me.render().0 - 10.0).abs() < 1e-3, "render starts where the eye was");
me.advance(0.05);
assert!((me.render().0 - 5.0).abs() < 0.2, "render eases halfway");
me.advance(0.05);
assert!((me.render().0 - 0.0).abs() < 1e-3, "render arrives at the logical state");
}
#[test]
fn a_forced_entity_predicts_the_force_from_its_context() {
fn apply_with_wind(p: &mut P, i: &f32, wind: &f32) {
p.0 += *i + *wind;
}
let mut me: PredictedPlayer<P, f32, f32> = PredictedPlayer::new(
P(0.0),
PlayerConfig { input_buffer: 64, smoothing_secs: 0.0, ..PlayerConfig::default() },
apply_with_wind,
lerp,
);
me.set_context(0.5);
me.input(1.0);
me.input(1.0);
assert_eq!(me.logical().0, 3.0, "each step carries the input plus the wind");
let correction = me.reconcile(P(3.0), me.latest_seq());
assert_eq!(correction.seen, correction.settled, "a matching rule needs no correction");
}
#[test]
fn a_frozen_entity_stops_predicting_instead_of_inventing_corrections() {
let mut me = player(0.0);
me.input(1.0);
assert_eq!(me.logical().0, 1.0);
me.set_active(false);
me.input(1.0);
me.input(1.0);
assert_eq!(me.logical().0, 1.0, "a frozen entity does not move on input");
let correction = me.reconcile(P(1.0), me.latest_seq());
assert_eq!(correction.seen, correction.settled, "and so it never disagrees with the server");
assert_eq!(me.unacked_count(), 0, "nothing is queued for replay while frozen");
me.set_active(true);
me.input(1.0);
assert_eq!(me.logical().0, 2.0);
}
#[test]
fn a_teleport_snaps_and_drops_the_journey() {
let mut me = player(0.5);
me.input(1.0);
me.reconcile(P(50.0), 0); assert!(me.render().0 < 50.0, "an ordinary correction eases");
me.teleport(P(900.0));
assert_eq!(me.render().0, 900.0, "a teleport is visible immediately");
assert_eq!(me.logical().0, 900.0);
assert_eq!(me.unacked_count(), 0, "pending inputs described a journey that did not happen");
}
#[test]
fn reconcile_reports_what_it_corrected() {
let mut me = player(0.0);
me.input(1.0);
me.input(1.0);
let correction = me.reconcile(P(10.0), 1);
assert_eq!(correction.seen.0, 2.0, "where it was being drawn");
assert_eq!(correction.settled.0, 11.0, "10 authoritative, replaying the unacked second input");
}
#[test]
fn overflowing_the_input_buffer_does_not_panic_or_go_wrong_within_the_window() {
let mut me = PredictedPlayer::new(
P(0.0),
PlayerConfig {
input_buffer: 4,
smoothing_secs: 0.0,
..PlayerConfig::default()
},
apply,
lerp,
);
for _ in 0..20 {
me.input(1.0);
}
assert_eq!(me.logical().0, 20.0, "prediction advanced through all inputs");
let latest = me.latest_seq();
me.reconcile(P(17.0), latest - 3); assert_eq!(me.logical().0, 20.0, "17 authoritative, replay 18/19/20 back to 20");
assert!(me.logical().0.is_finite());
}
#[test]
fn reconciling_with_a_future_ack_snaps_and_clears() {
let mut me = player(0.0);
me.input(1.0);
me.input(1.0);
me.reconcile(P(42.0), 9999);
assert_eq!(me.logical().0, 42.0);
assert_eq!(me.unacked_count(), 0);
}
#[test]
fn zero_smoothing_renders_the_logical_at_once() {
let mut me = player(0.0);
let s = me.input(10.0);
me.reconcile(P(0.0), s);
assert_eq!(me.render().0, 0.0, "no ease, render is the logical state");
}
#[test]
fn a_one_shot_runs_once_however_many_times_its_input_is_replayed() {
use std::cell::Cell;
#[derive(Clone, Debug, PartialEq)]
struct Shot {
dx: f32,
fire: bool,
}
#[derive(Default)]
struct Counts {
applied_fires: Cell<u32>,
fired: Cell<u32>,
}
fn apply(p: &mut P, i: &Shot, counts: &Counts) {
p.0 += i.dx;
if i.fire {
counts.applied_fires.set(counts.applied_fires.get() + 1);
}
}
fn on_first(i: &Shot, counts: &Counts) {
if i.fire {
counts.fired.set(counts.fired.get() + 1);
}
}
let mut me: PredictedPlayer<P, Shot, Counts> = PredictedPlayer::new(
P(0.0),
PlayerConfig {
input_buffer: 64,
smoothing_secs: 0.0,
..PlayerConfig::default()
},
apply,
lerp,
)
.on_first(on_first);
for n in 0..5 {
me.input(Shot { dx: 1.0, fire: n == 2 });
}
for _ in 0..10 {
me.reconcile(P(0.0), 0);
}
assert_eq!(me.context().fired.get(), 1, "one trigger pull is one shot");
assert_eq!(me.context().applied_fires.get(), 11, "the applier still replays the input every reconcile, which is its job");
assert_eq!(me.logical().0, 5.0, "and the replay still moves the player");
}
}