Compare commits

..

1 Commits

Author SHA1 Message Date
66b6cdf01b feat: split animated texture away from atlas texture details 2025-07-23 16:25:40 -05:00
4 changed files with 188 additions and 159 deletions

View File

@@ -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::{ use sdl2::{
rect::Rect, rect::Rect,
render::{Canvas, Texture}, render::{Canvas, Texture},
@@ -7,144 +7,75 @@ use sdl2::{
use crate::direction::Direction; use crate::direction::Direction;
/// An animated texture, which is a texture that is rendered as a series of /// Trait for drawable atlas-based textures
/// frames. pub trait FrameDrawn {
/// fn render(
/// 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(
&mut self, &mut self,
canvas: &mut Canvas<Window>, canvas: &mut Canvas<Window>,
position: (i32, i32), position: (i32, i32),
direction: Direction, direction: Direction,
) { frame: Option<u32>,
self.render_static(canvas, position, direction, Some(self.current_frame())); );
self.tick(); }
/// 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. pub fn get_frame_rect(&self, frame: u32) -> Option<Rect> {
/// if frame >= self.frame_count {
/// This method renders a static frame without advancing the animation ticker. return None;
/// 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. Some(Rect::new(
/// frame as i32 * self.frame_width as i32,
/// # Arguments 0,
/// self.frame_width,
/// * `canvas` - The SDL canvas to render to self.frame_height,
/// * `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
/// pub fn set_color_modulation(&mut self, r: u8, g: u8, b: u8) {
/// # Panics self.raw_texture.set_color_mod(r, g, b);
/// }
/// Panics if the specified frame is out of bounds for this texture. }
pub fn render_static(
impl<'a> FrameDrawn for AtlasTexture<'a> {
fn render(
&mut self, &mut self,
canvas: &mut Canvas<Window>, canvas: &mut Canvas<Window>,
position: (i32, i32), position: (i32, i32),
direction: Direction, direction: Direction,
frame: Option<u32>, frame: Option<u32>,
) { ) {
let texture_source_frame_rect = let texture_source_frame_rect = self.get_frame_rect(frame.unwrap_or(0));
self.get_frame_rect(frame.unwrap_or_else(|| self.current_frame()));
let canvas_destination_rect = Rect::new( let canvas_destination_rect = Rect::new(
position.0 + self.offset.0, position.0 + self.offset.0,
position.1 + self.offset.1, position.1 + self.offset.1,
self.width, self.frame_width,
self.height, self.frame_height,
); );
canvas canvas
.copy_ex( .copy_ex(
&self.raw_texture, &self.raw_texture,
@@ -157,9 +88,88 @@ impl<'a> AnimatedTexture<'a> {
) )
.expect("Could not render texture on canvas"); .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) { 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();
} }
} }

View File

@@ -14,6 +14,7 @@ use tracing::event;
use crate::audio::Audio; use crate::audio::Audio;
use crate::{ use crate::{
animation::{AtlasTexture, FrameDrawn},
constants::{MapTile, BOARD_HEIGHT, BOARD_WIDTH, RAW_BOARD}, constants::{MapTile, BOARD_HEIGHT, BOARD_WIDTH, RAW_BOARD},
direction::Direction, direction::Direction,
entity::{Entity, Renderable}, entity::{Entity, Renderable},
@@ -48,8 +49,8 @@ pub enum DebugMode {
pub struct Game<'a> { pub struct Game<'a> {
canvas: &'a mut Canvas<Window>, canvas: &'a mut Canvas<Window>,
map_texture: Texture<'a>, map_texture: Texture<'a>,
pellet_texture: Texture<'a>, pellet_texture: AtlasTexture<'a>,
power_pellet_texture: Texture<'a>, power_pellet_texture: AtlasTexture<'a>,
font: Font<'a, 'static>, font: Font<'a, 'static>,
pacman: Rc<RefCell<Pacman<'a>>>, pacman: Rc<RefCell<Pacman<'a>>>,
map: Rc<std::cell::RefCell<Map>>, map: Rc<std::cell::RefCell<Map>>,
@@ -105,14 +106,24 @@ impl Game<'_> {
); );
// Load pellet texture from embedded data // Load pellet texture from embedded data
let pellet_texture = texture_creator let pellet_texture = AtlasTexture::new(
.load_texture_bytes(PELLET_TEXTURE_DATA) texture_creator
.expect("Could not load pellet texture from embedded data"); .load_texture_bytes(PELLET_TEXTURE_DATA)
.expect("Could not load pellet texture from embedded data"),
// Load power pellet texture from embedded data 1,
let power_pellet_texture = texture_creator 24,
.load_texture_bytes(POWER_PELLET_TEXTURE_DATA) 24,
.expect("Could not load power pellet texture from embedded data"); 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 // Load font from embedded data
let font_rwops = RWops::from_bytes(FONT_DATA).expect("Failed to create RWops for font"); 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)) .get_tile((x as i32, y as i32))
.unwrap_or(MapTile::Empty); .unwrap_or(MapTile::Empty);
let texture = match tile { match tile {
MapTile::Pellet => Some(&self.pellet_texture), MapTile::Pellet => {
MapTile::PowerPellet => Some(&self.power_pellet_texture), let position = Map::cell_to_pixel((x, y));
_ => None, self.pellet_texture.render(
}; self.canvas,
position,
if let Some(texture) = texture { Direction::Right,
let position = Map::cell_to_pixel((x, y)); Some(0),
let dst_rect = sdl2::rect::Rect::new(position.0, position.1, 24, 24); );
self.canvas }
.copy(texture, None, Some(dst_rect)) MapTile::PowerPellet => {
.expect("Could not render pellet"); let position = Map::cell_to_pixel((x, y));
self.power_pellet_texture.render(
self.canvas,
position,
Direction::Right,
Some(0),
);
}
_ => {}
} }
} }
} }

View File

@@ -2,7 +2,7 @@ use pathfinding::prelude::dijkstra;
use rand::Rng; use rand::Rng;
use crate::{ use crate::{
animation::AnimatedTexture, animation::{AnimatedAtlasTexture, FrameDrawn},
constants::{MapTile, BOARD_WIDTH}, constants::{MapTile, BOARD_WIDTH},
direction::Direction, direction::Direction,
entity::{Entity, MovableEntity, Renderable}, entity::{Entity, MovableEntity, Renderable},
@@ -58,9 +58,9 @@ pub struct Ghost<'a> {
/// Reference to Pac-Man for targeting /// Reference to Pac-Man for targeting
pub pacman: std::rc::Rc<std::cell::RefCell<Pacman<'a>>>, pub pacman: std::rc::Rc<std::cell::RefCell<Pacman<'a>>>,
/// Ghost body sprite /// Ghost body sprite
body_sprite: AnimatedTexture<'a>, body_sprite: AnimatedAtlasTexture<'a>,
/// Ghost eyes sprite /// Ghost eyes sprite
eyes_sprite: AnimatedTexture<'a>, eyes_sprite: AnimatedAtlasTexture<'a>,
} }
impl Ghost<'_> { impl Ghost<'_> {
@@ -74,7 +74,7 @@ impl Ghost<'_> {
pacman: std::rc::Rc<std::cell::RefCell<Pacman<'a>>>, pacman: std::rc::Rc<std::cell::RefCell<Pacman<'a>>>,
) -> Ghost<'a> { ) -> Ghost<'a> {
let color = ghost_type.color(); 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); body_sprite.set_color_modulation(color.r, color.g, color.b);
let pixel_position = Map::cell_to_pixel(starting_position); let pixel_position = Map::cell_to_pixel(starting_position);
Ghost { Ghost {
@@ -90,7 +90,7 @@ impl Ghost<'_> {
ghost_type, ghost_type,
pacman, pacman,
body_sprite, 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 self.body_sprite
.set_color_modulation(color.r, color.g, color.b); .set_color_modulation(color.r, color.g, color.b);
self.body_sprite 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 // Always render eyes on top
@@ -314,7 +314,7 @@ impl Renderable for Ghost<'_> {
} }
}; };
self.eyes_sprite.render_static( self.eyes_sprite.render(
canvas, canvas,
self.base.pixel_position, self.base.pixel_position,
Direction::Right, Direction::Right,

View File

@@ -9,7 +9,7 @@ use sdl2::{
use tracing::event; use tracing::event;
use crate::{ use crate::{
animation::AnimatedTexture, animation::{AnimatedAtlasTexture, FrameDrawn},
direction::Direction, direction::Direction,
entity::{Entity, MovableEntity, Renderable}, entity::{Entity, MovableEntity, Renderable},
map::Map, map::Map,
@@ -24,7 +24,7 @@ pub struct Pacman<'a> {
pub next_direction: Option<Direction>, pub next_direction: Option<Direction>,
/// Whether Pac-Man is currently stopped. /// Whether Pac-Man is currently stopped.
pub stopped: bool, pub stopped: bool,
sprite: AnimatedTexture<'a>, sprite: AnimatedAtlasTexture<'a>,
} }
impl Pacman<'_> { impl Pacman<'_> {
@@ -46,7 +46,7 @@ impl Pacman<'_> {
), ),
next_direction: None, next_direction: None,
stopped: false, 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<'_> { impl Renderable for Pacman<'_> {
fn render(&mut self, canvas: &mut Canvas<Window>) { fn render(&mut self, canvas: &mut Canvas<Window>) {
if self.stopped { if self.stopped {
self.sprite.render_static( self.sprite.render(
canvas, canvas,
self.base.pixel_position, self.base.pixel_position,
self.base.direction, self.base.direction,
@@ -115,7 +115,7 @@ impl Renderable for Pacman<'_> {
); );
} else { } else {
self.sprite self.sprite
.render(canvas, self.base.pixel_position, self.base.direction); .render(canvas, self.base.pixel_position, self.base.direction, None);
} }
} }
} }