mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-06 07:15:41 -06:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66b6cdf01b |
260
src/animation.rs
260
src/animation.rs
@@ -1,4 +1,4 @@
|
||||
//! This module provides a simple animation system for textures.
|
||||
//! This module provides a simple animation and atlas system for textures.
|
||||
use sdl2::{
|
||||
rect::Rect,
|
||||
render::{Canvas, Texture},
|
||||
@@ -7,144 +7,75 @@ use sdl2::{
|
||||
|
||||
use crate::direction::Direction;
|
||||
|
||||
/// An animated texture, which is a texture that is rendered as a series of
|
||||
/// frames.
|
||||
///
|
||||
/// This struct manages the state of an animated texture, including the current
|
||||
/// frame and the number of frames in the animation.
|
||||
pub struct AnimatedTexture<'a> {
|
||||
// Parameters
|
||||
raw_texture: Texture<'a>,
|
||||
offset: (i32, i32),
|
||||
ticks_per_frame: u32,
|
||||
frame_count: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
// State
|
||||
ticker: u32,
|
||||
reversed: bool,
|
||||
}
|
||||
|
||||
impl<'a> AnimatedTexture<'a> {
|
||||
pub fn new(
|
||||
texture: Texture<'a>,
|
||||
ticks_per_frame: u32,
|
||||
frame_count: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
offset: Option<(i32, i32)>,
|
||||
) -> Self {
|
||||
AnimatedTexture {
|
||||
raw_texture: texture,
|
||||
ticker: 0,
|
||||
reversed: false,
|
||||
ticks_per_frame,
|
||||
frame_count,
|
||||
width,
|
||||
height,
|
||||
offset: offset.unwrap_or((0, 0)),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_frame(&self) -> u32 {
|
||||
self.ticker / self.ticks_per_frame
|
||||
}
|
||||
|
||||
/// Advances the animation by one tick.
|
||||
///
|
||||
/// This method updates the internal ticker that tracks the current frame
|
||||
/// of the animation. The animation automatically reverses direction when
|
||||
/// it reaches the end, creating a ping-pong effect.
|
||||
///
|
||||
/// When `reversed` is `false`, the ticker increments until it reaches
|
||||
/// the total number of ticks for all frames, then reverses direction.
|
||||
/// When `reversed` is `true`, the ticker decrements until it reaches 0,
|
||||
/// then reverses direction again.
|
||||
pub fn tick(&mut self) {
|
||||
if self.reversed {
|
||||
self.ticker -= 1;
|
||||
|
||||
if self.ticker == 0 {
|
||||
self.reversed = !self.reversed;
|
||||
}
|
||||
} else {
|
||||
self.ticker += 1;
|
||||
|
||||
if self.ticker + 1 == self.ticks_per_frame * self.frame_count {
|
||||
self.reversed = !self.reversed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the source rectangle for a specific frame of the animated texture.
|
||||
///
|
||||
/// This method calculates the position and dimensions of a frame within the
|
||||
/// texture atlas. Frames are arranged horizontally in a single row, so the
|
||||
/// rectangle's x-coordinate is calculated by multiplying the frame index
|
||||
/// by the frame width.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `frame` - The frame index to get the rectangle for (0-based)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `Rect` representing the source rectangle for the specified frame
|
||||
fn get_frame_rect(&self, frame: u32) -> Option<Rect> {
|
||||
if frame >= self.frame_count {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Rect::new(
|
||||
frame as i32 * self.width as i32,
|
||||
0,
|
||||
self.width,
|
||||
self.height,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
/// Trait for drawable atlas-based textures
|
||||
pub trait FrameDrawn {
|
||||
fn render(
|
||||
&mut self,
|
||||
canvas: &mut Canvas<Window>,
|
||||
position: (i32, i32),
|
||||
direction: Direction,
|
||||
) {
|
||||
self.render_static(canvas, position, direction, Some(self.current_frame()));
|
||||
self.tick();
|
||||
frame: Option<u32>,
|
||||
);
|
||||
}
|
||||
|
||||
/// A texture atlas abstraction for static (non-animated) rendering.
|
||||
pub struct AtlasTexture<'a> {
|
||||
pub raw_texture: Texture<'a>,
|
||||
pub offset: (i32, i32),
|
||||
pub frame_count: u32,
|
||||
pub frame_width: u32,
|
||||
pub frame_height: u32,
|
||||
}
|
||||
|
||||
impl<'a> AtlasTexture<'a> {
|
||||
pub fn new(
|
||||
texture: Texture<'a>,
|
||||
frame_count: u32,
|
||||
frame_width: u32,
|
||||
frame_height: u32,
|
||||
offset: Option<(i32, i32)>,
|
||||
) -> Self {
|
||||
AtlasTexture {
|
||||
raw_texture: texture,
|
||||
frame_count,
|
||||
frame_width,
|
||||
frame_height,
|
||||
offset: offset.unwrap_or((0, 0)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders a specific frame of the animated texture to the canvas.
|
||||
///
|
||||
/// This method renders a static frame without advancing the animation ticker.
|
||||
/// It's useful for displaying a specific frame, such as when an entity is stopped
|
||||
/// or when you want to manually control which frame is displayed.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `canvas` - The SDL canvas to render to
|
||||
/// * `position` - The pixel position where the texture should be rendered
|
||||
/// * `direction` - The direction to rotate the texture based on entity facing
|
||||
/// * `frame` - Optional specific frame to render. If `None`, uses the current frame
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the specified frame is out of bounds for this texture.
|
||||
pub fn render_static(
|
||||
pub fn get_frame_rect(&self, frame: u32) -> Option<Rect> {
|
||||
if frame >= self.frame_count {
|
||||
return None;
|
||||
}
|
||||
Some(Rect::new(
|
||||
frame as i32 * self.frame_width as i32,
|
||||
0,
|
||||
self.frame_width,
|
||||
self.frame_height,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn set_color_modulation(&mut self, r: u8, g: u8, b: u8) {
|
||||
self.raw_texture.set_color_mod(r, g, b);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FrameDrawn for AtlasTexture<'a> {
|
||||
fn render(
|
||||
&mut self,
|
||||
canvas: &mut Canvas<Window>,
|
||||
position: (i32, i32),
|
||||
direction: Direction,
|
||||
frame: Option<u32>,
|
||||
) {
|
||||
let texture_source_frame_rect =
|
||||
self.get_frame_rect(frame.unwrap_or_else(|| self.current_frame()));
|
||||
let texture_source_frame_rect = self.get_frame_rect(frame.unwrap_or(0));
|
||||
let canvas_destination_rect = Rect::new(
|
||||
position.0 + self.offset.0,
|
||||
position.1 + self.offset.1,
|
||||
self.width,
|
||||
self.height,
|
||||
self.frame_width,
|
||||
self.frame_height,
|
||||
);
|
||||
|
||||
canvas
|
||||
.copy_ex(
|
||||
&self.raw_texture,
|
||||
@@ -157,9 +88,88 @@ impl<'a> AnimatedTexture<'a> {
|
||||
)
|
||||
.expect("Could not render texture on canvas");
|
||||
}
|
||||
}
|
||||
|
||||
/// An animated texture using a texture atlas.
|
||||
pub struct AnimatedAtlasTexture<'a> {
|
||||
pub atlas: AtlasTexture<'a>,
|
||||
pub ticks_per_frame: u32,
|
||||
pub ticker: u32,
|
||||
pub reversed: bool,
|
||||
pub paused: bool,
|
||||
}
|
||||
|
||||
impl<'a> AnimatedAtlasTexture<'a> {
|
||||
pub fn new(
|
||||
texture: Texture<'a>,
|
||||
ticks_per_frame: u32,
|
||||
frame_count: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
offset: Option<(i32, i32)>,
|
||||
) -> Self {
|
||||
AnimatedAtlasTexture {
|
||||
atlas: AtlasTexture::new(texture, frame_count, width, height, offset),
|
||||
ticks_per_frame,
|
||||
ticker: 0,
|
||||
reversed: false,
|
||||
paused: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn current_frame(&self) -> u32 {
|
||||
self.ticker / self.ticks_per_frame
|
||||
}
|
||||
|
||||
/// Advances the animation by one tick, unless paused.
|
||||
pub fn tick(&mut self) {
|
||||
if self.paused {
|
||||
return;
|
||||
}
|
||||
if self.reversed {
|
||||
if self.ticker > 0 {
|
||||
self.ticker -= 1;
|
||||
}
|
||||
if self.ticker == 0 {
|
||||
self.reversed = !self.reversed;
|
||||
}
|
||||
} else {
|
||||
self.ticker += 1;
|
||||
if self.ticker + 1 == self.ticks_per_frame * self.atlas.frame_count {
|
||||
self.reversed = !self.reversed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause(&mut self) {
|
||||
self.paused = true;
|
||||
}
|
||||
pub fn resume(&mut self) {
|
||||
self.paused = false;
|
||||
}
|
||||
pub fn is_paused(&self) -> bool {
|
||||
self.paused
|
||||
}
|
||||
|
||||
/// Sets the color modulation for the texture.
|
||||
pub fn set_color_modulation(&mut self, r: u8, g: u8, b: u8) {
|
||||
self.raw_texture.set_color_mod(r, g, b);
|
||||
self.atlas.set_color_modulation(r, g, b);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FrameDrawn for AnimatedAtlasTexture<'a> {
|
||||
fn render(
|
||||
&mut self,
|
||||
canvas: &mut Canvas<Window>,
|
||||
position: (i32, i32),
|
||||
direction: Direction,
|
||||
frame: Option<u32>,
|
||||
) {
|
||||
self.atlas.render(
|
||||
canvas,
|
||||
position,
|
||||
direction,
|
||||
frame.or(Some(self.current_frame())),
|
||||
);
|
||||
self.tick();
|
||||
}
|
||||
}
|
||||
|
||||
63
src/game.rs
63
src/game.rs
@@ -14,6 +14,7 @@ use tracing::event;
|
||||
|
||||
use crate::audio::Audio;
|
||||
use crate::{
|
||||
animation::{AtlasTexture, FrameDrawn},
|
||||
constants::{MapTile, BOARD_HEIGHT, BOARD_WIDTH, RAW_BOARD},
|
||||
direction::Direction,
|
||||
entity::{Entity, Renderable},
|
||||
@@ -48,8 +49,8 @@ pub enum DebugMode {
|
||||
pub struct Game<'a> {
|
||||
canvas: &'a mut Canvas<Window>,
|
||||
map_texture: Texture<'a>,
|
||||
pellet_texture: Texture<'a>,
|
||||
power_pellet_texture: Texture<'a>,
|
||||
pellet_texture: AtlasTexture<'a>,
|
||||
power_pellet_texture: AtlasTexture<'a>,
|
||||
font: Font<'a, 'static>,
|
||||
pacman: Rc<RefCell<Pacman<'a>>>,
|
||||
map: Rc<std::cell::RefCell<Map>>,
|
||||
@@ -105,14 +106,24 @@ impl Game<'_> {
|
||||
);
|
||||
|
||||
// Load pellet texture from embedded data
|
||||
let pellet_texture = texture_creator
|
||||
.load_texture_bytes(PELLET_TEXTURE_DATA)
|
||||
.expect("Could not load pellet texture from embedded data");
|
||||
|
||||
// Load power pellet texture from embedded data
|
||||
let power_pellet_texture = texture_creator
|
||||
.load_texture_bytes(POWER_PELLET_TEXTURE_DATA)
|
||||
.expect("Could not load power pellet texture from embedded data");
|
||||
let pellet_texture = AtlasTexture::new(
|
||||
texture_creator
|
||||
.load_texture_bytes(PELLET_TEXTURE_DATA)
|
||||
.expect("Could not load pellet texture from embedded data"),
|
||||
1,
|
||||
24,
|
||||
24,
|
||||
None,
|
||||
);
|
||||
let power_pellet_texture = AtlasTexture::new(
|
||||
texture_creator
|
||||
.load_texture_bytes(POWER_PELLET_TEXTURE_DATA)
|
||||
.expect("Could not load power pellet texture from embedded data"),
|
||||
1,
|
||||
24,
|
||||
24,
|
||||
None,
|
||||
);
|
||||
|
||||
// Load font from embedded data
|
||||
let font_rwops = RWops::from_bytes(FONT_DATA).expect("Failed to create RWops for font");
|
||||
@@ -277,18 +288,26 @@ impl Game<'_> {
|
||||
.get_tile((x as i32, y as i32))
|
||||
.unwrap_or(MapTile::Empty);
|
||||
|
||||
let texture = match tile {
|
||||
MapTile::Pellet => Some(&self.pellet_texture),
|
||||
MapTile::PowerPellet => Some(&self.power_pellet_texture),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(texture) = texture {
|
||||
let position = Map::cell_to_pixel((x, y));
|
||||
let dst_rect = sdl2::rect::Rect::new(position.0, position.1, 24, 24);
|
||||
self.canvas
|
||||
.copy(texture, None, Some(dst_rect))
|
||||
.expect("Could not render pellet");
|
||||
match tile {
|
||||
MapTile::Pellet => {
|
||||
let position = Map::cell_to_pixel((x, y));
|
||||
self.pellet_texture.render(
|
||||
self.canvas,
|
||||
position,
|
||||
Direction::Right,
|
||||
Some(0),
|
||||
);
|
||||
}
|
||||
MapTile::PowerPellet => {
|
||||
let position = Map::cell_to_pixel((x, y));
|
||||
self.power_pellet_texture.render(
|
||||
self.canvas,
|
||||
position,
|
||||
Direction::Right,
|
||||
Some(0),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
src/ghost.rs
14
src/ghost.rs
@@ -2,7 +2,7 @@ use pathfinding::prelude::dijkstra;
|
||||
use rand::Rng;
|
||||
|
||||
use crate::{
|
||||
animation::AnimatedTexture,
|
||||
animation::{AnimatedAtlasTexture, FrameDrawn},
|
||||
constants::{MapTile, BOARD_WIDTH},
|
||||
direction::Direction,
|
||||
entity::{Entity, MovableEntity, Renderable},
|
||||
@@ -58,9 +58,9 @@ pub struct Ghost<'a> {
|
||||
/// Reference to Pac-Man for targeting
|
||||
pub pacman: std::rc::Rc<std::cell::RefCell<Pacman<'a>>>,
|
||||
/// Ghost body sprite
|
||||
body_sprite: AnimatedTexture<'a>,
|
||||
body_sprite: AnimatedAtlasTexture<'a>,
|
||||
/// Ghost eyes sprite
|
||||
eyes_sprite: AnimatedTexture<'a>,
|
||||
eyes_sprite: AnimatedAtlasTexture<'a>,
|
||||
}
|
||||
|
||||
impl Ghost<'_> {
|
||||
@@ -74,7 +74,7 @@ impl Ghost<'_> {
|
||||
pacman: std::rc::Rc<std::cell::RefCell<Pacman<'a>>>,
|
||||
) -> Ghost<'a> {
|
||||
let color = ghost_type.color();
|
||||
let mut body_sprite = AnimatedTexture::new(body_texture, 8, 2, 32, 32, Some((-4, -4)));
|
||||
let mut body_sprite = AnimatedAtlasTexture::new(body_texture, 8, 2, 32, 32, Some((-4, -4)));
|
||||
body_sprite.set_color_modulation(color.r, color.g, color.b);
|
||||
let pixel_position = Map::cell_to_pixel(starting_position);
|
||||
Ghost {
|
||||
@@ -90,7 +90,7 @@ impl Ghost<'_> {
|
||||
ghost_type,
|
||||
pacman,
|
||||
body_sprite,
|
||||
eyes_sprite: AnimatedTexture::new(eyes_texture, 1, 4, 32, 32, Some((-4, -4))),
|
||||
eyes_sprite: AnimatedAtlasTexture::new(eyes_texture, 1, 4, 32, 32, Some((-4, -4))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ impl Renderable for Ghost<'_> {
|
||||
self.body_sprite
|
||||
.set_color_modulation(color.r, color.g, color.b);
|
||||
self.body_sprite
|
||||
.render(canvas, self.base.pixel_position, Direction::Right);
|
||||
.render(canvas, self.base.pixel_position, Direction::Right, None);
|
||||
}
|
||||
|
||||
// Always render eyes on top
|
||||
@@ -314,7 +314,7 @@ impl Renderable for Ghost<'_> {
|
||||
}
|
||||
};
|
||||
|
||||
self.eyes_sprite.render_static(
|
||||
self.eyes_sprite.render(
|
||||
canvas,
|
||||
self.base.pixel_position,
|
||||
Direction::Right,
|
||||
|
||||
@@ -9,7 +9,7 @@ use sdl2::{
|
||||
use tracing::event;
|
||||
|
||||
use crate::{
|
||||
animation::AnimatedTexture,
|
||||
animation::{AnimatedAtlasTexture, FrameDrawn},
|
||||
direction::Direction,
|
||||
entity::{Entity, MovableEntity, Renderable},
|
||||
map::Map,
|
||||
@@ -24,7 +24,7 @@ pub struct Pacman<'a> {
|
||||
pub next_direction: Option<Direction>,
|
||||
/// Whether Pac-Man is currently stopped.
|
||||
pub stopped: bool,
|
||||
sprite: AnimatedTexture<'a>,
|
||||
sprite: AnimatedAtlasTexture<'a>,
|
||||
}
|
||||
|
||||
impl Pacman<'_> {
|
||||
@@ -46,7 +46,7 @@ impl Pacman<'_> {
|
||||
),
|
||||
next_direction: None,
|
||||
stopped: false,
|
||||
sprite: AnimatedTexture::new(atlas, 2, 3, 32, 32, Some((-4, -4))),
|
||||
sprite: AnimatedAtlasTexture::new(atlas, 2, 3, 32, 32, Some((-4, -4))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ impl Entity for Pacman<'_> {
|
||||
impl Renderable for Pacman<'_> {
|
||||
fn render(&mut self, canvas: &mut Canvas<Window>) {
|
||||
if self.stopped {
|
||||
self.sprite.render_static(
|
||||
self.sprite.render(
|
||||
canvas,
|
||||
self.base.pixel_position,
|
||||
self.base.direction,
|
||||
@@ -115,7 +115,7 @@ impl Renderable for Pacman<'_> {
|
||||
);
|
||||
} else {
|
||||
self.sprite
|
||||
.render(canvas, self.base.pixel_position, self.base.direction);
|
||||
.render(canvas, self.base.pixel_position, self.base.direction, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user