Compare commits

..

19 Commits

Author SHA1 Message Date
03b2c5a659 ci: cleanup build script 2025-06-17 11:54:13 -05:00
64e226be70 ci: properly create deps folder for pacman.data during build 2025-06-17 11:54:13 -05:00
f998ddd344 docs: add build details 2025-06-17 11:54:13 -05:00
b2ad8e7afe ci: add permisions for deployment job 2025-06-17 11:54:13 -05:00
799d5d85e8 ci: add action-based deploy with artifacts 2025-06-17 11:54:13 -05:00
9730d02da5 ci: fix build-wasm execution permissions 2025-06-17 11:54:13 -05:00
f634beffee fix(wasm): remove unnecessary emscripten looping 2025-06-17 11:54:13 -05:00
d15dbe3982 ci: prepare proper build script, move script into /scripts, move index.html into /assets 2025-06-17 11:54:13 -05:00
de5cddd9b6 fix(wasm): wasm32-unknown-emscripten compiler flags 2025-06-17 11:54:13 -05:00
e3f37ab48e ci: target proper version of Emscripten (1.39.20) 2025-06-17 11:54:13 -05:00
3dd8d5aff7 chore: increase stat reporting period to 60 seconds 2025-06-17 11:54:13 -05:00
ad084d1cd8 feat: add pausing functionality, clean up statistic calculations 2025-06-17 11:54:13 -05:00
852e54f1bf chore: rust-fmt entire project 2025-06-17 11:54:13 -05:00
a62ddab9af fix: prevent changing direction into walls 2025-06-17 11:54:13 -05:00
50d0bc7d5f refactor: simplify keyboard event direction change logic in Game 2025-06-17 11:54:13 -05:00
2c6045aa1b feat: add map struct, overhaul stored map representation 2025-06-17 11:54:13 -05:00
bf8370ef35 feat: sprite frame pinning, conditional on stopped PacMan 2025-06-17 11:54:13 -05:00
c71b6d69ab fix: always use tracing, provide timing info 2s in + every 60s first 2025-06-17 11:54:13 -05:00
a7e87c18a3 feat: pacman next cell debug func 2025-06-17 11:54:13 -05:00
17 changed files with 307 additions and 189 deletions

View File

@@ -1,4 +1,8 @@
[target.wasm32-unknown-emscripten] [target.wasm32-unknown-emscripten]
rustflags = [ rustflags = [
"--use-preload-plugins --preload-file assets -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s ASSERTIONS=1", "-O", "-C", "link-args=-O2 --profiling",
] #"-C", "link-args=-O3 --closure 1",
"-C", "link-args=-sASYNCIFY -sALLOW_MEMORY_GROWTH=1",
"-C", "link-args=-sUSE_SDL=2 -sUSE_SDL_IMAGE=2 -sSDL2_IMAGE_FORMATS=['png']",
"-C", "link-args=--preload-file assets/ -lidbfs.js",
]

View File

@@ -1,28 +1,38 @@
name: Github Pages name: Github Pages
on: [push] on: [push]
permissions: permissions:
contents: write contents: write
jobs: jobs:
build-github-pages: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
steps: steps:
- uses: actions/checkout@v2 # repo checkout - uses: actions/checkout@v2 # repo checkout
- uses: mymindstorm/setup-emsdk@v11 # setup emscripten toolchain - uses: mymindstorm/setup-emsdk@v11 # setup emscripten toolchain
# with: with:
# version: 3.1.35 version: 1.39.20
- uses: actions-rs/toolchain@v1 # get rust toolchain for wasm - uses: actions-rs/toolchain@v1 # get rust toolchain for wasm
with: with:
toolchain: stable toolchain: stable
target: wasm32-unknown-emscripten target: wasm32-unknown-emscripten
override: true override: true
# TODO: Update to v2
- name: Rust Cache # cache the rust build artefacts - name: Rust Cache # cache the rust build artefacts
uses: Swatinem/rust-cache@v1 uses: Swatinem/rust-cache@v1
- name: Build # build - name: Build # build
run: ./build.sh run: ./scripts/build-wasm.sh
- name: Deploy
uses: JamesIves/github-pages-deploy-action@v4 - name: Upload Artifact
uses: actions/upload-pages-artifact@v2
with: with:
folder: dist path: './dist/'
retention-days: 7
- name: Deploy
uses: actions/deploy-pages@v2

7
BUILD.md Normal file
View File

@@ -0,0 +1,7 @@
# Building Pac-Man
## GitHub Actions Workflow
1. Build workflow produces executables & WASM files for all platforms
2. Uploaded as artifacts
3. Deployment workflow downloads artifacts and uploads to GitHub Pages

21
assets/index.html Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
var Module = {
'locateFile': function (path, prefix) {
if (path.endsWith(".data")) {
return prefix + "deps/" + path;
}
return prefix + path;
},
'canvas': document.getElementById('canvas'),
};
</script>
<script src="pacman.js"></script>
</body>
</html>

View File

@@ -1,10 +0,0 @@
#!/bin/sh
set -eux
cargo build --target=wasm32-unknown-emscripten --release
mkdir -p dist
cp target/wasm32-unknown-emscripten/release/Pac_Man.wasm dist
cp target/wasm32-unknown-emscripten/release/Pac-Man.js dist
cp index.html dist

View File

@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="text/javascript">
let Module = {
canvas: (function () {
// this is how we provide a canvas to our sdl2
return document.getElementById("canvas");
})(),
preRun: [function () {
ENV.RUST_LOG = "info,wgpu=warn"
}]
};
</script>
<script src="Pac-Man.js"></script>
</body>
</html>

16
scripts/build-wasm.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
set -eux
echo "Building WASM with Emscripten"
cargo build --target=wasm32-unknown-emscripten --release
echo "Copying release files to dist/"
mkdir -p dist
mkdir -p dist/deps
output_folder="target/wasm32-unknown-emscripten/release"
cp $output_folder/pacman.wasm dist
cp $output_folder/pacman.js dist
cp $output_folder/deps/pacman.data dist/deps
cp assets/index.html dist

View File

@@ -83,7 +83,24 @@ impl<'a> AnimatedTexture<'a> {
self.render_static(canvas, position, direction, Some(self.current_frame())); self.render_static(canvas, position, direction, Some(self.current_frame()));
self.tick(); 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( pub fn render_static(
&mut self, &mut self,
canvas: &mut Canvas<Window>, canvas: &mut Canvas<Window>,

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

@@ -40,4 +40,4 @@ impl Direction {
_ => None, _ => None,
} }
} }
} }

View File

@@ -2,18 +2,20 @@
#[cfg(target_os = "emscripten")] #[cfg(target_os = "emscripten")]
pub mod emscripten { pub mod emscripten {
use std::cell::RefCell; use std::cell::RefCell;
use std::ptr::null_mut;
use std::os::raw::{c_int, c_void, c_char, c_float};
use std::ffi::{CStr, CString}; use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_float, c_int, c_void};
use std::ptr::null_mut;
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
type em_callback_func = unsafe extern "C" fn(); type em_callback_func = unsafe extern "C" fn();
extern "C" { extern "C" {
// void emscripten_set_main_loop(em_callback_func func, int fps, int simulate_infinite_loop) // void emscripten_set_main_loop(em_callback_func func, int fps, int simulate_infinite_loop)
pub fn emscripten_set_main_loop(func: em_callback_func, pub fn emscripten_set_main_loop(
fps: c_int, func: em_callback_func,
simulate_infinite_loop: c_int); fps: c_int,
simulate_infinite_loop: c_int,
);
pub fn emscripten_cancel_main_loop(); pub fn emscripten_cancel_main_loop();
pub fn emscripten_pause_main_loop(); pub fn emscripten_pause_main_loop();
@@ -23,17 +25,20 @@ pub mod emscripten {
thread_local!(static MAIN_LOOP_CALLBACK: RefCell<*mut c_void> = RefCell::new(null_mut())); thread_local!(static MAIN_LOOP_CALLBACK: RefCell<*mut c_void> = RefCell::new(null_mut()));
pub fn set_main_loop_callback<F>(callback: F) pub fn set_main_loop_callback<F>(callback: F)
where F: FnMut() where
F: FnMut(),
{ {
MAIN_LOOP_CALLBACK MAIN_LOOP_CALLBACK.with(|log| {
.with(|log| { *log.borrow_mut() = &callback as *const _ as *mut c_void; }); *log.borrow_mut() = &callback as *const _ as *mut c_void;
});
unsafe { unsafe {
emscripten_set_main_loop(wrapper::<F>, -1, 1); emscripten_set_main_loop(wrapper::<F>, -1, 1);
} }
unsafe extern "C" fn wrapper<F>() unsafe extern "C" fn wrapper<F>()
where F: FnMut() where
F: FnMut(),
{ {
MAIN_LOOP_CALLBACK.with(|z| { MAIN_LOOP_CALLBACK.with(|z| {
let closure = *z.borrow_mut() as *mut F; let closure = *z.borrow_mut() as *mut F;
@@ -41,4 +46,4 @@ pub mod emscripten {
}); });
} }
} }
} }

View File

@@ -6,6 +6,6 @@ pub trait Entity {
// Returns the cell position of the entity (XY position within the grid) // Returns the cell position of the entity (XY position within the grid)
fn cell_position(&self) -> (u32, u32); fn cell_position(&self) -> (u32, u32);
fn internal_position(&self) -> (u32, u32); fn internal_position(&self) -> (u32, u32);
// Tick the entity (move it, perform collision checks, etc) // Tick the entity (move it, perform collision checks, etc)
fn tick(&mut self); fn tick(&mut self);
} }

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,43 +26,31 @@ 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"),
} }
} }
pub fn cell_to_pixel(cell: (u32, u32)) -> (i32, i32) {
((cell.0 as i32 * 24), ((cell.1) as i32 * 24))
}
pub fn keyboard_event(&mut self, keycode: Keycode) { pub fn keyboard_event(&mut self, keycode: Keycode) {
match keycode { // Change direction
Keycode::D => { let direction = Direction::from_keycode(keycode);
self.pacman.next_direction = Some(Direction::Right); self.pacman.next_direction = direction;
}
Keycode::A => { // Toggle debug mode
self.pacman.next_direction = Some(Direction::Left); if keycode == Keycode::Space {
} self.debug = !self.debug;
Keycode::W => {
self.pacman.next_direction = Some(Direction::Up);
}
Keycode::S => {
self.pacman.next_direction = Some(Direction::Down);
}
Keycode::Space => {
self.debug = !self.debug;
}
_ => {}
} }
} }
@@ -81,7 +74,10 @@ 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 +97,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

@@ -1,21 +1,21 @@
use crate::constants::{WINDOW_HEIGHT, WINDOW_WIDTH}; use crate::constants::{WINDOW_HEIGHT, WINDOW_WIDTH};
use crate::game::Game; use crate::game::Game;
use tracing::{event}; use sdl2::event::Event;
use sdl2::event::{Event};
use sdl2::keyboard::Keycode; use sdl2::keyboard::Keycode;
use std::time::{Duration, Instant};
use spin_sleep::sleep; use spin_sleep::sleep;
use std::time::{Duration, Instant};
#[cfg(target_os = "emscripten")] use tracing::event;
pub mod emscripten; use tracing_error::ErrorLayer;
use tracing_subscriber::layer::SubscriberExt;
mod animation; mod animation;
mod constants; mod constants;
mod direction; mod direction;
mod entity; mod entity;
mod game; mod game;
mod pacman; mod map;
mod modulation; mod modulation;
mod pacman;
#[cfg(target_os = "emscripten")] #[cfg(target_os = "emscripten")]
mod emscripten; mod emscripten;
@@ -25,18 +25,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)] let subscriber = tracing_subscriber::fmt()
{ .with_max_level(tracing::Level::DEBUG)
use tracing_error::ErrorLayer; .finish()
use tracing_subscriber::layer::SubscriberExt; .with(ErrorLayer::default());
let subscriber = tracing_subscriber::fmt() tracing::subscriber::set_global_default(subscriber).expect("Could not set global default");
.with_max_level(tracing::Level::DEBUG)
.finish()
.with(ErrorLayer::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)
@@ -71,11 +65,17 @@ pub fn main() {
// The start of a period of time over which we average the frame time. // The start of a period of time over which we average the frame time.
let mut last_averaging_time = Instant::now(); let mut last_averaging_time = Instant::now();
let mut sleep_time = Duration::ZERO; let mut sleep_time = Duration::ZERO;
let mut paused = false;
event!(tracing::Level::INFO, "Starting game loop ({:.3}ms)", loop_time.as_secs_f32() * 1000.0); event!(
tracing::Level::INFO,
"Starting game loop ({:.3}ms)",
loop_time.as_secs_f32() * 1000.0
);
let mut main_loop = || { let mut main_loop = || {
let start = Instant::now(); let start = Instant::now();
// TODO: Fix key repeat delay issues by using VecDeque for instant key repeat
for event in event_pump.poll_iter() { for event in event_pump.poll_iter() {
match event { match event {
// Handle quitting keys or window close // Handle quitting keys or window close
@@ -83,7 +83,21 @@ 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: Some(Keycode::P),
..
} => {
paused = !paused;
event!(
tracing::Level::INFO,
"{}",
if paused { "Paused" } else { "Unpaused" }
);
}
Event::KeyDown { keycode, .. } => { Event::KeyDown { keycode, .. } => {
game.keyboard_event(keycode.unwrap()); game.keyboard_event(keycode.unwrap());
} }
@@ -91,8 +105,11 @@ pub fn main() {
} }
} }
game.tick(); // TODO: Proper pausing implementation that does not interfere with statistic gathering
game.draw(); if !paused {
game.tick();
game.draw();
}
if start.elapsed() < loop_time { if start.elapsed() < loop_time {
let time = loop_time - start.elapsed(); let time = loop_time - start.elapsed();
@@ -108,9 +125,11 @@ pub fn main() {
tick_no += 1; tick_no += 1;
if tick_no % (60 * 5) == 0 { const PERIOD: u32 = 60 * 60;
let average_fps = tick_no as f32 / last_averaging_time.elapsed().as_secs_f32(); let tick_mod = tick_no % PERIOD;
let average_sleep = sleep_time / tick_no; if tick_mod % PERIOD == 0 {
let average_fps = PERIOD as f32 / last_averaging_time.elapsed().as_secs_f32();
let average_sleep = sleep_time / PERIOD;
let average_process = loop_time - average_sleep; let average_process = loop_time - average_sleep;
event!( event!(
@@ -118,24 +137,16 @@ pub fn main() {
"Timing Averages [fps={}] [sleep={:?}] [process={:?}]", "Timing Averages [fps={}] [sleep={:?}] [process={:?}]",
average_fps, average_fps,
average_sleep, average_sleep,
average_process average_process
); );
sleep_time = Duration::ZERO; sleep_time = Duration::ZERO;
last_averaging_time = Instant::now(); last_averaging_time = Instant::now();
tick_no = 0;
} }
true true
}; };
#[cfg(target_os = "emscripten")]
use emscripten::emscripten;
#[cfg(target_os = "emscripten")]
emscripten::set_main_loop_callback(main_loop);
#[cfg(not(target_os = "emscripten"))]
loop { loop {
if !main_loop() { if !main_loop() {
break; break;

59
src/map.rs Normal file
View File

@@ -0,0 +1,59 @@
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,15 +1,15 @@
/// A tick modulator allows you to slow down operations by a percentage. /// 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 /// 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. /// 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, /// 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. /// 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 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. /// 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. /// 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. /// 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. /// For example, if we want to slow down the speed by 10%, we would need to skip every 10th tick.
pub trait TickModulator { pub trait TickModulator {
fn new(percent: f32) -> Self; fn new(percent: f32) -> Self;

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::{BOARD_OFFSET, CELL_SIZE},
direction::Direction,
entity::Entity,
map::Map,
modulation::{SimpleTickModulator, TickModulator},
}; };
pub struct Pacman<'a> { pub struct Pacman<'a> {
@@ -15,33 +22,61 @@ 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>) {
self.sprite.render(canvas, self.position, self.direction); // 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);
}
} }
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)
} }
fn handle_requested_direction(&mut self) {
if self.next_direction.is_none() {
return;
}
if self.next_direction.unwrap() == self.direction {
self.next_direction = None;
return;
}
let proposed_next_cell = self.next_cell(self.next_direction);
let proposed_next_tile = self
.map
.get_tile(proposed_next_cell)
.unwrap_or(MapTile::Empty);
if proposed_next_tile != MapTile::Wall {
self.direction = self.next_direction.unwrap();
self.next_direction = None;
}
}
} }
impl Entity for Pacman<'_> { impl Entity for Pacman<'_> {
@@ -56,8 +91,11 @@ 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) {
@@ -67,10 +105,19 @@ impl Entity for Pacman<'_> {
fn tick(&mut self) { fn tick(&mut self) {
let can_change = self.internal_position() == (0, 0); let can_change = self.internal_position() == (0, 0);
if can_change { if can_change {
if let Some(direction) = self.next_direction { self.handle_requested_direction();
self.direction = direction;
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;
} }
} }
@@ -91,10 +138,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;
}
} }
} }