library

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

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:
MCargo.lock | 2+-
Mclient/src/main.rs | 12+++++++++++-
Mclient/src/render.rs | 29++++++++++++++++++++++++++++-
Mcommon/Cargo.toml | 2+-
Mcommon/src/game.rs | 104++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
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,