Compare commits

...

57 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
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
95298fbc00 feat: keycode to direction utility function 2025-06-17 11:54:13 -05:00
fe18eafbaf chore: doc & expose TickModulator trait, rename speed to tick 2025-06-17 11:54:13 -05:00
60eaa428ac reformat: improve AnimatedTexture API with paused animation abilities 2025-06-17 11:54:13 -05:00
18eaeee19e fix: compile time removal of tracing below WARN on release builds 2025-06-17 11:54:13 -05:00
b3c1a30a74 feat: tracing, sleep timing calculations, use spin_sleeper for accurate sleeps on Windows 2025-06-17 11:52:08 -05:00
0d76c6528b docs: add DLL instructions to README, expand .gitignore 2025-06-17 11:51:57 -05:00
da98b54216 feat: wall collisions 2025-06-17 11:51:49 -05:00
6ce3a5ce79 feat: speed modulation to implement precise speed decrease despite integers 2025-06-17 11:51:40 -05:00
b987599f10 reformat: general, target conditional module 2025-06-17 11:51:35 -05:00
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
28 changed files with 1146 additions and 248 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

4
.gitignore vendored
View File

@@ -1,2 +1,4 @@
/target
.idea
/dist
.idea
*.dll

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

276
Cargo.lock generated
View File

@@ -2,6 +2,15 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
dependencies = [
"memchr",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -26,14 +35,129 @@ version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
]
[[package]]
name = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "once_cell"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "pacman"
version = "0.1.0"
dependencies = [
"lazy_static",
"sdl2",
"spin_sleep",
"tracing",
"tracing-error",
"tracing-subscriber",
]
[[package]]
name = "pin-project-lite"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
name = "proc-macro2"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.3.3",
"regex-syntax 0.7.4",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
name = "regex-automata"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.7.4",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
[[package]]
name = "sdl2"
version = "0.35.2"
@@ -57,8 +181,160 @@ dependencies = [
"version-compare",
]
[[package]]
name = "sharded-slab"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
dependencies = [
"lazy_static",
]
[[package]]
name = "smallvec"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
[[package]]
name = "spin_sleep"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cafa7900db085f4354dbc7025e25d7a839a14360ea13b5fc4fd717f2d3b23134"
dependencies = [
"once_cell",
"winapi",
]
[[package]]
name = "syn"
version = "2.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thread_local"
version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "tracing"
version = "0.1.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
dependencies = [
"cfg-if",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-error"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e"
dependencies = [
"tracing",
"tracing-subscriber",
]
[[package]]
name = "tracing-log"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
dependencies = [
"lazy_static",
"log",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
name = "unicode-ident"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version-compare"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

View File

@@ -8,3 +8,7 @@ edition = "2021"
[dependencies]
lazy_static = "1.4.0"
sdl2 = { version = "0.35", features = ["image", "ttf", "mixer"] }
spin_sleep = "1.1.1"
tracing = { version = "0.1.37", features = ["max_level_debug", "release_max_level_warn"]}
tracing-error = "0.2.0"
tracing-subscriber = {version = "0.3.17", features = ["env-filter"]}

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
@@ -49,6 +52,16 @@ The latest releases can be found here:
Download each for your architecture, and locate the appropriately named DLL within. Move said DLL to root of this project.
In total, you should have the following DLLs in the root of the project:
- SDL2.dll
- SDL2_mixer.dll
- SDL2_ttf.dll
- SDL2_image.dll
- libpngX-X.dll
- Not sure on what specific version is to be used, or if naming matters. `libpng16-16.dll` is what I had used.
- zlib1.dll
## Building
To build the project, run the following command:

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

132
src/animation.rs Normal file
View File

@@ -0,0 +1,132 @@
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)),
}
}
// Get the current frame number
fn current_frame(&self) -> u32 {
self.ticker / self.ticks_per_frame
}
// 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 {
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;
}
}
}
// 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(
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,
) {
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,
) {
// 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));
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(
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");
}
}

View File

@@ -1,11 +1,11 @@
use lazy_static::lazy_static;
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 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_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)]
pub enum MapTile {
@@ -17,9 +17,6 @@ pub enum MapTile {
}
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
};
}

43
src/direction.rs Normal file
View File

@@ -0,0 +1,43 @@
use sdl2::keyboard::Keycode;
#[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,
}
}
pub fn offset(&self) -> (i32, i32) {
match self {
Direction::Right => (1, 0),
Direction::Down => (0, 1),
Direction::Left => (-1, 0),
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,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)();
});
}
}
}

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,63 +1,123 @@
use std::rc::Rc;
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};
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::entity::Entity;
use crate::map::Map;
use crate::pacman::Pacman;
use crate::textures::TextureManager;
pub struct Game<'a> {
pub textures: TextureManager<'a>,
canvas: &'a mut Canvas<Window>,
map_texture: Texture<'a>,
pacman: Pacman<'a>,
map: Rc<Map>,
debug: bool,
}
impl Game<'_> {
pub fn new<'a>(
canvas: &'a mut Canvas<Window>,
texture_manager: TextureManager<'a>,
texture_creator: &'a TextureCreator<WindowContext>,
) -> Game<'a> {
let pacman = Pacman::new(None, &texture_manager.pacman);
let map = Rc::new(Map::new(RAW_BOARD));
let pacman_atlas = texture_creator
.load_texture("assets/32/pacman.png")
.expect("Could not load pacman texture");
let pacman = Pacman::new((1, 1), pacman_atlas, Rc::clone(&map));
Game {
canvas,
textures: texture_manager,
pacman: pacman,
debug: true,
debug: false,
map: map,
map_texture: texture_creator
.load_texture("assets/map.png")
.expect("Could not load pacman texture"),
}
}
pub fn tick(&mut self) {}
pub fn keyboard_event(&mut self, keycode: Keycode) {
// Change direction
let direction = Direction::from_keycode(keycode);
self.pacman.next_direction = direction;
// Toggle debug mode
if keycode == 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();
// Render the map
self.canvas
.copy(&self.textures.map, None, None)
.copy(&self.map_texture, None, None)
.expect("Could not render texture on canvas");
// Draw a grid
for x in 0..BOARD_WIDTH {
for y in 0..BOARD_HEIGHT {
let tile = BOARD[x as usize][y as usize];
let 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),
};
// Render the pacman
self.pacman.render(self.canvas);
if let Some(color) = color {
self.canvas.set_draw_color(color);
self.canvas
.draw_rect(sdl2::rect::Rect::new(x as i32 * 24, y as i32 * 24, 24, 24))
.expect("Could not draw rectangle");
// Draw a grid
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 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);
}
}
}
// 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);
}
// Present the canvas
self.canvas.present();
}
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
.draw_rect(sdl2::rect::Rect::new(
position.0 as i32,
position.1 as i32,
24,
24,
))
.expect("Could not draw rectangle");
}
}

15
src/helper.rs Normal file
View File

@@ -0,0 +1,15 @@
pub fn is_adjacent(a: (u32, u32), b: (u32, u32), diagonal: bool) -> bool {
let (ax, ay) = a;
let (bx, by) = b;
if diagonal {
(ax == bx && (ay == by + 1 || ay == by - 1))
|| (ay == by && (ax == bx + 1 || ax == bx - 1))
|| (ax == bx + 1 && ay == by + 1)
|| (ax == bx + 1 && ay == by - 1)
|| (ax == bx - 1 && ay == by + 1)
|| (ax == bx - 1 && ay == by - 1)
} else {
(ax == bx && (ay == by + 1 || ay == by - 1))
|| (ay == by && (ax == bx + 1 || ax == bx - 1))
}
}

View File

@@ -1,35 +1,34 @@
use crate::constants::{WINDOW_HEIGHT, WINDOW_WIDTH};
use crate::game::Game;
use crate::textures::TextureManager;
use sdl2::event::{Event};
use sdl2::event::{Event, WindowEvent};
use sdl2::keyboard::Keycode;
use sdl2::pixels::Color;
use sdl2::render::{Canvas, Texture};
use std::time::{Duration, Instant};
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 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");
}
mod game;
mod map;
mod modulation;
mod pacman;
pub fn main() {
let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
// Setup tracing
let subscriber = tracing_subscriber::fmt()
.with_ansi(cfg!(not(target_os = "emscripten")))
.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
.window("Pac-Man", WINDOW_WIDTH, WINDOW_HEIGHT)
.position_centered()
@@ -46,62 +45,125 @@ 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()
.expect("Could not get SDL EventPump");
// Initial draw and tick
game.draw();
game.tick();
let loop_time = Duration::from_secs(1) / 60;
let mut tick_no = 0u32;
// 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
);
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 {
keycode: Some(Keycode::Escape) | Some(Keycode::Q),
..
} => return false,
event @ Event::KeyDown { .. } => {
println!("{:?}", event);
} => {
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, .. } => {
game.keyboard_event(keycode.unwrap());
}
_ => {}
}
}
let tick_time = {
let start = Instant::now();
// TODO: Proper pausing implementation that does not interfere with statistic gathering
if !paused {
game.tick();
start.elapsed()
};
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);
}
::std::thread::sleep(Duration::from_millis(10));
if start.elapsed() < loop_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!(
tracing::Level::WARN,
"Game loop behind schedule by: {:?}",
start.elapsed() - loop_time
);
}
tick_no += 1;
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!(
tracing::Level::DEBUG,
"Timing Averages [fps={}] [sleep={:?}] [process={:?}]",
average_fps,
average_sleep,
average_process
);
sleep_time = Duration::ZERO;
last_averaging_time = Instant::now();
}
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;

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)
}
}

48
src/modulation.rs Normal file
View File

@@ -0,0 +1,48 @@
/// 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,
ticks_left: u32,
}
// TODO: Add tests
// 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;
SimpleTickModulator {
tick_count: ticks_required,
ticks_left: ticks_required,
}
}
fn next(&mut self) -> bool {
self.ticks_left -= 1;
// Return whether or not we should skip this tick
if self.ticks_left == 0 {
// We've reached the tick to skip, reset the counter
self.ticks_left = self.tick_count;
false
} else {
true
}
}
}

147
src/pacman.rs Normal file
View File

@@ -0,0 +1,147 @@
use std::rc::Rc;
use sdl2::{
render::{Canvas, Texture},
video::Window,
};
use tracing::event;
use crate::{
animation::AnimatedTexture,
constants::MapTile,
constants::{BOARD_OFFSET, CELL_SIZE},
direction::Direction,
entity::Entity,
map::Map,
modulation::{SimpleTickModulator, TickModulator},
};
pub struct Pacman<'a> {
// Absolute position on the board (precise)
pub position: (i32, i32),
pub direction: Direction,
pub next_direction: Option<Direction>,
pub stopped: bool,
map: Rc<Map>,
speed: u32,
modulation: SimpleTickModulator,
sprite: AnimatedTexture<'a>,
}
impl Pacman<'_> {
pub fn new<'a>(starting_position: (u32, u32), atlas: Texture<'a>, map: Rc<Map>) -> Pacman<'a> {
Pacman {
position: Map::cell_to_pixel(starting_position),
direction: Direction::Right,
next_direction: None,
speed: 3,
map,
stopped: false,
modulation: SimpleTickModulator::new(1.0),
sprite: AnimatedTexture::new(atlas, 2, 3, 32, 32, Some((-4, -4))),
}
}
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);
}
}
pub fn next_cell(&self, direction: Option<Direction>) -> (i32, i32) {
let (x, y) = direction.unwrap_or(self.direction).offset();
let cell = self.cell_position();
(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;
}
}
fn internal_position_even(&self) -> (u32, u32) {
let (x, y ) = self.internal_position();
((x / 2u32) * 2u32, (y / 2u32) * 2u32)
}
}
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) - BOARD_OFFSET.0,
(y as u32 / CELL_SIZE) - BOARD_OFFSET.1,
)
}
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_even() == (0, 0);
if can_change {
self.handle_requested_direction();
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() {
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;
}
}
}
}
}

View File

@@ -1,29 +0,0 @@
use sdl2::{
image::LoadTexture,
render::{Texture, TextureCreator},
video::WindowContext,
};
pub struct TextureManager<'a> {
pub map: Texture<'a>,
pub pacman: Texture<'a>,
}
impl<'a> TextureManager<'a> {
pub fn new(texture_creator: &'a TextureCreator<WindowContext>) -> Self {
let map_texture = texture_creator
.load_texture("assets/map.png")
.expect("Could not load pacman texture");
let pacman_atlas = texture_creator
.load_texture("assets/pacman.png")
.expect("Could not load pacman texture");
TextureManager {
map: map_texture,
pacman: pacman_atlas,
}
}
}