mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-15 08:12:32 -06:00
refactor: huge refactor into atlas-based resources
This commit is contained in:
333
src/game.rs
333
src/game.rs
@@ -3,6 +3,7 @@ use std::cell::RefCell;
|
||||
use std::ops::Not;
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::Result;
|
||||
use glam::{IVec2, UVec2};
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::seq::IteratorRandom;
|
||||
@@ -25,150 +26,100 @@ use crate::entity::edible::{reconstruct_edibles, Edible, EdibleKind};
|
||||
use crate::entity::pacman::Pacman;
|
||||
use crate::entity::Renderable;
|
||||
use crate::map::Map;
|
||||
use crate::texture::atlas::{texture_to_static, AtlasTexture};
|
||||
use crate::texture::animated::AnimatedTexture;
|
||||
use crate::texture::blinking::BlinkingTexture;
|
||||
use crate::texture::FrameDrawn;
|
||||
use crate::texture::sprite::{AtlasMapper, AtlasTile, SpriteAtlas};
|
||||
use crate::texture::{get_atlas_tile, sprite};
|
||||
|
||||
/// The main game state.
|
||||
///
|
||||
/// This struct contains all the information necessary to run the game, including
|
||||
/// the canvas, textures, fonts, game objects, and the current score.
|
||||
/// Contains all the information necessary to run the game, including
|
||||
/// the game state, rendering resources, and audio.
|
||||
pub struct Game {
|
||||
canvas: &'static mut Canvas<Window>,
|
||||
map_texture: Texture<'static>,
|
||||
pellet_texture: Rc<Box<dyn FrameDrawn>>,
|
||||
power_pellet_texture: Rc<RefCell<BlinkingTexture>>,
|
||||
font: Font<'static, 'static>,
|
||||
// Game state
|
||||
pacman: Rc<RefCell<Pacman>>,
|
||||
map: Rc<RefCell<Map>>,
|
||||
debug_mode: DebugMode,
|
||||
score: u32,
|
||||
pub audio: Audio,
|
||||
blinky: Blinky,
|
||||
edibles: Vec<Edible>,
|
||||
map: Rc<RefCell<Map>>,
|
||||
score: u32,
|
||||
debug_mode: DebugMode,
|
||||
|
||||
// FPS tracking
|
||||
fps_1s: f64,
|
||||
fps_10s: f64,
|
||||
|
||||
// Rendering resources
|
||||
atlas: Rc<SpriteAtlas>,
|
||||
font: Font<'static, 'static>,
|
||||
map_texture: AtlasTile,
|
||||
|
||||
// Audio
|
||||
pub audio: Audio,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
/// Creates a new `Game` instance.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `canvas` - The SDL canvas to render to.
|
||||
/// * `texture_creator` - The SDL texture creator.
|
||||
/// * `ttf_context` - The SDL TTF context.
|
||||
/// * `_audio_subsystem` - The SDL audio subsystem (currently unused).
|
||||
pub fn new(
|
||||
canvas: &'static mut Canvas<Window>,
|
||||
texture_creator: &TextureCreator<WindowContext>,
|
||||
ttf_context: &sdl2::ttf::Sdl2TtfContext,
|
||||
_audio_subsystem: &sdl2::AudioSubsystem,
|
||||
) -> Game {
|
||||
let map = Rc::new(RefCell::new(Map::new(RAW_BOARD)));
|
||||
|
||||
// Load Pacman texture from asset API
|
||||
let pacman_bytes = get_asset_bytes(Asset::Pacman).expect("Failed to load asset");
|
||||
let pacman_atlas = texture_creator
|
||||
.load_texture_bytes(&pacman_bytes)
|
||||
.expect("Could not load pacman texture from asset API");
|
||||
let pacman = Rc::new(RefCell::new(Pacman::new(UVec2::new(1, 1), pacman_atlas, Rc::clone(&map))));
|
||||
|
||||
// Load ghost textures
|
||||
let ghost_body_bytes = get_asset_bytes(Asset::GhostBody).expect("Failed to load asset");
|
||||
let ghost_body = texture_creator
|
||||
.load_texture_bytes(&ghost_body_bytes)
|
||||
.expect("Could not load ghost body texture from asset API");
|
||||
let ghost_eyes_bytes = get_asset_bytes(Asset::GhostEyes).expect("Failed to load asset");
|
||||
let ghost_eyes = texture_creator
|
||||
.load_texture_bytes(&ghost_eyes_bytes)
|
||||
.expect("Could not load ghost eyes texture from asset API");
|
||||
|
||||
// Create Blinky
|
||||
let blinky = Blinky::new(
|
||||
UVec2::new(13, 11), // Starting position just above ghost house
|
||||
ghost_body,
|
||||
ghost_eyes,
|
||||
let atlas_bytes = get_asset_bytes(Asset::Atlas).expect("Failed to load asset");
|
||||
let atlas_texture = unsafe {
|
||||
sprite::texture_to_static(
|
||||
texture_creator
|
||||
.load_texture_bytes(&atlas_bytes)
|
||||
.expect("Could not load atlas texture from asset API"),
|
||||
)
|
||||
};
|
||||
let atlas_json = get_asset_bytes(Asset::AtlasJson).expect("Failed to load asset");
|
||||
let atlas_mapper: AtlasMapper = serde_json::from_slice(&atlas_json).expect("Could not parse atlas JSON");
|
||||
let atlas = Rc::new(SpriteAtlas::new(atlas_texture, atlas_mapper));
|
||||
let pacman = Rc::new(RefCell::new(Pacman::new(
|
||||
UVec2::new(1, 1),
|
||||
Rc::clone(&atlas),
|
||||
Rc::clone(&map),
|
||||
Rc::clone(&pacman),
|
||||
);
|
||||
|
||||
// Load pellet texture from asset API
|
||||
let pellet_bytes = get_asset_bytes(Asset::Pellet).expect("Failed to load asset");
|
||||
let power_pellet_bytes = get_asset_bytes(Asset::Energizer).expect("Failed to load asset");
|
||||
let pellet_texture: Rc<Box<dyn FrameDrawn>> = Rc::new(Box::new(AtlasTexture::new(
|
||||
unsafe {
|
||||
texture_to_static(
|
||||
texture_creator
|
||||
.load_texture_bytes(&pellet_bytes)
|
||||
.expect("Could not load pellet texture from asset API"),
|
||||
)
|
||||
},
|
||||
1,
|
||||
24,
|
||||
24,
|
||||
None,
|
||||
)));
|
||||
let power_pellet_texture = Rc::new(RefCell::new(BlinkingTexture::new(
|
||||
texture_creator
|
||||
.load_texture_bytes(&power_pellet_bytes)
|
||||
.expect("Could not load power pellet texture from asset API"),
|
||||
1,
|
||||
24,
|
||||
24,
|
||||
None,
|
||||
30, // on_ticks
|
||||
9, // off_ticks
|
||||
)));
|
||||
|
||||
// Load map texture from asset API
|
||||
let map_bytes = get_asset_bytes(Asset::Map).expect("Failed to load asset");
|
||||
let mut map_texture = texture_creator
|
||||
.load_texture_bytes(&map_bytes)
|
||||
.expect("Could not load map texture from asset API");
|
||||
map_texture.set_color_mod(0, 0, 255);
|
||||
let map_texture = unsafe { texture_to_static(map_texture) };
|
||||
|
||||
let blinky = Blinky::new(UVec2::new(13, 11), Rc::clone(&atlas), Rc::clone(&map), Rc::clone(&pacman));
|
||||
let map_texture = get_atlas_tile(&atlas, "maze/full.png");
|
||||
let edibles = reconstruct_edibles(
|
||||
Rc::clone(&map),
|
||||
Rc::clone(&pellet_texture),
|
||||
Rc::clone(&power_pellet_texture),
|
||||
Rc::clone(&pellet_texture), // placeholder for fruit sprite
|
||||
AnimatedTexture::new(vec![get_atlas_tile(&atlas, "maze/pellet.png")], 0),
|
||||
BlinkingTexture::new(
|
||||
AnimatedTexture::new(vec![get_atlas_tile(&atlas, "maze/energizer.png")], 0),
|
||||
17,
|
||||
17,
|
||||
),
|
||||
AnimatedTexture::new(vec![get_atlas_tile(&atlas, "edible/cherry.png")], 0),
|
||||
);
|
||||
|
||||
// Load font from asset API
|
||||
let font = {
|
||||
let font_bytes = get_asset_bytes(Asset::FontKonami).expect("Failed to load asset").into_owned();
|
||||
let font_bytes_static: &'static [u8] = Box::leak(font_bytes.into_boxed_slice());
|
||||
let font_rwops = RWops::from_bytes(font_bytes_static).expect("Failed to create RWops for font");
|
||||
// Leak the ttf_context to get a 'static lifetime
|
||||
let ttf_context_static: &'static sdl2::ttf::Sdl2TtfContext = unsafe { std::mem::transmute(ttf_context) };
|
||||
ttf_context_static
|
||||
.load_font_from_rwops(font_rwops, 24)
|
||||
.expect("Could not load font from asset API")
|
||||
};
|
||||
|
||||
let audio = Audio::new();
|
||||
|
||||
Game {
|
||||
canvas,
|
||||
pacman,
|
||||
debug_mode: DebugMode::None,
|
||||
map,
|
||||
map_texture,
|
||||
pellet_texture,
|
||||
power_pellet_texture,
|
||||
font,
|
||||
score: 0,
|
||||
audio,
|
||||
blinky,
|
||||
edibles,
|
||||
map,
|
||||
score: 0,
|
||||
debug_mode: DebugMode::None,
|
||||
atlas,
|
||||
font,
|
||||
map_texture,
|
||||
audio,
|
||||
fps_1s: 0.0,
|
||||
fps_10s: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles a keyboard event.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `keycode` - The keycode of the key that was pressed.
|
||||
pub fn keyboard_event(&mut self, keycode: Keycode) {
|
||||
// Change direction
|
||||
let direction = Direction::from_keycode(keycode);
|
||||
@@ -209,6 +160,12 @@ impl Game {
|
||||
self.score += points;
|
||||
}
|
||||
|
||||
/// Updates the FPS tracking values.
|
||||
pub fn update_fps(&mut self, fps_1s: f64, fps_10s: f64) {
|
||||
self.fps_1s = fps_1s;
|
||||
self.fps_10s = fps_10s;
|
||||
}
|
||||
|
||||
/// Resets the game to its initial state.
|
||||
pub fn reset(&mut self) {
|
||||
// Reset the map to restore all pellets
|
||||
@@ -249,22 +206,34 @@ impl Game {
|
||||
|
||||
self.edibles = reconstruct_edibles(
|
||||
Rc::clone(&self.map),
|
||||
Rc::clone(&self.pellet_texture),
|
||||
Rc::clone(&self.power_pellet_texture),
|
||||
Rc::clone(&self.pellet_texture), // placeholder for fruit sprite
|
||||
AnimatedTexture::new(vec![get_atlas_tile(&self.atlas, "maze/pellet.png")], 0),
|
||||
BlinkingTexture::new(
|
||||
AnimatedTexture::new(vec![get_atlas_tile(&self.atlas, "maze/energizer.png")], 0),
|
||||
12,
|
||||
12,
|
||||
),
|
||||
AnimatedTexture::new(vec![get_atlas_tile(&self.atlas, "edible/cherry.png")], 0),
|
||||
);
|
||||
}
|
||||
|
||||
/// Advances the game by one tick.
|
||||
pub fn tick(&mut self) {
|
||||
// Advance animation frames for Pacman and Blinky
|
||||
self.pacman.borrow_mut().sprite.tick();
|
||||
self.blinky.body_sprite.tick();
|
||||
self.blinky.eyes_sprite.tick();
|
||||
|
||||
// Advance blinking for power pellets
|
||||
self.power_pellet_texture.borrow_mut().tick();
|
||||
|
||||
self.tick_entities();
|
||||
self.handle_edible_collisions();
|
||||
self.tick_entities();
|
||||
}
|
||||
fn tick_entities(&mut self) {
|
||||
self.pacman.borrow_mut().tick();
|
||||
self.blinky.tick();
|
||||
for edible in self.edibles.iter_mut() {
|
||||
if let EdibleKind::PowerPellet = edible.kind {
|
||||
if let crate::entity::edible::EdibleSprite::PowerPellet(texture) = &mut edible.sprite {
|
||||
texture.tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn handle_edible_collisions(&mut self) {
|
||||
let pacman = self.pacman.borrow();
|
||||
let mut eaten_indices = vec![];
|
||||
for (i, edible) in self.edibles.iter().enumerate() {
|
||||
@@ -272,7 +241,7 @@ impl Game {
|
||||
eaten_indices.push(i);
|
||||
}
|
||||
}
|
||||
drop(pacman); // Release immutable borrow before mutably borrowing self
|
||||
drop(pacman);
|
||||
for &i in eaten_indices.iter().rev() {
|
||||
let edible = &self.edibles[i];
|
||||
match edible.kind {
|
||||
@@ -290,93 +259,109 @@ impl Game {
|
||||
}
|
||||
}
|
||||
self.edibles.remove(i);
|
||||
// Set Pac-Man to skip the next movement tick
|
||||
self.pacman.borrow_mut().skip_move_tick = true;
|
||||
}
|
||||
self.pacman.borrow_mut().tick();
|
||||
self.blinky.tick();
|
||||
}
|
||||
|
||||
/// Draws the entire game to the canvas.
|
||||
pub fn draw(&mut self) {
|
||||
// Clear the screen (black)
|
||||
self.canvas.set_draw_color(Color::RGB(0, 0, 0));
|
||||
self.canvas.clear();
|
||||
|
||||
// Render the map
|
||||
self.canvas
|
||||
.copy(&self.map_texture, None, None)
|
||||
.expect("Could not render texture on canvas");
|
||||
|
||||
// Render all edibles
|
||||
for edible in &self.edibles {
|
||||
edible.render(self.canvas);
|
||||
}
|
||||
|
||||
// Render Pac-Man
|
||||
self.pacman.borrow().render(self.canvas);
|
||||
|
||||
// Render ghost
|
||||
self.blinky.render(self.canvas);
|
||||
|
||||
// Render score
|
||||
self.render_ui();
|
||||
|
||||
// Draw the debug grid
|
||||
match self.debug_mode {
|
||||
DebugMode::Grid => {
|
||||
DebugRenderer::draw_debug_grid(self.canvas, &self.map.borrow(), self.pacman.borrow().base.base.cell_position);
|
||||
let next_cell = <Pacman as crate::entity::Moving>::next_cell(&*self.pacman.borrow(), None);
|
||||
DebugRenderer::draw_next_cell(self.canvas, &self.map.borrow(), next_cell.as_uvec2());
|
||||
}
|
||||
DebugMode::ValidPositions => {
|
||||
DebugRenderer::draw_valid_positions(self.canvas, &mut self.map.borrow_mut());
|
||||
}
|
||||
DebugMode::Pathfinding => {
|
||||
DebugRenderer::draw_pathfinding(self.canvas, &self.blinky, &self.map.borrow());
|
||||
}
|
||||
DebugMode::None => {}
|
||||
}
|
||||
|
||||
// Present the canvas
|
||||
self.canvas.present();
|
||||
/// Draws the entire game to the canvas using a backbuffer.
|
||||
pub fn draw(&mut self, window_canvas: &mut Canvas<Window>, backbuffer: &mut Texture) -> Result<()> {
|
||||
let texture_creator = window_canvas.texture_creator();
|
||||
window_canvas
|
||||
.with_texture_canvas(backbuffer, |texture_canvas| {
|
||||
let this = self as *mut Self;
|
||||
let this = unsafe { &mut *this };
|
||||
texture_canvas.set_draw_color(Color::BLACK);
|
||||
texture_canvas.clear();
|
||||
this.map.borrow_mut().render(texture_canvas, &this.map_texture);
|
||||
for edible in this.edibles.iter_mut() {
|
||||
let _ = edible.render(texture_canvas);
|
||||
}
|
||||
let _ = this.pacman.borrow_mut().render(texture_canvas);
|
||||
let _ = this.blinky.render(texture_canvas);
|
||||
this.render_ui_on(texture_canvas, &texture_creator);
|
||||
match this.debug_mode {
|
||||
DebugMode::Grid => {
|
||||
DebugRenderer::draw_debug_grid(
|
||||
texture_canvas,
|
||||
&this.map.borrow(),
|
||||
this.pacman.borrow().base.base.cell_position,
|
||||
);
|
||||
let next_cell = <Pacman as crate::entity::Moving>::next_cell(&*this.pacman.borrow(), None);
|
||||
DebugRenderer::draw_next_cell(texture_canvas, &this.map.borrow(), next_cell.as_uvec2());
|
||||
}
|
||||
DebugMode::ValidPositions => {
|
||||
DebugRenderer::draw_valid_positions(texture_canvas, &mut this.map.borrow_mut());
|
||||
}
|
||||
DebugMode::Pathfinding => {
|
||||
DebugRenderer::draw_pathfinding(texture_canvas, &this.blinky, &this.map.borrow());
|
||||
}
|
||||
DebugMode::None => {}
|
||||
}
|
||||
})
|
||||
.map_err(|e| anyhow::anyhow!(format!("Failed to render to backbuffer: {e}")))
|
||||
}
|
||||
pub fn present_backbuffer(&self, canvas: &mut Canvas<Window>, backbuffer: &Texture) -> Result<()> {
|
||||
canvas.set_draw_color(Color::BLACK);
|
||||
canvas.clear();
|
||||
canvas.copy(backbuffer, None, None).map_err(anyhow::Error::msg)?;
|
||||
canvas.present();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Renders the user interface, including the score and lives.
|
||||
fn render_ui(&mut self) {
|
||||
fn render_ui_on<C: sdl2::render::RenderTarget>(
|
||||
&mut self,
|
||||
canvas: &mut sdl2::render::Canvas<C>,
|
||||
texture_creator: &TextureCreator<WindowContext>,
|
||||
) {
|
||||
let lives = 3;
|
||||
let score_text = format!("{:02}", self.score);
|
||||
|
||||
let x_offset = 12;
|
||||
let y_offset = 2;
|
||||
let lives_offset = 3;
|
||||
let score_offset = 7 - (score_text.len() as i32);
|
||||
let gap_offset = 6;
|
||||
|
||||
// Render the score and high score
|
||||
self.render_text(
|
||||
self.render_text_on(
|
||||
canvas,
|
||||
&*texture_creator,
|
||||
&format!("{lives}UP HIGH SCORE "),
|
||||
IVec2::new(24 * lives_offset + x_offset, y_offset),
|
||||
Color::WHITE,
|
||||
);
|
||||
self.render_text(
|
||||
self.render_text_on(
|
||||
canvas,
|
||||
&*texture_creator,
|
||||
&score_text,
|
||||
IVec2::new(24 * score_offset + x_offset, 24 + y_offset + gap_offset),
|
||||
Color::WHITE,
|
||||
);
|
||||
|
||||
// Display FPS information in top-left corner
|
||||
// let fps_text = format!("FPS: {:.1} (1s) / {:.1} (10s)", self.fps_1s, self.fps_10s);
|
||||
// self.render_text_on(
|
||||
// canvas,
|
||||
// &*texture_creator,
|
||||
// &fps_text,
|
||||
// IVec2::new(10, 10),
|
||||
// Color::RGB(255, 255, 0), // Yellow color for FPS display
|
||||
// );
|
||||
}
|
||||
|
||||
/// Renders text to the screen at the given position.
|
||||
fn render_text(&mut self, text: &str, position: IVec2, color: Color) {
|
||||
fn render_text_on<C: sdl2::render::RenderTarget>(
|
||||
&mut self,
|
||||
canvas: &mut sdl2::render::Canvas<C>,
|
||||
texture_creator: &TextureCreator<WindowContext>,
|
||||
text: &str,
|
||||
position: IVec2,
|
||||
color: Color,
|
||||
) {
|
||||
let surface = self.font.render(text).blended(color).expect("Could not render text surface");
|
||||
|
||||
let texture_creator = self.canvas.texture_creator();
|
||||
let texture = texture_creator
|
||||
.create_texture_from_surface(&surface)
|
||||
.expect("Could not create texture from surface");
|
||||
let query = texture.query();
|
||||
|
||||
let dst_rect = sdl2::rect::Rect::new(position.x, position.y, query.width, query.height);
|
||||
|
||||
self.canvas
|
||||
canvas
|
||||
.copy(&texture, None, Some(dst_rect))
|
||||
.expect("Could not render text texture");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user