library

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

commit 122859620d858760d081b09e990abaea4b2be3da
parent ad6a6b6eb52c27345d76e4d1377af0a1ecc83675
Author: Bradley Gannon <bradley@bradleygannon.com>
Date:   Tue, 18 Aug 2026 18:27:23 -0400

Add entity interpolation

Diffstat:
Mclient/src/main.rs | 15+++++++++++++--
Mcommon/src/game.rs | 33+++++++++++++++++++++++++++++++++
Mserver/src/main.rs | 3++-
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(),