Compare commits

..

7 Commits

8 changed files with 216 additions and 96 deletions

View File

@@ -38,11 +38,13 @@ impl<'a> AnimatedTexture<'a> {
} }
} }
// Get the current frame number
fn current_frame(&self) -> u32 { fn current_frame(&self) -> u32 {
self.ticker / self.ticks_per_frame self.ticker / self.ticks_per_frame
} }
fn next_frame(&mut self) { // Move to the next frame. If we are at the end of the animation, reverse the direction
pub fn tick(&mut self) {
if self.reversed { if self.reversed {
self.ticker -= 1; self.ticker -= 1;
@@ -58,9 +60,14 @@ impl<'a> AnimatedTexture<'a> {
} }
} }
fn get_frame_rect(&self) -> Rect { // Calculate the frame rect (portion of the texture to render) for the given frame.
fn get_frame_rect(&self, frame: u32) -> Rect {
if frame >= self.frame_count {
panic!("Frame {} is out of bounds for this texture", frame);
}
Rect::new( Rect::new(
self.current_frame() as i32 * self.frame_width as i32, frame as i32 * self.frame_width as i32,
0, 0,
self.frame_width, self.frame_width,
self.frame_height, self.frame_height,
@@ -73,7 +80,35 @@ impl<'a> AnimatedTexture<'a> {
position: (i32, i32), position: (i32, i32),
direction: Direction, direction: Direction,
) { ) {
let frame_rect = self.get_frame_rect(); self.render_static(canvas, position, direction, Some(self.current_frame()));
self.tick();
}
// Functions like render, but only ticks the animation until the given frame is reached.
pub fn render_until(
&mut self,
canvas: &mut Canvas<Window>,
position: (i32, i32),
direction: Direction,
frame: u32,
) {
let current = self.current_frame();
self.render_static(canvas, position, direction, Some(current));
if frame != current {
self.tick();
}
}
// Renders a specific frame of the animation. Defaults to the current frame.
pub fn render_static(
&mut self,
canvas: &mut Canvas<Window>,
position: (i32, i32),
direction: Direction,
frame: Option<u32>,
) {
let frame_rect = self.get_frame_rect(frame.unwrap_or(self.current_frame()));
let position_rect = Rect::new( let position_rect = Rect::new(
position.0 + self.offset.0, position.0 + self.offset.0,
position.1 + self.offset.1, position.1 + self.offset.1,
@@ -92,7 +127,5 @@ impl<'a> AnimatedTexture<'a> {
false, false,
) )
.expect("Could not render texture on canvas"); .expect("Could not render texture on canvas");
self.next_frame();
} }
} }

View File

@@ -1,11 +1,11 @@
use lazy_static::lazy_static;
pub const BOARD_WIDTH: u32 = 28; pub const BOARD_WIDTH: u32 = 28;
pub const BOARD_HEIGHT: u32 = 37; // Adjusted to fit map texture? pub const BOARD_HEIGHT: u32 = 31; // Adjusted to fit map texture?
pub const CELL_SIZE: u32 = 24; pub const CELL_SIZE: u32 = 24;
pub const BOARD_OFFSET: (u32, u32) = (0, 3); // Relative cell offset for where map text / grid starts
pub const WINDOW_WIDTH: u32 = CELL_SIZE * BOARD_WIDTH; pub const WINDOW_WIDTH: u32 = CELL_SIZE * BOARD_WIDTH;
pub const WINDOW_HEIGHT: u32 = CELL_SIZE * BOARD_HEIGHT; pub const WINDOW_HEIGHT: u32 = CELL_SIZE * (BOARD_HEIGHT + 6); // Map texture is 6 cells taller (3 above, 3 below) than the grid
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub enum MapTile { pub enum MapTile {
@@ -17,9 +17,6 @@ pub enum MapTile {
} }
pub const RAW_BOARD: [&str; BOARD_HEIGHT as usize] = [ pub const RAW_BOARD: [&str; BOARD_HEIGHT as usize] = [
" ",
" ",
" ",
"############################", "############################",
"#............##............#", "#............##............#",
"#.####.#####.##.#####.####.#", "#.####.#####.##.#####.####.#",
@@ -51,45 +48,4 @@ pub const RAW_BOARD: [&str; BOARD_HEIGHT as usize] = [
"#.##########.##.##########.#", "#.##########.##.##########.#",
"#..........................#", "#..........................#",
"############################", "############################",
" ",
" ",
" ",
]; ];
lazy_static! {
pub static ref BOARD: [[MapTile; BOARD_HEIGHT as usize]; BOARD_HEIGHT as usize] = {
let mut board = [[MapTile::Empty; BOARD_HEIGHT as usize]; BOARD_HEIGHT as usize];
for y in 0..BOARD_HEIGHT as usize {
let line = RAW_BOARD[y];
for x in 0..BOARD_WIDTH as usize {
if x >= line.len() {
break;
}
let i = (y * (BOARD_WIDTH as usize) + x) as usize;
let character = line
.chars()
.nth(x as usize)
.unwrap_or_else(|| panic!("Could not get character at {} = ({}, {})", i, x, y));
let tile = match character {
'#' => MapTile::Wall,
'.' => MapTile::Pellet,
'o' => MapTile::PowerPellet,
' ' => MapTile::Empty,
c @ '0' | c @ '1' | c @ '2' | c @ '3' | c @ '4' => {
MapTile::StartingPosition(c.to_digit(10).unwrap() as u8)
},
'=' => MapTile::Empty,
_ => panic!("Unknown character in board: {}", character),
};
board[x as usize][y as usize] = tile;
}
}
board
};
}

View File

@@ -1,3 +1,5 @@
use sdl2::keyboard::Keycode;
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub enum Direction { pub enum Direction {
Up, Up,
@@ -24,4 +26,18 @@ impl Direction {
Direction::Up => (0, -1), Direction::Up => (0, -1),
} }
} }
pub fn from_keycode(keycode: Keycode) -> Option<Direction> {
match keycode {
Keycode::D => Some(Direction::Right),
Keycode::Right => Some(Direction::Right),
Keycode::A => Some(Direction::Left),
Keycode::Left => Some(Direction::Left),
Keycode::W => Some(Direction::Up),
Keycode::Up => Some(Direction::Up),
Keycode::S => Some(Direction::Down),
Keycode::Down => Some(Direction::Down),
_ => None,
}
}
} }

View File

@@ -1,18 +1,23 @@
use std::rc::Rc;
use sdl2::image::LoadTexture; use sdl2::image::LoadTexture;
use sdl2::keyboard::Keycode; use sdl2::keyboard::Keycode;
use sdl2::render::{Texture, TextureCreator}; use sdl2::render::{Texture, TextureCreator};
use sdl2::video::WindowContext; use sdl2::video::WindowContext;
use sdl2::{pixels::Color, render::Canvas, video::Window}; use sdl2::{pixels::Color, render::Canvas, video::Window};
use tracing::event;
use crate::constants::{MapTile, BOARD, BOARD_HEIGHT, BOARD_WIDTH}; use crate::constants::{MapTile, BOARD_HEIGHT, BOARD_WIDTH, RAW_BOARD};
use crate::direction::Direction; use crate::direction::Direction;
use crate::entity::Entity; use crate::entity::Entity;
use crate::map::Map;
use crate::pacman::Pacman; use crate::pacman::Pacman;
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>,
pacman: Pacman<'a>, pacman: Pacman<'a>,
map: Rc<Map>,
debug: bool, debug: bool,
} }
@@ -21,15 +26,17 @@ impl Game<'_> {
canvas: &'a mut Canvas<Window>, canvas: &'a mut Canvas<Window>,
texture_creator: &'a TextureCreator<WindowContext>, texture_creator: &'a TextureCreator<WindowContext>,
) -> Game<'a> { ) -> Game<'a> {
let map = Rc::new(Map::new(RAW_BOARD));
let pacman_atlas = texture_creator let pacman_atlas = texture_creator
.load_texture("assets/32/pacman.png") .load_texture("assets/32/pacman.png")
.expect("Could not load pacman texture"); .expect("Could not load pacman texture");
let pacman = Pacman::new(Some(Game::cell_to_pixel((1, 4))), pacman_atlas); let pacman = Pacman::new((1, 1), pacman_atlas, Rc::clone(&map));
Game { Game {
canvas, canvas,
pacman: pacman, pacman: pacman,
debug: false, debug: false,
map: map,
map_texture: texture_creator map_texture: texture_creator
.load_texture("assets/map.png") .load_texture("assets/map.png")
.expect("Could not load pacman texture"), .expect("Could not load pacman texture"),
@@ -81,7 +88,7 @@ impl Game<'_> {
if self.debug { if self.debug {
for x in 0..BOARD_WIDTH { for x in 0..BOARD_WIDTH {
for y in 0..BOARD_HEIGHT { for y in 0..BOARD_HEIGHT {
let tile = BOARD[x as usize][y as usize]; let tile = self.map.get_tile((x as i32, y as i32)).unwrap_or(MapTile::Empty);
let mut color = None; let mut color = None;
if (x, y) == self.pacman.cell_position() { if (x, y) == self.pacman.cell_position() {
@@ -101,17 +108,22 @@ impl Game<'_> {
} }
} }
} }
// Draw the next cell
let next_cell = self.pacman.next_cell(None);
self.draw_cell((next_cell.0 as u32, next_cell.1 as u32), Color::YELLOW);
} }
self.canvas.present(); self.canvas.present();
} }
fn draw_cell(&mut self, cell: (u32, u32), color: Color) { fn draw_cell(&mut self, cell: (u32, u32), color: Color) {
let position = Map::cell_to_pixel(cell);
self.canvas.set_draw_color(color); self.canvas.set_draw_color(color);
self.canvas self.canvas
.draw_rect(sdl2::rect::Rect::new( .draw_rect(sdl2::rect::Rect::new(
cell.0 as i32 * 24, position.0 as i32,
cell.1 as i32 * 24, position.1 as i32,
24, 24,
24, 24,
)) ))

View File

@@ -5,6 +5,8 @@ use sdl2::event::{Event};
use sdl2::keyboard::Keycode; use sdl2::keyboard::Keycode;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use spin_sleep::sleep; use spin_sleep::sleep;
use tracing_error::ErrorLayer;
use tracing_subscriber::layer::SubscriberExt;
#[cfg(target_os = "emscripten")] #[cfg(target_os = "emscripten")]
pub mod emscripten; pub mod emscripten;
@@ -16,6 +18,7 @@ mod entity;
mod game; mod game;
mod pacman; mod pacman;
mod modulation; mod modulation;
mod map;
#[cfg(target_os = "emscripten")] #[cfg(target_os = "emscripten")]
mod emscripten; mod emscripten;
@@ -25,18 +28,12 @@ pub fn main() {
let video_subsystem = sdl_context.video().unwrap(); let video_subsystem = sdl_context.video().unwrap();
// Setup tracing // Setup tracing
#[cfg(debug_assertions)]
{
use tracing_error::ErrorLayer;
use tracing_subscriber::layer::SubscriberExt;
let subscriber = tracing_subscriber::fmt() let subscriber = tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG) .with_max_level(tracing::Level::DEBUG)
.finish() .finish()
.with(ErrorLayer::default()); .with(ErrorLayer::default());
tracing::subscriber::set_global_default(subscriber).expect("Could not set global default"); tracing::subscriber::set_global_default(subscriber).expect("Could not set global default");
}
let window = video_subsystem let window = video_subsystem
.window("Pac-Man", WINDOW_WIDTH, WINDOW_HEIGHT) .window("Pac-Man", WINDOW_WIDTH, WINDOW_HEIGHT)
@@ -83,7 +80,10 @@ pub fn main() {
| Event::KeyDown { | Event::KeyDown {
keycode: Some(Keycode::Escape) | Some(Keycode::Q), keycode: Some(Keycode::Escape) | Some(Keycode::Q),
.. ..
} => return false, } => {
event!(tracing::Level::INFO, "Exit requested. Exiting...");
return false
},
Event::KeyDown { keycode, .. } => { Event::KeyDown { keycode, .. } => {
game.keyboard_event(keycode.unwrap()); game.keyboard_event(keycode.unwrap());
} }
@@ -108,8 +108,8 @@ pub fn main() {
tick_no += 1; tick_no += 1;
if tick_no % (60 * 5) == 0 { if tick_no % (60 * 60) == 0 || tick_no == (60 * 2) {
let average_fps = tick_no as f32 / last_averaging_time.elapsed().as_secs_f32(); let average_fps = (tick_no % (60 * 60)) as f32 / last_averaging_time.elapsed().as_secs_f32();
let average_sleep = sleep_time / tick_no; let average_sleep = sleep_time / tick_no;
let average_process = loop_time - average_sleep; let average_process = loop_time - average_sleep;
@@ -123,7 +123,6 @@ pub fn main() {
sleep_time = Duration::ZERO; sleep_time = Duration::ZERO;
last_averaging_time = Instant::now(); last_averaging_time = Instant::now();
tick_no = 0;
} }
true true

61
src/map.rs Normal file
View File

@@ -0,0 +1,61 @@
use crate::constants::MapTile;
use crate::constants::{BOARD_HEIGHT, BOARD_WIDTH};
pub struct Map {
inner: [[MapTile; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize]
}
impl Map {
pub fn new(raw_board: [&str; BOARD_HEIGHT as usize]) -> Map {
let mut inner = [[MapTile::Empty; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize];
for y in 0..BOARD_HEIGHT as usize {
let line = raw_board[y];
for x in 0..BOARD_WIDTH as usize {
if x >= line.len() {
break;
}
let i = (y * (BOARD_WIDTH as usize) + x) as usize;
let character = line
.chars()
.nth(x as usize)
.unwrap_or_else(|| panic!("Could not get character at {} = ({}, {})", i, x, y));
let tile = match character {
'#' => MapTile::Wall,
'.' => MapTile::Pellet,
'o' => MapTile::PowerPellet,
' ' => MapTile::Empty,
c @ '0' | c @ '1' | c @ '2' | c @ '3' | c @ '4' => {
MapTile::StartingPosition(c.to_digit(10).unwrap() as u8)
},
'=' => MapTile::Empty,
_ => panic!("Unknown character in board: {}", character),
};
inner[x as usize][y as usize] = tile;
}
}
Map {
inner: inner
}
}
pub fn get_tile(&self, cell: (i32, i32)) -> Option<MapTile> {
let x = cell.0 as usize;
let y = cell.1 as usize;
if x >= BOARD_WIDTH as usize || y >= BOARD_HEIGHT as usize {
return None;
}
Some(self.inner[x][y])
}
pub fn cell_to_pixel(cell: (u32, u32)) -> (i32, i32) {
((cell.0 as i32) * 24, ((cell.1 + 3) as i32) * 24)
}
}

View File

@@ -1,22 +1,44 @@
pub struct SpeedModulator { /// A tick modulator allows you to slow down operations by a percentage.
///
/// Unfortunately, switching to floating point numbers for entities can induce floating point errors, slow down calculations
/// and make the game less deterministic. This is why we use a speed modulator instead.
/// Additionally, with small integers, lowering the speed by a percentage is not possible. For example, if we have a speed of 2,
/// and we want to slow it down by 10%, we would need to slow it down by 0.2. However, since we are using integers, we can't.
/// The only amount you can slow it down by is 1, which is 50% of the speed.
///
/// The basic principle of the Speed Modulator is to instead 'skip' movement ticks every now and then.
/// At 60 ticks per second, skips could happen several times per second, or once every few seconds.
/// Whatever it be, as long as the tick rate is high enough, the human eye will not be able to tell the difference.
///
/// For example, if we want to slow down the speed by 10%, we would need to skip every 10th tick.
pub trait TickModulator {
fn new(percent: f32) -> Self;
fn next(&mut self) -> bool;
}
pub struct SimpleTickModulator {
tick_count: u32, tick_count: u32,
ticks_left: u32, ticks_left: u32,
} }
impl SpeedModulator { // TODO: Add tests
pub fn new(percent: f32) -> Self { // TODO: Look into average precision, binary code modulation strategy
impl TickModulator for SimpleTickModulator {
fn new(percent: f32) -> Self {
let ticks_required: u32 = (1f32 / (1f32 - percent)).round() as u32; let ticks_required: u32 = (1f32 / (1f32 - percent)).round() as u32;
SpeedModulator { SimpleTickModulator {
tick_count: ticks_required, tick_count: ticks_required,
ticks_left: ticks_required, ticks_left: ticks_required,
} }
} }
pub fn next(&mut self) -> bool { fn next(&mut self) -> bool {
self.ticks_left -= 1; self.ticks_left -= 1;
// Return whether or not we should skip this tick
if self.ticks_left == 0 { if self.ticks_left == 0 {
// We've reached the tick to skip, reset the counter
self.ticks_left = self.tick_count; self.ticks_left = self.tick_count;
false false
} else { } else {

View File

@@ -1,12 +1,19 @@
use std::rc::Rc;
use sdl2::{ use sdl2::{
render::{Canvas, Texture}, render::{Canvas, Texture},
video::Window, video::Window,
}; };
use tracing::event;
use crate::{ use crate::{
constants::{BOARD, MapTile}, animation::AnimatedTexture,
animation::AnimatedTexture, constants::CELL_SIZE, direction::Direction, entity::Entity, constants::MapTile,
modulation::SpeedModulator, constants::{CELL_SIZE, BOARD_OFFSET},
direction::Direction,
entity::Entity,
map::Map,
modulation::{SimpleTickModulator, TickModulator},
}; };
pub struct Pacman<'a> { pub struct Pacman<'a> {
@@ -15,30 +22,38 @@ pub struct Pacman<'a> {
pub direction: Direction, pub direction: Direction,
pub next_direction: Option<Direction>, pub next_direction: Option<Direction>,
pub stopped: bool, pub stopped: bool,
map: Rc<Map>,
speed: u32, speed: u32,
modulation: SpeedModulator, modulation: SimpleTickModulator,
sprite: AnimatedTexture<'a>, sprite: AnimatedTexture<'a>,
} }
impl Pacman<'_> { impl Pacman<'_> {
pub fn new<'a>(starting_position: Option<(i32, i32)>, atlas: Texture<'a>) -> Pacman<'a> { pub fn new<'a>(starting_position: (u32, u32), atlas: Texture<'a>, map: Rc<Map>) -> Pacman<'a> {
Pacman { Pacman {
position: starting_position.unwrap_or((0i32, 0i32)), position: Map::cell_to_pixel(starting_position),
direction: Direction::Right, direction: Direction::Right,
next_direction: None, next_direction: None,
speed: 2, speed: 2,
map,
stopped: false, stopped: false,
modulation: SpeedModulator::new(0.9333), modulation: SimpleTickModulator::new(0.9333),
sprite: AnimatedTexture::new(atlas, 4, 3, 32, 32, Some((-4, -4))), sprite: AnimatedTexture::new(atlas, 4, 3, 32, 32, Some((-4, -4))),
} }
} }
pub fn render(&mut self, canvas: &mut Canvas<Window>) { pub fn render(&mut self, canvas: &mut Canvas<Window>) {
// When stopped, render the last frame of the animation
if self.stopped {
self.sprite
.render_until(canvas, self.position, self.direction, 2);
} else {
self.sprite.render(canvas, self.position, self.direction); self.sprite.render(canvas, self.position, self.direction);
} }
}
fn next_cell(&self) -> (i32, i32) { pub fn next_cell(&self, direction: Option<Direction>) -> (i32, i32) {
let (x, y) = self.direction.offset(); let (x, y) = direction.unwrap_or(self.direction).offset();
let cell = self.cell_position(); let cell = self.cell_position();
(cell.0 as i32 + x, cell.1 as i32 + y) (cell.0 as i32 + x, cell.1 as i32 + y)
} }
@@ -56,8 +71,8 @@ impl Entity for Pacman<'_> {
} }
fn cell_position(&self) -> (u32, u32) { fn cell_position(&self) -> (u32, u32) {
let (x, y) = self.position(); let (x, y) = self.position;
(x as u32 / CELL_SIZE, y as u32 / CELL_SIZE) ((x as u32 / CELL_SIZE) - BOARD_OFFSET.0, (y as u32 / CELL_SIZE) - BOARD_OFFSET.1)
} }
fn internal_position(&self) -> (u32, u32) { fn internal_position(&self) -> (u32, u32) {
@@ -72,6 +87,17 @@ impl Entity for Pacman<'_> {
self.direction = direction; self.direction = direction;
self.next_direction = None; self.next_direction = None;
} }
let next = self.next_cell(None);
let next_tile = self.map.get_tile(next).unwrap_or(MapTile::Empty);
if !self.stopped && next_tile == MapTile::Wall {
event!(tracing::Level::DEBUG, "Wall collision. Stopping.");
self.stopped = true;
} else if self.stopped && next_tile != MapTile::Wall {
event!(tracing::Level::DEBUG, "Wall collision resolved. Moving.");
self.stopped = false;
}
} }
if !self.stopped && self.modulation.next() { if !self.stopped && self.modulation.next() {
@@ -91,10 +117,5 @@ impl Entity for Pacman<'_> {
} }
} }
} }
let next = self.next_cell();
if BOARD[next.1 as usize][next.0 as usize] == MapTile::Wall {
self.stopped = true;
}
} }
} }