commit 122859620d858760d081b09e990abaea4b2be3da
parent ad6a6b6eb52c27345d76e4d1377af0a1ecc83675
Author: Bradley Gannon <bradley@bradleygannon.com>
Date: Tue, 18 Aug 2026 18:27:23 -0400
Add entity interpolation
Diffstat:
3 files changed, 48 insertions(+), 3 deletions(-)
diff --git a/client/src/main.rs b/client/src/main.rs
@@ -1,4 +1,4 @@
-use std::{collections::VecDeque, sync::mpsc::channel, thread};
+use std::{collections::VecDeque, sync::mpsc::channel, thread, time::SystemTime};
use common::game::{Action, ActionKind, GameState, ResponseKind};
use macroquad::prelude::*;
@@ -30,9 +30,11 @@ async fn main() {
let mut camera = Camera::default();
let mut game_state = GameState::default();
+ let mut old_game_state = GameState::default();
let mut local_player_id = None;
let mut sequence_number = 0;
let mut unresponded_actions = VecDeque::new();
+ let mut interpolation_basis = SystemTime::now();
loop {
for response in server_to_client_rx.try_iter() {
@@ -55,6 +57,8 @@ async fn main() {
ResponseKind::UpdateGameState {
game_state: new_game_state,
} => {
+ old_game_state = game_state.clone();
+
// Drop any unresponded actions whose sequence numbers are less than the number
// we just got from the server
if let Some(new_front_index) = unresponded_actions
@@ -68,6 +72,7 @@ async fn main() {
// Reapply the remaining unresponded actions to the latest server game state
game_state = new_game_state;
+ interpolation_basis = SystemTime::now();
for unresponded_action in unresponded_actions.iter() {
game_state.apply(unresponded_action.clone());
}
@@ -86,7 +91,13 @@ async fn main() {
}
follow_local_player(&mut camera, &game_state, &id);
}
- render(&camera, &game_state, &spritesheet);
+ let interpolated_game_state = GameState::interpolate(
+ &old_game_state,
+ &game_state,
+ interpolation_basis,
+ local_player_id,
+ );
+ render(&camera, &interpolated_game_state, &spritesheet);
next_frame().await;
}
}
diff --git a/common/src/game.rs b/common/src/game.rs
@@ -28,6 +28,39 @@ impl GameState {
}
}
}
+ pub fn interpolate(
+ old: &Self,
+ new: &Self,
+ basis: SystemTime,
+ local_player_id: Option<Uuid>,
+ ) -> GameState {
+ let mut output = GameState::default();
+ for (new_id, new_player) in new.players.iter() {
+ if let Some(old_player) = old.players.get(new_id) {
+ if let Some(ref lpid) = local_player_id
+ && new_id == lpid
+ {
+ output.players.insert(*new_id, new_player.clone());
+ } else {
+ let span = new.timestamp.duration_since(old.timestamp);
+ let fraction = SystemTime::now().duration_since(basis);
+ let t = match (fraction, span) {
+ (Ok(f), Ok(s)) => (f.as_secs_f32() / s.as_secs_f32()).clamp(0., 1.),
+ _ => return new.clone(),
+ };
+ let interpolated_player = Player {
+ position: (new_player.position - old_player.position) * t
+ + old_player.position,
+ ..new_player.clone()
+ };
+ output.players.insert(*new_id, interpolated_player);
+ }
+ } else {
+ output.players.insert(*new_id, new_player.clone());
+ }
+ }
+ output
+ }
}
impl Default for GameState {
diff --git a/server/src/main.rs b/server/src/main.rs
@@ -2,7 +2,7 @@ use std::{
collections::LinkedList,
sync::{Arc, Mutex, mpsc::channel},
thread,
- time::Instant,
+ time::{Instant, SystemTime},
};
use common::game::{GameState, Response, ResponseKind, TICK_DURATION};
@@ -53,6 +53,7 @@ fn main() {
// Only bother to send the latest game status to clients if we really need to, instead
// of every tick
if send_game_state {
+ game_state.timestamp = SystemTime::now();
for client in c.iter() {
let _ = client.send(Response::new(
client.sequence_number(),