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:
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)]