Compare commits

...

1 Commits

Author SHA1 Message Date
33672d8d5a feat: implement collision detection system for entities 2025-08-11 23:24:23 -05:00
6 changed files with 259 additions and 21 deletions

128
src/entity/collision.rs Normal file
View File

@@ -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<usize, Vec<EntityId>>,
/// Maps entity IDs to their current positions
entity_positions: HashMap<EntityId, Position>,
/// 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<EntityId> {
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
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -1,3 +1,4 @@
pub mod collision;
pub mod direction;
pub mod ghost;
pub mod graph;

View File

@@ -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
}
}

View File

@@ -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<Item>,
pub debug_mode: bool,
// Collision system
collision_system: CollisionSystem,
pacman_id: EntityId,
ghost_ids: Vec<EntityId>,
item_ids: Vec<EntityId>,
// 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<usize> {
self.item_ids.iter().position(|&id| id == entity_id)
}
fn find_ghost_by_id(&self, entity_id: EntityId) -> Option<usize> {
self.ghost_ids.iter().position(|&id| id == entity_id)
}
pub fn draw<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>, backbuffer: &mut Texture) -> GameResult<()> {
canvas
.with_texture_canvas(backbuffer, |canvas| {