Compare commits

..

28 Commits

Author SHA1 Message Date
40acffafd1 fix: rebuild, try removing zero ms sleeps 2025-06-17 11:54:13 -05:00
2187212b7c chore: increase speed, no modulation, increase animation speed 2025-06-17 11:54:13 -05:00
229d2242ef fix: minor comments, disable accelerated, use std sleep on web builds 2025-06-17 11:54:13 -05:00
00c4c76299 chore: add ogg/vorbis comment for emscripten 2025-06-17 11:54:13 -05:00
8b30a602bf fix: remove idbfs.js inclusion linker arg 2025-06-17 11:54:13 -05:00
83a5ccdb8e chore: delete emscripten.rs 2025-06-17 11:54:13 -05:00
44d8184d8b feat: downloading in Windows build process, cleaning script 2025-06-17 11:54:13 -05:00
0630fc56ec fix: key stealing, disable Emscripten module, disable colored ANSI for emscripten builds 2025-06-17 11:54:13 -05:00
98d8960c57 docs(story): begin documenting project story/history 2025-06-17 11:54:13 -05:00
394344c11f docs: experimental scoreboard concept 2025-06-17 11:54:13 -05:00
93ba470ce9 fix: progress on reproducible Windows builds, disable script tracing 2025-06-17 11:54:13 -05:00
129aed0ffb docs: add more TODOs 2025-06-17 11:54:13 -05:00
e062ada301 chore: remove old windows build/serve scripts 2025-06-17 11:54:13 -05:00
af57199915 fix: center canvas, make background black 2025-06-17 11:54:13 -05:00
538cf1efb5 chore: copy .data file directly, remove locateFile step 2025-06-17 11:54:13 -05:00
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
23 changed files with 303 additions and 156 deletions

View File

@@ -1,4 +1,10 @@
[target.wasm32-unknown-emscripten]
# TODO: Document what the fuck this is.
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']",
# USE_OGG, USE_VORBIS for OGG/VORBIS usage
"-C", "link-args=--preload-file assets/",
]

View File

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

View File

@@ -23,6 +23,9 @@ at.
- More than 4 ghosts
- Custom Level Generation
- Multi-map tunnelling
- Online Scoreboard
- WebAssembly build contains a special API key for communicating with server.
- To prevent abuse, the server will only accept scores from the WebAssembly build.
## Installation

54
STORY.md Normal file
View File

@@ -0,0 +1,54 @@
# Story
This is living document that describes the story of the project, from inspiration to solution.
When a website is available, this document will help curate it's content.
## Inspiration
I initially got the idea for this project after finding a video about another Pac-Man clone on YouTube.
[![Code Review Thumbnail][code-review-thumbnail]][code-review-video]
This implementation was written in C++, used SDL2 for graphics, and was kinda weird - but it worked.
- I think it was weird because the way it linked files together is extremely non-standard.
Essentially, it was a single file that included all the other files. This is not how C++ projects are typically structured.
- This implementation was also extremely dependent on OOP; Rust has no real counterpart for OOP code, so writing my own implementation would be a challenge.
## Lifetimes
Rust's SDL2 implementation is a wrapper around the C library, so it's not as nice as the C++ implementation.
Additionally, lifetimes in this library are a bit weird, making them quite difficult to deal with.
I found a whole blog post complaining about this ([1][fighting-lifetimes-1], [2][fighting-lifetimes-2], [3][fighting-lifetimes-3]), so I'm not alone in this.
## Emscripten & RuggRogue
One of the targets for this project is to build a web-accessible version of the game. If you were watching at all during
the Rust hype, one of it's primary selling points was a growing community of Rust-based web applications, thanks to
WebAssembly.
The problem is that much of this work was done for pure-Rust applications - and SDL is C++.
This requires a C++ WebAssembly compiler such as Emscripten; and it's a pain to get working.
Luckily though, someone else has done this before, and they fully documented it - [RuggRouge][ruggrouge].
- Built with Rust
- Uses SDL2
- Compiling for WebAssembly with Emscripten
- Also compiles for Windows & Linux
This repository has been massively helpful in getting my WebAssembly builds working.
## Key Capturing Extensions in WASM Build
Some extensions I had installed were capturing keys.
The issue presented with some keys never being sent to the application.
To confirm, enter safe mode or switch to a different browser without said extensions.
If the issue disappears, it's because of an extension in your browser stealing keys in a way that is incompatible with the batshit insanity of Emscripten.
[code-review-video]: https://www.youtube.com/watch?v=OKs_JewEeOo
[code-review-thumbnail]: https://img.youtube.com/vi/OKs_JewEeOo/hqdefault.jpg
[fighting-lifetimes-1]: https://devcry.heiho.net/html/2022/20220709-rust-and-sdl2-fighting-with-lifetimes.html
[fighting-lifetimes-2]: https://devcry.heiho.net/html/2022/20220716-rust-and-sdl2-fighting-with-lifetimes-2.html
[fighting-lifetimes-3]: https://devcry.heiho.net/html/2022/20220724-rust-and-sdl2-fighting-with-lifetimes-3.html
[ruggrogue]: https://tung.github.io/ruggrogue/

27
assets/index.html Normal file
View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<style>
body {
margin: 0;
padding: 0;
background: #000;
}
canvas {
display: block;
margin: 0 auto;
background: #000;
}
</style>
<body>
<canvas id="canvas"></canvas>
<script>
var Module = {
'canvas': document.getElementById('canvas'),
};
</script>
<script src="pacman.js"></script>
</body>
</html>

View File

@@ -1,7 +0,0 @@
& cargo build --target=wasm32-unknown-emscripten --release
mkdir -p dist -Force
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,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>

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

@@ -0,0 +1,13 @@
#!/bin/sh
# set -eu
echo "Building WASM with Emscripten"
cargo build --target=wasm32-unknown-emscripten --release
echo "Copying release files to dist/"
mkdir -p dist
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
cp assets/index.html dist

58
scripts/build-windows.sh Executable file
View File

@@ -0,0 +1,58 @@
#!/bin/bash
set -eu
SDL_VERSION="2.28.3"
SDL_IMAGE_VERSION="2.6.3"
SDL_MIXER_VERSION="2.6.3"
SDL_TTF_VERSION="2.20.2"
SDL="https://github.com/libsdl-org/SDL/releases/download/release-${SDL_VERSION}/SDL2-devel-${SDL_VERSION}-mingw.tar.gz"
SLD_IMAGE="https://github.com/libsdl-org/SDL_image/releases/download/release-${SDL_IMAGE_VERSION}/SDL2_image-devel-${SDL_IMAGE_VERSION}-mingw.tar.gz"
SDL_MIXER="https://github.com/libsdl-org/SDL_mixer/releases/download/release-${SDL_MIXER_VERSION}/SDL2_mixer-devel-${SDL_MIXER_VERSION}-mingw.tar.gz"
SDL_TTF="https://github.com/libsdl-org/SDL_ttf/releases/download/release-${SDL_TTF_VERSION}/SDL2_ttf-devel-${SDL_TTF_VERSION}-mingw.tar.gz"
EXTRACT_DIR="./target/x86_64-pc-windows-gnu/release/deps"
if [ ! -f $EXTRACT_DIR/libSDL2.a ]; then
if [ ! -f ./sdl2.tar.gz ]; then
echo "Downloading SDL2@$SDL_VERSION..."
curl -L -o ./sdl2.tar.gz $SDL
fi
echo "Extracting SDL2..."
tar -xzf ./sdl2.tar.gz -C $EXTRACT_DIR --strip-components=3 "SDL2-$SDL_VERSION/x86_64-w64-mingw32/lib/libSDL2.a"
rm -f ./sdl2.tar.gz
fi
if [ ! -f $EXTRACT_DIR/libSDL2_image.a ]; then
if [ ! -f ./sdl2_image.tar.gz ]; then
echo "Downloading SDL2_image@$SDL_IMAGE_VERSION..."
curl -L -o ./sdl2_image.tar.gz $SLD_IMAGE
fi
echo "Extracting SDL2_image..."
tar -xzf ./sdl2_image.tar.gz -C $EXTRACT_DIR --strip-components=3 "SDL2_image-$SDL_IMAGE_VERSION/x86_64-w64-mingw32/lib/libSDL2_image.a"
fi
rm -f ./sdl2_image.tar.gz
if [ ! -f $EXTRACT_DIR/libSDL2_mixer.a ]; then
if [ ! -f ./sdl2_mixer.tar.gz ]; then
echo "Downloading SDL2_mixer@$SDL_MIXER_VERSION..."
curl -L -o ./sdl2_mixer.tar.gz $SDL_MIXER
fi
echo "Extracting SDL2_mixer..."
tar -xzf ./sdl2_mixer.tar.gz -C $EXTRACT_DIR --strip-components=3 "SDL2_mixer-$SDL_MIXER_VERSION/x86_64-w64-mingw32/lib/libSDL2_mixer.a"
rm -f ./sdl2_mixer.tar.gz
fi
if [ ! -f $EXTRACT_DIR/libSDL2_ttf.a ]; then
if [ ! -f ./sdl2_ttf.tar.gz ]; then
echo "Downloading SDL2_ttf@$SDL_TTF_VERSION..."
curl -L -o ./sdl2_ttf.tar.gz $SDL_TTF
fi
echo "Extracting SDL2_ttf..."
tar -xzf ./sdl2_ttf.tar.gz -C $EXTRACT_DIR --strip-components=3 "SDL2_ttf-$SDL_TTF_VERSION/x86_64-w64-mingw32/lib/libSDL2_ttf.a"
rm -f ./sdl2_ttf.tar.gz
fi
echo "Building..."
cargo zigbuild --release --target x86_64-pc-windows-gnu

9
scripts/clean-windows.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/bash
set -eux
echo "Cleaning library files from ./target/x86_64-pc-windows-gnu/release/deps"
rm -f ./target/x86_64-pc-windows-gnu/release/deps/libSDL2.a
rm -f ./target/x86_64-pc-windows-gnu/release/deps/libSDL2_image.a
rm -f ./target/x86_64-pc-windows-gnu/release/deps/libSDL2_mixer.a
rm -f ./target/x86_64-pc-windows-gnu/release/deps/libSDL2_ttf.a
echo "Done."

View File

@@ -1,3 +0,0 @@
& ./build.ps1
cd ./dist
python -m http.server

View File

@@ -92,6 +92,7 @@ impl<'a> AnimatedTexture<'a> {
direction: Direction,
frame: u32,
) {
// TODO: If the frame we're targeting is in the opposite direction (due to self.reverse), we should pre-emptively reverse.
let current = self.current_frame();
self.render_static(canvas, position, direction, Some(current));
@@ -99,7 +100,7 @@ impl<'a> AnimatedTexture<'a> {
self.tick();
}
}
// Renders a specific frame of the animation. Defaults to the current frame.
pub fn render_static(
&mut self,

View File

@@ -48,4 +48,4 @@ pub const RAW_BOARD: [&str; BOARD_HEIGHT as usize] = [
"#.##########.##.##########.#",
"#..........................#",
"############################",
];
];

View File

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

View File

@@ -1,44 +0,0 @@
// taken from https://github.com/Gigoteur/PX8/blob/master/src/px8/emscripten.rs
#[cfg(target_os = "emscripten")]
pub mod emscripten {
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};
#[allow(non_camel_case_types)]
type em_callback_func = unsafe extern "C" fn();
extern "C" {
// 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,
fps: c_int,
simulate_infinite_loop: c_int);
pub fn emscripten_cancel_main_loop();
pub fn emscripten_pause_main_loop();
pub fn emscripten_get_now() -> c_float;
}
thread_local!(static MAIN_LOOP_CALLBACK: RefCell<*mut c_void> = RefCell::new(null_mut()));
pub fn set_main_loop_callback<F>(callback: F)
where F: FnMut()
{
MAIN_LOOP_CALLBACK
.with(|log| { *log.borrow_mut() = &callback as *const _ as *mut c_void; });
unsafe {
emscripten_set_main_loop(wrapper::<F>, -1, 1);
}
unsafe extern "C" fn wrapper<F>()
where F: FnMut()
{
MAIN_LOOP_CALLBACK.with(|z| {
let closure = *z.borrow_mut() as *mut F;
(*closure)();
});
}
}
}

View File

@@ -6,6 +6,6 @@ pub trait Entity {
// 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)
// Tick the entity (move it, perform collision checks, etc)
fn tick(&mut self);
}
}

View File

@@ -5,7 +5,6 @@ use sdl2::keyboard::Keycode;
use sdl2::render::{Texture, TextureCreator};
use sdl2::video::WindowContext;
use sdl2::{pixels::Color, render::Canvas, video::Window};
use tracing::event;
use crate::constants::{MapTile, BOARD_HEIGHT, BOARD_WIDTH, RAW_BOARD};
use crate::direction::Direction;
@@ -47,7 +46,7 @@ impl Game<'_> {
// Change direction
let direction = Direction::from_keycode(keycode);
self.pacman.next_direction = direction;
// Toggle debug mode
if keycode == Keycode::Space {
self.debug = !self.debug;
@@ -63,6 +62,8 @@ impl Game<'_> {
self.canvas.set_draw_color(Color::RGB(0, 0, 0));
self.canvas.clear();
// Render the map
self.canvas
.copy(&self.map_texture, None, None)
.expect("Could not render texture on canvas");
@@ -74,7 +75,10 @@ impl Game<'_> {
if self.debug {
for x in 0..BOARD_WIDTH {
for y in 0..BOARD_HEIGHT {
let tile = self.map.get_tile((x as i32, y as i32)).unwrap_or(MapTile::Empty);
let tile = self
.map
.get_tile((x as i32, y as i32))
.unwrap_or(MapTile::Empty);
let mut color = None;
if (x, y) == self.pacman.cell_position() {
@@ -100,6 +104,7 @@ impl Game<'_> {
self.draw_cell((next_cell.0 as u32, next_cell.1 as u32), Color::YELLOW);
}
// Present the canvas
self.canvas.present();
}

View File

@@ -1,27 +1,20 @@
use crate::constants::{WINDOW_HEIGHT, WINDOW_WIDTH};
use crate::game::Game;
use tracing::{event};
use sdl2::event::{Event};
use sdl2::event::{Event, WindowEvent};
use sdl2::keyboard::Keycode;
use std::time::{Duration, Instant};
use spin_sleep::sleep;
use tracing::event;
use tracing_error::ErrorLayer;
use tracing_subscriber::layer::SubscriberExt;
#[cfg(target_os = "emscripten")]
pub mod emscripten;
mod animation;
mod constants;
mod direction;
mod entity;
mod game;
mod pacman;
mod modulation;
mod map;
#[cfg(target_os = "emscripten")]
mod emscripten;
mod modulation;
mod pacman;
pub fn main() {
let sdl_context = sdl2::init().unwrap();
@@ -29,6 +22,7 @@ pub fn main() {
// Setup tracing
let subscriber = tracing_subscriber::fmt()
.with_ansi(cfg!(not(target_os = "emscripten")))
.with_max_level(tracing::Level::DEBUG)
.finish()
.with(ErrorLayer::default());
@@ -43,7 +37,6 @@ pub fn main() {
let mut canvas = window
.into_canvas()
.accelerated()
.build()
.expect("Could not build canvas");
@@ -68,14 +61,31 @@ pub fn main() {
// The start of a period of time over which we average the frame time.
let mut last_averaging_time = Instant::now();
let mut sleep_time = Duration::ZERO;
let mut paused = false;
let mut shown = 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 start = Instant::now();
// TODO: Fix key repeat delay issues by using VecDeque for instant key repeat
for event in event_pump.poll_iter() {
match event {
Event::Window { win_event, .. } => match win_event {
WindowEvent::Hidden => {
event!(tracing::Level::DEBUG, "Window hidden");
shown = false;
}
WindowEvent::Shown => {
event!(tracing::Level::DEBUG, "Window shown");
shown = true;
}
_ => {}
},
// Handle quitting keys or window close
Event::Quit { .. }
| Event::KeyDown {
@@ -83,8 +93,19 @@ pub fn main() {
..
} => {
event!(tracing::Level::INFO, "Exit requested. Exiting...");
return false
},
return false;
}
Event::KeyDown {
keycode: Some(Keycode::P),
..
} => {
paused = !paused;
event!(
tracing::Level::INFO,
"{}",
if paused { "Paused" } else { "Unpaused" }
);
}
Event::KeyDown { keycode, .. } => {
game.keyboard_event(keycode.unwrap());
}
@@ -92,12 +113,24 @@ pub fn main() {
}
}
game.tick();
game.draw();
// TODO: Proper pausing implementation that does not interfere with statistic gathering
if !paused {
game.tick();
game.draw();
}
if start.elapsed() < loop_time {
let time = loop_time - start.elapsed();
sleep(time);
let time = loop_time.saturating_sub(start.elapsed());
if time != Duration::ZERO {
#[cfg(not(target_os = "emscripten"))]
{
spin_sleep::sleep(time);
}
#[cfg(target_os = "emscripten")]
{
std::thread::sleep(time);
}
}
sleep_time += time;
} else {
event!(
@@ -109,9 +142,11 @@ pub fn main() {
tick_no += 1;
if tick_no % (60 * 60) == 0 || tick_no == (60 * 2) {
let average_fps = (tick_no % (60 * 60)) as f32 / last_averaging_time.elapsed().as_secs_f32();
let average_sleep = sleep_time / tick_no;
const PERIOD: u32 = 60 * 60;
let tick_mod = tick_no % PERIOD;
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;
event!(
@@ -119,7 +154,7 @@ pub fn main() {
"Timing Averages [fps={}] [sleep={:?}] [process={:?}]",
average_fps,
average_sleep,
average_process
average_process
);
sleep_time = Duration::ZERO;
@@ -129,13 +164,6 @@ pub fn main() {
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 {
if !main_loop() {
break;

View File

@@ -2,7 +2,7 @@ use crate::constants::MapTile;
use crate::constants::{BOARD_HEIGHT, BOARD_WIDTH};
pub struct Map {
inner: [[MapTile; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize]
inner: [[MapTile; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize],
}
impl Map {
@@ -16,7 +16,7 @@ impl Map {
if x >= line.len() {
break;
}
let i = (y * (BOARD_WIDTH as usize) + x) as usize;
let character = line
.chars()
@@ -30,7 +30,7 @@ impl Map {
' ' => 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),
};
@@ -39,9 +39,7 @@ impl Map {
}
}
Map {
inner: inner
}
Map { inner: inner }
}
pub fn get_tile(&self, cell: (i32, i32)) -> Option<MapTile> {
@@ -58,4 +56,4 @@ impl Map {
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.
///
///
/// 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;

View File

@@ -9,7 +9,7 @@ use tracing::event;
use crate::{
animation::AnimatedTexture,
constants::MapTile,
constants::{CELL_SIZE, BOARD_OFFSET},
constants::{BOARD_OFFSET, CELL_SIZE},
direction::Direction,
entity::Entity,
map::Map,
@@ -34,11 +34,11 @@ impl Pacman<'_> {
position: Map::cell_to_pixel(starting_position),
direction: Direction::Right,
next_direction: None,
speed: 2,
speed: 3,
map,
stopped: false,
modulation: SimpleTickModulator::new(0.9333),
sprite: AnimatedTexture::new(atlas, 4, 3, 32, 32, Some((-4, -4))),
modulation: SimpleTickModulator::new(1.0),
sprite: AnimatedTexture::new(atlas, 2, 3, 32, 32, Some((-4, -4))),
}
}
@@ -59,19 +59,29 @@ impl Pacman<'_> {
}
fn handle_requested_direction(&mut self) {
if self.next_direction.is_none() { return; }
if self.next_direction.unwrap() == self.direction {
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);
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;
}
}
fn internal_position_even(&self) -> (u32, u32) {
let (x, y ) = self.internal_position();
((x / 2u32) * 2u32, (y / 2u32) * 2u32)
}
}
impl Entity for Pacman<'_> {
@@ -87,7 +97,10 @@ impl Entity for Pacman<'_> {
fn cell_position(&self) -> (u32, u32) {
let (x, y) = self.position;
((x as u32 / CELL_SIZE) - BOARD_OFFSET.0, (y as u32 / CELL_SIZE) - BOARD_OFFSET.1)
(
(x as u32 / CELL_SIZE) - BOARD_OFFSET.0,
(y as u32 / CELL_SIZE) - BOARD_OFFSET.1,
)
}
fn internal_position(&self) -> (u32, u32) {
@@ -96,7 +109,7 @@ impl Entity for Pacman<'_> {
}
fn tick(&mut self) {
let can_change = self.internal_position() == (0, 0);
let can_change = self.internal_position_even() == (0, 0);
if can_change {
self.handle_requested_direction();
@@ -112,7 +125,7 @@ impl Entity for Pacman<'_> {
self.stopped = false;
}
}
if !self.stopped && self.modulation.next() {
let speed = self.speed as i32;
match self.direction {