mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-10 20:07:53 -06:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 33672d8d5a |
128
src/entity/collision.rs
Normal file
128
src/entity/collision.rs
Normal 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
|
||||||
|
}
|
||||||
@@ -9,10 +9,13 @@ use rand::prelude::*;
|
|||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::entity::direction::Direction;
|
use crate::entity::{
|
||||||
use crate::entity::graph::{Edge, EdgePermissions, Graph, NodeId};
|
collision::Collidable,
|
||||||
use crate::entity::r#trait::Entity;
|
direction::Direction,
|
||||||
use crate::entity::traversal::Traverser;
|
graph::{Edge, EdgePermissions, Graph, NodeId},
|
||||||
|
r#trait::Entity,
|
||||||
|
traversal::Traverser,
|
||||||
|
};
|
||||||
use crate::texture::animated::AnimatedTexture;
|
use crate::texture::animated::AnimatedTexture;
|
||||||
use crate::texture::directional::DirectionalAnimatedTexture;
|
use crate::texture::directional::DirectionalAnimatedTexture;
|
||||||
use crate::texture::sprite::SpriteAtlas;
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
constants,
|
constants,
|
||||||
entity::graph::Graph,
|
entity::{collision::Collidable, graph::Graph},
|
||||||
error::EntityError,
|
error::EntityError,
|
||||||
texture::sprite::{Sprite, SpriteAtlas},
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod collision;
|
||||||
pub mod direction;
|
pub mod direction;
|
||||||
pub mod ghost;
|
pub mod ghost;
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
|
|||||||
@@ -4,10 +4,13 @@
|
|||||||
//! animation, and rendering. Pac-Man moves through the game graph using
|
//! animation, and rendering. Pac-Man moves through the game graph using
|
||||||
//! a traverser and displays directional animated textures.
|
//! a traverser and displays directional animated textures.
|
||||||
|
|
||||||
use crate::entity::direction::Direction;
|
use crate::entity::{
|
||||||
use crate::entity::graph::{Edge, EdgePermissions, Graph, NodeId};
|
collision::Collidable,
|
||||||
use crate::entity::r#trait::Entity;
|
direction::Direction,
|
||||||
use crate::entity::traversal::Traverser;
|
graph::{Edge, EdgePermissions, Graph, NodeId},
|
||||||
|
r#trait::Entity,
|
||||||
|
traversal::Traverser,
|
||||||
|
};
|
||||||
use crate::texture::animated::AnimatedTexture;
|
use crate::texture::animated::AnimatedTexture;
|
||||||
use crate::texture::directional::DirectionalAnimatedTexture;
|
use crate::texture::directional::DirectionalAnimatedTexture;
|
||||||
use crate::texture::sprite::SpriteAtlas;
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
109
src/game.rs
109
src/game.rs
@@ -17,6 +17,7 @@ use crate::{
|
|||||||
audio::Audio,
|
audio::Audio,
|
||||||
constants::{CELL_SIZE, RAW_BOARD},
|
constants::{CELL_SIZE, RAW_BOARD},
|
||||||
entity::{
|
entity::{
|
||||||
|
collision::{Collidable, CollisionSystem, EntityId},
|
||||||
ghost::{Ghost, GhostType},
|
ghost::{Ghost, GhostType},
|
||||||
item::Item,
|
item::Item,
|
||||||
pacman::Pacman,
|
pacman::Pacman,
|
||||||
@@ -41,6 +42,12 @@ pub struct Game {
|
|||||||
pub items: Vec<Item>,
|
pub items: Vec<Item>,
|
||||||
pub debug_mode: bool,
|
pub debug_mode: bool,
|
||||||
|
|
||||||
|
// Collision system
|
||||||
|
collision_system: CollisionSystem,
|
||||||
|
pacman_id: EntityId,
|
||||||
|
ghost_ids: Vec<EntityId>,
|
||||||
|
item_ids: Vec<EntityId>,
|
||||||
|
|
||||||
// Rendering resources
|
// Rendering resources
|
||||||
atlas: SpriteAtlas,
|
atlas: SpriteAtlas,
|
||||||
map_texture: AtlasTile,
|
map_texture: AtlasTile,
|
||||||
@@ -109,6 +116,26 @@ impl Game {
|
|||||||
ghosts.push(ghost);
|
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 {
|
Ok(Game {
|
||||||
score: 0,
|
score: 0,
|
||||||
map,
|
map,
|
||||||
@@ -116,6 +143,10 @@ impl Game {
|
|||||||
ghosts,
|
ghosts,
|
||||||
items,
|
items,
|
||||||
debug_mode: false,
|
debug_mode: false,
|
||||||
|
collision_system,
|
||||||
|
pacman_id,
|
||||||
|
ghost_ids,
|
||||||
|
item_ids,
|
||||||
map_texture,
|
map_texture,
|
||||||
text_texture,
|
text_texture,
|
||||||
audio,
|
audio,
|
||||||
@@ -164,6 +195,26 @@ impl Game {
|
|||||||
*ghost = Ghost::new(&self.map.graph, random_node, ghost_types[i], &self.atlas)?;
|
*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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,27 +226,61 @@ impl Game {
|
|||||||
ghost.tick(dt, &self.map.graph);
|
ghost.tick(dt, &self.map.graph);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for item collisions
|
// Update collision system positions
|
||||||
self.check_item_collisions();
|
self.update_collision_positions();
|
||||||
|
|
||||||
|
// Check for collisions
|
||||||
|
self.check_collisions();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_item_collisions(&mut self) {
|
fn update_collision_positions(&mut self) {
|
||||||
let pacman_node = self.pacman.current_node_id();
|
// Update Pac-Man's position
|
||||||
|
self.collision_system.update_position(self.pacman_id, self.pacman.position());
|
||||||
|
|
||||||
for item in &mut self.items {
|
// Update ghost positions
|
||||||
if !item.is_collected() && item.node_index == pacman_node {
|
for (ghost, &ghost_id) in self.ghosts.iter().zip(&self.ghost_ids) {
|
||||||
item.collect();
|
self.collision_system.update_position(ghost_id, ghost.position());
|
||||||
self.score += item.get_score();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle energizer effects
|
fn check_collisions(&mut self) {
|
||||||
if matches!(item.item_type, crate::entity::item::ItemType::Energizer) {
|
// Check Pac-Man vs Items
|
||||||
// TODO: Make ghosts frightened
|
let potential_collisions = self.collision_system.potential_collisions(&self.pacman.position());
|
||||||
tracing::info!("Energizer collected! Ghosts should become frightened.");
|
|
||||||
|
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<()> {
|
pub fn draw<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>, backbuffer: &mut Texture) -> GameResult<()> {
|
||||||
canvas
|
canvas
|
||||||
.with_texture_canvas(backbuffer, |canvas| {
|
.with_texture_canvas(backbuffer, |canvas| {
|
||||||
|
|||||||
Reference in New Issue
Block a user