From 33672d8d5a42cc61722a27f3c855dbf67db4d034 Mon Sep 17 00:00:00 2001 From: Xevion Date: Mon, 11 Aug 2025 23:24:23 -0500 Subject: [PATCH] feat: implement collision detection system for entities --- src/entity/collision.rs | 128 ++++++++++++++++++++++++++++++++++++++++ src/entity/ghost.rs | 17 ++++-- src/entity/item.rs | 8 ++- src/entity/mod.rs | 1 + src/entity/pacman.rs | 17 ++++-- src/game.rs | 109 ++++++++++++++++++++++++++++++---- 6 files changed, 259 insertions(+), 21 deletions(-) create mode 100644 src/entity/collision.rs diff --git a/src/entity/collision.rs b/src/entity/collision.rs new file mode 100644 index 0000000..d7e7746 --- /dev/null +++ b/src/entity/collision.rs @@ -0,0 +1,128 @@ +use smallvec::SmallVec; +use std::collections::HashMap; + +use crate::entity::traversal::Position; + +/// Trait for entities that can participate in collision detection. +pub trait Collidable { + /// Returns the current position of this entity. + fn position(&self) -> Position; + + /// Checks if this entity is colliding with another entity. + #[allow(dead_code)] + fn is_colliding_with(&self, other: &dyn Collidable) -> bool { + positions_overlap(&self.position(), &other.position()) + } +} + +/// System for tracking entities by their positions for efficient collision detection. +#[derive(Default)] +pub struct CollisionSystem { + /// Maps node IDs to lists of entity IDs that are at that node + node_entities: HashMap>, + /// Maps entity IDs to their current positions + entity_positions: HashMap, + /// Next available entity ID + next_id: EntityId, +} + +/// Unique identifier for an entity in the collision system +pub type EntityId = u32; + +impl CollisionSystem { + /// Registers an entity with the collision system and returns its ID + pub fn register_entity(&mut self, position: Position) -> EntityId { + let id = self.next_id; + self.next_id += 1; + + self.entity_positions.insert(id, position); + self.update_node_entities(id, position); + + id + } + + /// Updates an entity's position + pub fn update_position(&mut self, entity_id: EntityId, new_position: Position) { + if let Some(old_position) = self.entity_positions.get(&entity_id) { + // Remove from old nodes + self.remove_from_nodes(entity_id, *old_position); + } + + // Update position and add to new nodes + self.entity_positions.insert(entity_id, new_position); + self.update_node_entities(entity_id, new_position); + } + + /// Removes an entity from the collision system + #[allow(dead_code)] + pub fn remove_entity(&mut self, entity_id: EntityId) { + if let Some(position) = self.entity_positions.remove(&entity_id) { + self.remove_from_nodes(entity_id, position); + } + } + + /// Gets all entity IDs at a specific node + pub fn entities_at_node(&self, node: usize) -> &[EntityId] { + self.node_entities.get(&node).map(|v| v.as_slice()).unwrap_or(&[]) + } + + /// Gets all entity IDs that could collide with an entity at the given position + pub fn potential_collisions(&self, position: &Position) -> Vec { + let mut collisions = Vec::new(); + let nodes = get_nodes(position); + + for node in nodes { + collisions.extend(self.entities_at_node(node)); + } + + // Remove duplicates + collisions.sort_unstable(); + collisions.dedup(); + collisions + } + + /// Updates the node_entities map when an entity's position changes + fn update_node_entities(&mut self, entity_id: EntityId, position: Position) { + let nodes = get_nodes(&position); + for node in nodes { + self.node_entities.entry(node).or_default().push(entity_id); + } + } + + /// Removes an entity from all nodes it was previously at + fn remove_from_nodes(&mut self, entity_id: EntityId, position: Position) { + let nodes = get_nodes(&position); + for node in nodes { + if let Some(entities) = self.node_entities.get_mut(&node) { + entities.retain(|&id| id != entity_id); + if entities.is_empty() { + self.node_entities.remove(&node); + } + } + } + } +} + +/// Checks if two positions overlap (entities are at the same location). +fn positions_overlap(a: &Position, b: &Position) -> bool { + let a_nodes = get_nodes(a); + let b_nodes = get_nodes(b); + + // Check if any nodes overlap + a_nodes.iter().any(|a_node| b_nodes.contains(a_node)) + + // TODO: More complex overlap detection, the above is a simple check, but it could become an early filter for more precise calculations later +} + +/// Gets all nodes that an entity is currently at or between. +fn get_nodes(pos: &Position) -> SmallVec<[usize; 2]> { + let mut nodes = SmallVec::new(); + match pos { + Position::AtNode(node) => nodes.push(*node), + Position::BetweenNodes { from, to, .. } => { + nodes.push(*from); + nodes.push(*to); + } + } + nodes +} diff --git a/src/entity/ghost.rs b/src/entity/ghost.rs index bd11a74..28735cc 100644 --- a/src/entity/ghost.rs +++ b/src/entity/ghost.rs @@ -9,10 +9,13 @@ use rand::prelude::*; use smallvec::SmallVec; use tracing::error; -use crate::entity::direction::Direction; -use crate::entity::graph::{Edge, EdgePermissions, Graph, NodeId}; -use crate::entity::r#trait::Entity; -use crate::entity::traversal::Traverser; +use crate::entity::{ + collision::Collidable, + direction::Direction, + graph::{Edge, EdgePermissions, Graph, NodeId}, + r#trait::Entity, + traversal::Traverser, +}; use crate::texture::animated::AnimatedTexture; use crate::texture::directional::DirectionalAnimatedTexture; use crate::texture::sprite::SpriteAtlas; @@ -245,3 +248,9 @@ impl Ghost { } } } + +impl Collidable for Ghost { + fn position(&self) -> crate::entity::traversal::Position { + self.traverser.position + } +} diff --git a/src/entity/item.rs b/src/entity/item.rs index 11cf450..783b89e 100644 --- a/src/entity/item.rs +++ b/src/entity/item.rs @@ -1,6 +1,6 @@ use crate::{ constants, - entity::graph::Graph, + entity::{collision::Collidable, graph::Graph}, error::EntityError, texture::sprite::{Sprite, SpriteAtlas}, }; @@ -93,3 +93,9 @@ impl Item { } } } + +impl Collidable for Item { + fn position(&self) -> crate::entity::traversal::Position { + crate::entity::traversal::Position::AtNode(self.node_index) + } +} diff --git a/src/entity/mod.rs b/src/entity/mod.rs index 700ee6a..d23c3be 100644 --- a/src/entity/mod.rs +++ b/src/entity/mod.rs @@ -1,3 +1,4 @@ +pub mod collision; pub mod direction; pub mod ghost; pub mod graph; diff --git a/src/entity/pacman.rs b/src/entity/pacman.rs index ee0dda4..6d6e63b 100644 --- a/src/entity/pacman.rs +++ b/src/entity/pacman.rs @@ -4,10 +4,13 @@ //! animation, and rendering. Pac-Man moves through the game graph using //! a traverser and displays directional animated textures. -use crate::entity::direction::Direction; -use crate::entity::graph::{Edge, EdgePermissions, Graph, NodeId}; -use crate::entity::r#trait::Entity; -use crate::entity::traversal::Traverser; +use crate::entity::{ + collision::Collidable, + direction::Direction, + graph::{Edge, EdgePermissions, Graph, NodeId}, + r#trait::Entity, + traversal::Traverser, +}; use crate::texture::animated::AnimatedTexture; use crate::texture::directional::DirectionalAnimatedTexture; use crate::texture::sprite::SpriteAtlas; @@ -125,3 +128,9 @@ impl Pacman { } } } + +impl Collidable for Pacman { + fn position(&self) -> crate::entity::traversal::Position { + self.traverser.position + } +} diff --git a/src/game.rs b/src/game.rs index 81b9eac..43643a1 100644 --- a/src/game.rs +++ b/src/game.rs @@ -17,6 +17,7 @@ use crate::{ audio::Audio, constants::{CELL_SIZE, RAW_BOARD}, entity::{ + collision::{Collidable, CollisionSystem, EntityId}, ghost::{Ghost, GhostType}, item::Item, pacman::Pacman, @@ -41,6 +42,12 @@ pub struct Game { pub items: Vec, pub debug_mode: bool, + // Collision system + collision_system: CollisionSystem, + pacman_id: EntityId, + ghost_ids: Vec, + item_ids: Vec, + // Rendering resources atlas: SpriteAtlas, map_texture: AtlasTile, @@ -109,6 +116,26 @@ impl Game { ghosts.push(ghost); } + // Initialize collision system + let mut collision_system = CollisionSystem::default(); + + // Register Pac-Man + let pacman_id = collision_system.register_entity(pacman.position()); + + // Register items + let mut item_ids = Vec::new(); + for item in &items { + let item_id = collision_system.register_entity(item.position()); + item_ids.push(item_id); + } + + // Register ghosts + let mut ghost_ids = Vec::new(); + for ghost in &ghosts { + let ghost_id = collision_system.register_entity(ghost.position()); + ghost_ids.push(ghost_id); + } + Ok(Game { score: 0, map, @@ -116,6 +143,10 @@ impl Game { ghosts, items, debug_mode: false, + collision_system, + pacman_id, + ghost_ids, + item_ids, map_texture, text_texture, audio, @@ -164,6 +195,26 @@ impl Game { *ghost = Ghost::new(&self.map.graph, random_node, ghost_types[i], &self.atlas)?; } + // Reset collision system + self.collision_system = CollisionSystem::default(); + + // Re-register Pac-Man + self.pacman_id = self.collision_system.register_entity(self.pacman.position()); + + // Re-register items + self.item_ids.clear(); + for item in &self.items { + let item_id = self.collision_system.register_entity(item.position()); + self.item_ids.push(item_id); + } + + // Re-register ghosts + self.ghost_ids.clear(); + for ghost in &self.ghosts { + let ghost_id = self.collision_system.register_entity(ghost.position()); + self.ghost_ids.push(ghost_id); + } + Ok(()) } @@ -175,27 +226,61 @@ impl Game { ghost.tick(dt, &self.map.graph); } - // Check for item collisions - self.check_item_collisions(); + // Update collision system positions + self.update_collision_positions(); + + // Check for collisions + self.check_collisions(); } - fn check_item_collisions(&mut self) { - let pacman_node = self.pacman.current_node_id(); + fn update_collision_positions(&mut self) { + // Update Pac-Man's position + self.collision_system.update_position(self.pacman_id, self.pacman.position()); - for item in &mut self.items { - if !item.is_collected() && item.node_index == pacman_node { - item.collect(); - self.score += item.get_score(); + // Update ghost positions + for (ghost, &ghost_id) in self.ghosts.iter().zip(&self.ghost_ids) { + self.collision_system.update_position(ghost_id, ghost.position()); + } + } - // Handle energizer effects - if matches!(item.item_type, crate::entity::item::ItemType::Energizer) { - // TODO: Make ghosts frightened - tracing::info!("Energizer collected! Ghosts should become frightened."); + fn check_collisions(&mut self) { + // Check Pac-Man vs Items + let potential_collisions = self.collision_system.potential_collisions(&self.pacman.position()); + + for entity_id in potential_collisions { + if entity_id != self.pacman_id { + // Check if this is an item collision + if let Some(item_index) = self.find_item_by_id(entity_id) { + let item = &mut self.items[item_index]; + if !item.is_collected() { + item.collect(); + self.score += item.get_score(); + + // Handle energizer effects + if matches!(item.item_type, crate::entity::item::ItemType::Energizer) { + // TODO: Make ghosts frightened + tracing::info!("Energizer collected! Ghosts should become frightened."); + } + } + } + + // Check if this is a ghost collision + if let Some(_ghost_index) = self.find_ghost_by_id(entity_id) { + // TODO: Handle Pac-Man being eaten by ghost + tracing::info!("Pac-Man collided with ghost!"); } } } } + fn find_item_by_id(&self, entity_id: EntityId) -> Option { + self.item_ids.iter().position(|&id| id == entity_id) + } + + fn find_ghost_by_id(&self, entity_id: EntityId) -> Option { + self.ghost_ids.iter().position(|&id| id == entity_id) + } + pub fn draw(&mut self, canvas: &mut Canvas, backbuffer: &mut Texture) -> GameResult<()> { canvas .with_texture_canvas(backbuffer, |canvas| {