refactor: huge refactor into atlas-based resources

This commit is contained in:
2025-07-26 12:20:04 -05:00
parent 6ca2e01fba
commit 8e5ec9fef0
17 changed files with 1700 additions and 463 deletions

View File

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