commit ddb46383fea7f172435649858df4ce7251df3dce
parent bbd1c094e4d8b48036a58d4178494afcc7b69733
Author: Bradley Gannon <bradley@bradleygannon.com>
Date: Thu, 20 Aug 2026 21:33:56 -0400
Add collision between players and bookshelves, add debug view
Diffstat:
5 files changed, 144 insertions(+), 5 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -89,7 +89,7 @@ name = "common"
version = "0.1.0"
dependencies = [
"anyhow",
- "glam",
+ "macroquad",
"mio",
"postcard",
"serde",
diff --git a/client/src/main.rs b/client/src/main.rs
@@ -35,6 +35,7 @@ async fn main() {
let mut sequence_number = 0;
let mut unresponded_actions = VecDeque::new();
let mut interpolation_basis = SystemTime::now();
+ let mut debug_view = cfg!(debug_assertions);
loop {
for response in server_to_client_rx.try_iter() {
@@ -97,7 +98,16 @@ async fn main() {
interpolation_basis,
local_player_id,
);
- render(&camera, &interpolated_game_state, &spritesheet);
+ if is_key_pressed(KeyCode::F3) {
+ debug_view = !debug_view;
+ }
+ render(
+ &camera,
+ &interpolated_game_state,
+ &spritesheet,
+ debug_view,
+ local_player_id,
+ );
next_frame().await;
}
}
diff --git a/client/src/render.rs b/client/src/render.rs
@@ -70,8 +70,18 @@ impl Index<SpriteIndex> for Spritesheet {
}
}
-pub(crate) fn render(camera: &Camera, game: &GameState, spritesheet: &Spritesheet) {
+pub(crate) fn render(
+ camera: &Camera,
+ game: &GameState,
+ spritesheet: &Spritesheet,
+ debug_view: bool,
+ local_player_id: Option<Uuid>,
+) {
clear_background(MAGENTA);
+ for bookshelf in game.bookshelves.iter() {
+ let bounds: Rect = bookshelf.rect().offset(-camera.position);
+ draw_rectangle(bounds.x, bounds.y, bounds.w, bounds.h, DARKBROWN);
+ }
for player in game.players.values() {
let sprite = match player.sprite_index {
SpriteIndex::Meeple => spritesheet.meeple.clone(),
@@ -86,6 +96,23 @@ pub(crate) fn render(camera: &Camera, game: &GameState, spritesheet: &Spriteshee
BLACK,
);
}
+ if debug_view {
+ draw_rectangle(0., 0., 284., 56., BLACK);
+ draw_text("Library Tech Demo 2026-08-20", 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)
+ {
+ draw_text(format!("id {}", id), 4., 48., 16., WHITE);
+ draw_text(
+ format!("pos ({:.1}, {:.1})", player.position.x, player.position.y),
+ 60.,
+ 32.,
+ 16.,
+ WHITE,
+ );
+ }
+ }
}
pub(crate) fn follow_local_player(
diff --git a/common/Cargo.toml b/common/Cargo.toml
@@ -5,7 +5,7 @@ edition = "2024"
[dependencies]
anyhow = "1.0.104"
-glam = { version = "0.27.0", features = ["scalar-math", "serde"] }
+macroquad = { version = "0.4.16", features = ["glam-serde"] }
mio = { version = "1.2.2", features = ["net", "os-poll"] }
postcard = { version = "1.1.3", default-features = false, features = ["use-std"] }
serde = { version = "1.0.229", features = ["derive"] }
diff --git a/common/src/game.rs b/common/src/game.rs
@@ -3,7 +3,7 @@ use std::{
time::{Duration, SystemTime},
};
-use glam::Vec2;
+use macroquad::math::{Rect, Vec2};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@@ -13,6 +13,13 @@ pub const TICK_DURATION: Duration = Duration::from_micros(16_667);
pub struct GameState {
pub timestamp: SystemTime,
pub players: HashMap<Uuid, Player>,
+ pub bookshelves: Vec<Bookshelf>,
+}
+
+/// Equivalent to [macroquad::math::Rect::overlaps], except using strict comparison (i.e., `<` and
+/// `>` instead of `<=` and `>=`)
+fn rects_strictly_overlap(a: &Rect, b: &Rect) -> bool {
+ a.left() < b.right() && a.right() > b.left() && a.top() < b.bottom() && a.bottom() > b.top()
}
impl GameState {
@@ -24,6 +31,36 @@ impl GameState {
ActionKind::Move { velocity } => {
if let Some(player) = self.players.get_mut(&action.player_id) {
player.position += velocity;
+
+ // Handle collisions with bookshelves
+ let player_hitbox = player.hitbox.rect().offset(player.position);
+ for bookshelf in self.bookshelves.iter() {
+ // If the player's movement would cause their hitbox to intersect with a
+ // bookshelf's hitbox, then compute the minimum axis-aligned motion required
+ // to make them touch but not intersect
+ let bookshelf_hitbox: Rect = bookshelf.bounds.into();
+ let (a, b) = (bookshelf_hitbox, player_hitbox);
+ if rects_strictly_overlap(&a, &b) {
+ // Compute the required motion in each possible direction and select the
+ // smallest among them
+ let up = Vec2::new(0., a.top() - b.bottom());
+ let down = Vec2::new(0., a.bottom() - b.top());
+ let left = Vec2::new(a.left() - b.right(), 0.);
+ let right = Vec2::new(a.right() - b.left(), 0.);
+ let min_y = if up.y.abs() < down.y.abs() { up } else { down };
+ let min_x = if right.x.abs() < left.x.abs() {
+ right
+ } else {
+ left
+ };
+ let min = if min_x.x.abs() < min_y.y.abs() {
+ min_x
+ } else {
+ min_y
+ };
+ player.position += min;
+ }
+ }
}
}
}
@@ -68,6 +105,24 @@ impl Default for GameState {
Self {
timestamp: SystemTime::now(),
players: HashMap::with_capacity(32),
+ bookshelves: vec![
+ Bookshelf {
+ bounds: RectSerde {
+ x: -200.,
+ y: -100.,
+ w: 100.,
+ h: 500.,
+ },
+ },
+ Bookshelf {
+ bounds: RectSerde {
+ x: -400.,
+ y: -100.,
+ w: 100.,
+ h: 500.,
+ },
+ },
+ ],
}
}
}
@@ -77,6 +132,7 @@ pub struct Player {
pub name: String,
pub position: Vec2,
pub sprite_index: SpriteIndex,
+ hitbox: RectSerde,
}
impl Player {
@@ -85,10 +141,56 @@ impl Player {
name,
position: Vec2::ZERO,
sprite_index: SpriteIndex::Meeple,
+ hitbox: RectSerde::new(0., 0., 43.4, 40.2),
}
}
}
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, PartialOrd, Serialize)]
+struct RectSerde {
+ x: f32,
+ y: f32,
+ w: f32,
+ h: f32,
+}
+
+impl RectSerde {
+ fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
+ RectSerde { x, y, w, h }
+ }
+ fn rect(&self) -> Rect {
+ (*self).into()
+ }
+}
+
+impl From<Rect> for RectSerde {
+ fn from(value: Rect) -> Self {
+ RectSerde {
+ x: value.x,
+ y: value.y,
+ w: value.w,
+ h: value.h,
+ }
+ }
+}
+
+impl From<RectSerde> for Rect {
+ fn from(value: RectSerde) -> Self {
+ Rect::new(value.x, value.y, value.w, value.h)
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, PartialOrd, Serialize)]
+pub struct Bookshelf {
+ bounds: RectSerde,
+}
+
+impl Bookshelf {
+ pub fn rect(&self) -> Rect {
+ self.bounds.into()
+ }
+}
+
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Action {
sequence_number: u32,