library

Online co-op book organization video game
git clone https://git.bradleygannon.com/library.git
Log | Files | Refs

commit 28136067d780b562c22447c6c5aa7b067595ba96
parent ddb46383fea7f172435649858df4ce7251df3dce
Author: Bradley Gannon <bradley@bradleygannon.com>
Date:   Sat, 22 Aug 2026 12:14:22 -0400

Limit player speed and compute velocity from time instead of frames

Diffstat:
Mclient/src/game.rs | 20+++++++++++++-------
Mclient/src/main.rs | 22++++++++++++++++++----
Mclient/src/render.rs | 2+-
Mcommon/src/game.rs | 22++++++++++++++++++----
4 files changed, 50 insertions(+), 16 deletions(-)

diff --git a/client/src/game.rs b/client/src/game.rs @@ -1,24 +1,30 @@ -use common::game::ActionKind; +use std::time::Duration; + +use common::game::{ActionKind, GameState, Player}; use macroquad::prelude::*; -pub(crate) fn handle_input() -> Option<ActionKind> { +pub(crate) fn handle_movement_input( + local_player: &mut Player, + dt: &Duration, +) -> Option<ActionKind> { let velocity_x = if is_key_down(KeyCode::A) || is_key_down(KeyCode::Left) { - -1. + -GameState::MAX_PLAYER_SPEED_PX_S } else if is_key_down(KeyCode::D) || is_key_down(KeyCode::Right) { - 1. + GameState::MAX_PLAYER_SPEED_PX_S } else { 0. }; let velocity_y = if is_key_down(KeyCode::W) || is_key_down(KeyCode::Up) { - -1. + -GameState::MAX_PLAYER_SPEED_PX_S } else if is_key_down(KeyCode::S) || is_key_down(KeyCode::Down) { - 1. + GameState::MAX_PLAYER_SPEED_PX_S } else { 0. }; if velocity_x != 0. || velocity_y != 0. { Some(ActionKind::Move { - velocity: Vec2::new(velocity_x, velocity_y), + new_position: local_player.position + + Vec2::new(velocity_x, velocity_y) * dt.as_secs_f32(), }) } else { None diff --git a/client/src/main.rs b/client/src/main.rs @@ -1,10 +1,15 @@ -use std::{collections::VecDeque, sync::mpsc::channel, thread, time::SystemTime}; +use std::{ + collections::VecDeque, + sync::mpsc::channel, + thread, + time::{Instant, SystemTime}, +}; use common::game::{Action, ActionKind, GameState, ResponseKind}; use macroquad::prelude::*; use crate::{ - game::handle_input, + game::handle_movement_input, net::handle_connection, render::{Camera, Spritesheet, follow_local_player, render}, }; @@ -36,8 +41,15 @@ async fn main() { let mut unresponded_actions = VecDeque::new(); let mut interpolation_basis = SystemTime::now(); let mut debug_view = cfg!(debug_assertions); + let mut t0; + let mut t1 = Instant::now(); + let mut dt; loop { + t0 = t1; + t1 = Instant::now(); + dt = t1.duration_since(t0); + for response in server_to_client_rx.try_iter() { match response.kind() { ResponseKind::SetId { id } => { @@ -80,8 +92,10 @@ async fn main() { } }; } - if let Some(id) = local_player_id { - if let Some(action_kind) = handle_input() { + if let Some(id) = local_player_id + && let Some(local_player) = game_state.players.get_mut(&id) + { + if let Some(action_kind) = handle_movement_input(local_player, &dt) { let action = Action::new(sequence_number, id, action_kind); game_state.apply(action.clone()); if client_to_server_tx.send(action.clone()).is_err() { diff --git a/client/src/render.rs b/client/src/render.rs @@ -98,7 +98,7 @@ pub(crate) fn render( } if debug_view { draw_rectangle(0., 0., 284., 56., BLACK); - draw_text("Library Tech Demo 2026-08-20", 4., 16., 16., WHITE); + draw_text("Library Tech Demo 2026-08-22", 4., 16., 16., WHITE); draw_text(format!("fps {}", get_fps()), 4., 32., 16., WHITE); if let Some(id) = local_player_id && let Some(player) = game.players.get(&id) diff --git a/common/src/game.rs b/common/src/game.rs @@ -23,14 +23,24 @@ fn rects_strictly_overlap(a: &Rect, b: &Rect) -> bool { } impl GameState { + pub const MAX_PLAYER_SPEED_PX_S: f32 = 60.; + pub fn apply(&mut self, action: Action) { match action.kind { ActionKind::Join { name } => { self.players.insert(action.player_id, Player::new(name)); } - ActionKind::Move { velocity } => { - if let Some(player) = self.players.get_mut(&action.player_id) { - player.position += velocity; + ActionKind::Move { new_position } => { + if let Some(player) = self.players.get_mut(&action.player_id) + && let Ok(dt) = action.timestamp.duration_since(player.timestamp) + { + let velocity = (new_position - player.position) / dt.as_secs_f32(); + let limited_velocity = if velocity.length() > GameState::MAX_PLAYER_SPEED_PX_S { + velocity.normalize() * GameState::MAX_PLAYER_SPEED_PX_S + } else { + velocity + }; + player.position += limited_velocity * dt.as_secs_f32(); // Handle collisions with bookshelves let player_hitbox = player.hitbox.rect().offset(player.position); @@ -133,6 +143,7 @@ pub struct Player { pub position: Vec2, pub sprite_index: SpriteIndex, hitbox: RectSerde, + timestamp: SystemTime, } impl Player { @@ -142,6 +153,7 @@ impl Player { position: Vec2::ZERO, sprite_index: SpriteIndex::Meeple, hitbox: RectSerde::new(0., 0., 43.4, 40.2), + timestamp: SystemTime::now(), } } } @@ -194,6 +206,7 @@ impl Bookshelf { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct Action { sequence_number: u32, + timestamp: SystemTime, player_id: Uuid, kind: ActionKind, } @@ -202,6 +215,7 @@ impl Action { pub fn new(sequence_number: u32, player_id: Uuid, kind: ActionKind) -> Self { Action { sequence_number, + timestamp: SystemTime::now(), player_id, kind, } @@ -220,7 +234,7 @@ impl Action { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum ActionKind { Join { name: String }, - Move { velocity: Vec2 }, + Move { new_position: Vec2 }, } #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]