Compare commits

...

16 Commits

Author SHA1 Message Date
786fbb5002 feat: change starting position of PacMan, draw current PacMan on grid 2025-06-17 11:51:26 -05:00
422535c00d feat: direction propagation, change direction at precise times 2025-06-17 11:51:21 -05:00
0120abe806 feat: add optional offset to AnimatedTexture 2025-06-17 11:51:17 -05:00
e61930c08a reformat: default debug off, conditional debug grid rendering, remove unused redraw() 2025-06-17 11:51:02 -05:00
f7ff9f5290 chore: delete TextureManager 2025-06-17 11:50:55 -05:00
de29dc6711 chore: remove unused tick timing 2025-06-17 11:50:49 -05:00
c90f221c73 feat: speed property to PacMan 2025-06-17 11:50:44 -05:00
841943e121 fix: frame flashing in sprite tick 2025-06-17 11:50:37 -05:00
83d665123c feat: smooth back-forth sprite frame ticks, sprite rotation 2025-06-17 11:50:32 -05:00
ffc21c8622 reformat: strict tick timing with lag prints 2025-06-17 11:50:26 -05:00
b46a51bc76 reformat: drop TextureManager based sprite rendering, directly hold Textures 2025-06-17 11:50:18 -05:00
443afb1223 feat: new PacMan entity, entity trait implementation 2025-06-17 11:50:04 -05:00
724878dc17 feat: atlas texture, animated sprite management 2025-06-17 11:49:47 -05:00
274404b9ea feat: entity trait, direction enum (util) 2025-06-17 11:49:43 -05:00
2214a5541f feat: game loop, texture ownership, tick & draw with grid rendering 2025-06-17 11:49:40 -05:00
64de5fb732 feat: texture manager, texture loading 2025-06-17 11:49:33 -05:00
6 changed files with 338 additions and 39 deletions

98
src/animation.rs Normal file
View File

@@ -0,0 +1,98 @@
use sdl2::{
rect::Rect,
render::{Canvas, Texture},
video::Window,
};
use crate::direction::Direction;
pub struct AnimatedTexture<'a> {
raw_texture: Texture<'a>,
ticker: u32,
reversed: bool,
offset: (i32, i32),
ticks_per_frame: u32,
frame_count: u32,
frame_width: u32,
frame_height: u32,
}
impl<'a> AnimatedTexture<'a> {
pub fn new(
texture: Texture<'a>,
ticks_per_frame: u32,
frame_count: u32,
frame_width: u32,
frame_height: u32,
offset: Option<(i32, i32)>,
) -> Self {
AnimatedTexture {
raw_texture: texture,
ticker: 0,
reversed: false,
ticks_per_frame,
frame_count,
frame_width,
frame_height,
offset: offset.unwrap_or((0, 0)),
}
}
fn current_frame(&self) -> u32 {
self.ticker / self.ticks_per_frame
}
fn next_frame(&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;
}
}
}
fn get_frame_rect(&self) -> Rect {
Rect::new(
self.current_frame() as i32 * self.frame_width as i32,
0,
self.frame_width,
self.frame_height,
)
}
pub fn render(
&mut self,
canvas: &mut Canvas<Window>,
position: (i32, i32),
direction: Direction,
) {
let frame_rect = self.get_frame_rect();
let position_rect = Rect::new(
position.0 + self.offset.0,
position.1 + self.offset.1,
self.frame_width,
self.frame_height,
);
canvas
.copy_ex(
&self.raw_texture,
Some(frame_rect),
Some(position_rect),
direction.angle(),
None,
false,
false,
)
.expect("Could not render texture on canvas");
self.next_frame();
}
}

18
src/direction.rs Normal file
View File

@@ -0,0 +1,18 @@
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Direction {
Up,
Down,
Left,
Right,
}
impl Direction {
pub fn angle(&self) -> f64 {
match self {
Direction::Right => 0f64,
Direction::Down => 90f64,
Direction::Left => 180f64,
Direction::Up => 270f64,
}
}
}

11
src/entity.rs Normal file
View File

@@ -0,0 +1,11 @@
pub trait Entity {
// Returns true if the entity is colliding with the other entity
fn is_colliding(&self, other: &dyn Entity) -> bool;
// Returns the absolute position of the entity
fn position(&self) -> (i32, i32);
// Returns the cell position of the entity (XY position within the grid)
fn cell_position(&self) -> (u32, u32);
fn internal_position(&self) -> (u32, u32);
// Tick the entity (move it, perform collision checks, etc)
fn tick(&mut self);
}

View File

@@ -1,11 +1,120 @@
pub struct Game {}
use sdl2::image::LoadTexture;
use sdl2::keyboard::Keycode;
use sdl2::render::{Texture, TextureCreator};
use sdl2::video::WindowContext;
use sdl2::{pixels::Color, render::Canvas, video::Window};
impl Game {
pub fn new() -> Game {
Game {}
use crate::constants::{MapTile, BOARD, BOARD_HEIGHT, BOARD_WIDTH};
use crate::direction::Direction;
use crate::entity::Entity;
use crate::pacman::Pacman;
pub struct Game<'a> {
canvas: &'a mut Canvas<Window>,
map_texture: Texture<'a>,
pacman: Pacman<'a>,
debug: bool,
}
impl Game<'_> {
pub fn new<'a>(
canvas: &'a mut Canvas<Window>,
texture_creator: &'a TextureCreator<WindowContext>,
) -> Game<'a> {
let pacman_atlas = texture_creator
.load_texture("assets/32/pacman.png")
.expect("Could not load pacman texture");
let pacman = Pacman::new(Some(Game::cell_to_pixel((1, 4))), pacman_atlas);
Game {
canvas,
pacman: pacman,
debug: false,
map_texture: texture_creator
.load_texture("assets/map.png")
.expect("Could not load pacman texture"),
}
}
pub fn tick() {}
pub fn cell_to_pixel(cell: (u32, u32)) -> (i32, i32) {
((cell.0 as i32 * 24), ((cell.1) as i32 * 24))
}
pub fn draw() {}
}
pub fn keyboard_event(&mut self, keycode: Keycode) {
match keycode {
Keycode::D => {
self.pacman.next_direction = Some(Direction::Right);
}
Keycode::A => {
self.pacman.next_direction = Some(Direction::Left);
}
Keycode::W => {
self.pacman.next_direction = Some(Direction::Up);
}
Keycode::S => {
self.pacman.next_direction = Some(Direction::Down);
}
Keycode::Space => {
self.debug = !self.debug;
}
_ => {}
}
}
pub fn tick(&mut self) {
self.pacman.tick();
}
pub fn draw(&mut self) {
// Clear the screen (black)
self.canvas.set_draw_color(Color::RGB(0, 0, 0));
self.canvas.clear();
self.canvas
.copy(&self.map_texture, None, None)
.expect("Could not render texture on canvas");
// Render the pacman
self.pacman.render(self.canvas);
// Draw a grid
if self.debug {
for x in 0..BOARD_WIDTH {
for y in 0..BOARD_HEIGHT {
let tile = BOARD[x as usize][y as usize];
let mut color = None;
if (x, y) == self.pacman.cell_position() {
self.draw_cell((x, y), Color::CYAN);
} else {
color = match tile {
MapTile::Empty => None,
MapTile::Wall => Some(Color::BLUE),
MapTile::Pellet => Some(Color::RED),
MapTile::PowerPellet => Some(Color::MAGENTA),
MapTile::StartingPosition(_) => Some(Color::GREEN),
};
}
if let Some(color) = color {
self.draw_cell((x, y), color);
}
}
}
}
self.canvas.present();
}
fn draw_cell(&mut self, cell: (u32, u32), color: Color) {
self.canvas.set_draw_color(color);
self.canvas
.draw_rect(sdl2::rect::Rect::new(
cell.0 as i32 * 24,
cell.1 as i32 * 24,
24,
24,
))
.expect("Could not draw rectangle");
}
}

View File

@@ -1,6 +1,5 @@
use crate::constants::{WINDOW_HEIGHT, WINDOW_WIDTH};
use crate::game::Game;
use crate::textures::TextureManager;
use sdl2::event::{Event};
use sdl2::keyboard::Keycode;
use sdl2::pixels::Color;
@@ -14,18 +13,9 @@ mod constants;
mod direction;
mod game;
mod pacman;
mod textures;
mod entity;
mod animation;
fn redraw(canvas: &mut Canvas<sdl2::video::Window>, tex: &Texture, i: u8) {
canvas.set_draw_color(Color::RGB(i, i, i));
canvas.clear();
canvas
.copy(tex, None, None)
.expect("Could not render texture on canvas");
}
pub fn main() {
let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
@@ -38,6 +28,7 @@ pub fn main() {
let mut canvas = window
.into_canvas()
.accelerated()
.build()
.expect("Could not build canvas");
@@ -46,7 +37,7 @@ pub fn main() {
.expect("Could not set logical size");
let texture_creator = canvas.texture_creator();
let mut game = Game::new(&mut canvas, TextureManager::new(&texture_creator));
let mut game = Game::new(&mut canvas, &texture_creator);
let mut event_pump = sdl_context
.event_pump()
@@ -55,7 +46,11 @@ pub fn main() {
game.draw();
game.tick();
let loop_time = Duration::from_millis(1000 / 60);
let mut main_loop = || {
let start = Instant::now();
for event in event_pump.poll_iter() {
match event {
// Handle quitting keys or window close
@@ -64,34 +59,22 @@ pub fn main() {
keycode: Some(Keycode::Escape) | Some(Keycode::Q),
..
} => return false,
event @ Event::KeyDown { .. } => {
println!("{:?}", event);
}
Event::KeyDown { keycode , .. } => {
game.keyboard_event(keycode.unwrap());
},
_ => {}
}
}
let tick_time = {
let start = Instant::now();
game.tick();
start.elapsed()
};
game.tick();
game.draw();
let draw_time = {
let start = Instant::now();
game.draw();
start.elapsed()
};
// Alert if tick time exceeds 10ms
if tick_time > Duration::from_millis(3) {
println!("Tick took: {:?}", tick_time);
}
if draw_time > Duration::from_millis(3) {
println!("Draw took: {:?}", draw_time);
if start.elapsed() < loop_time {
::std::thread::sleep(loop_time - start.elapsed());
} else {
println!("Game loop behind schedule by: {:?}", start.elapsed() - loop_time);
}
::std::thread::sleep(Duration::from_millis(10));
true
};

80
src/pacman.rs Normal file
View File

@@ -0,0 +1,80 @@
use sdl2::{
render::{Canvas, Texture},
video::Window,
};
use crate::{animation::AnimatedTexture, direction::Direction, entity::Entity, constants::CELL_SIZE};
pub struct Pacman<'a> {
// Absolute position on the board (precise)
pub position: (i32, i32),
pub direction: Direction,
pub next_direction: Option<Direction>,
speed: u32,
sprite: AnimatedTexture<'a>,
}
impl Pacman<'_> {
pub fn new<'a>(starting_position: Option<(i32, i32)>, atlas: Texture<'a>) -> Pacman<'a> {
Pacman {
position: starting_position.unwrap_or((0i32, 0i32)),
direction: Direction::Right,
next_direction: None,
speed: 2,
sprite: AnimatedTexture::new(atlas, 4, 3, 32, 32, Some((-4, -4))),
}
}
pub fn render(&mut self, canvas: &mut Canvas<Window>) {
self.sprite.render(canvas, self.position, self.direction);
}
}
impl Entity for Pacman<'_> {
fn is_colliding(&self, other: &dyn Entity) -> bool {
let (x, y) = self.position();
let (other_x, other_y) = other.position();
x == other_x && y == other_y
}
fn position(&self) -> (i32, i32) {
self.position
}
fn cell_position(&self) -> (u32, u32) {
let (x, y) = self.position();
(x as u32 / CELL_SIZE, y as u32 / CELL_SIZE)
}
fn internal_position(&self) -> (u32, u32) {
let (x, y) = self.position();
(x as u32 % CELL_SIZE, y as u32 % CELL_SIZE)
}
fn tick(&mut self) {
let can_change = self.internal_position() == (0, 0);
println!("{:?} ({:?})", self.internal_position(), can_change);
if can_change {
if let Some(direction) = self.next_direction {
self.direction = direction;
self.next_direction = None;
}
}
let speed = self.speed as i32;
match self.direction {
Direction::Right => {
self.position.0 += speed;
}
Direction::Left => {
self.position.0 -= speed;
}
Direction::Up => {
self.position.1 -= speed;
}
Direction::Down => {
self.position.1 += speed;
}
}
}
}