Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e498ee5c3 | |||
| 715eeb6296 | |||
| 682ce059fa | |||
| c8314b23dd | |||
| 40acffafd1 | |||
| 2187212b7c | |||
| 229d2242ef | |||
| 00c4c76299 | |||
| 8b30a602bf | |||
| 83a5ccdb8e | |||
| 44d8184d8b | |||
| 0630fc56ec | |||
| 98d8960c57 | |||
| 394344c11f | |||
| 93ba470ce9 | |||
| 129aed0ffb | |||
| e062ada301 | |||
| af57199915 | |||
| 538cf1efb5 | |||
| 03b2c5a659 | |||
| 64e226be70 | |||
| f998ddd344 | |||
| b2ad8e7afe | |||
| 799d5d85e8 | |||
| 9730d02da5 | |||
| f634beffee | |||
| d15dbe3982 | |||
| de5cddd9b6 | |||
| e3f37ab48e | |||
| 3dd8d5aff7 | |||
| ad084d1cd8 | |||
| 852e54f1bf | |||
| a62ddab9af | |||
| 50d0bc7d5f | |||
| 2c6045aa1b | |||
| bf8370ef35 | |||
| c71b6d69ab | |||
| a7e87c18a3 | |||
| 95298fbc00 | |||
| fe18eafbaf | |||
| 60eaa428ac | |||
| 18eaeee19e | |||
| b3c1a30a74 | |||
| 0d76c6528b | |||
| da98b54216 | |||
| 6ce3a5ce79 | |||
| b987599f10 | |||
| 786fbb5002 | |||
| 422535c00d | |||
| 0120abe806 | |||
| e61930c08a | |||
| f7ff9f5290 | |||
| de29dc6711 | |||
| c90f221c73 | |||
| 841943e121 | |||
| 83d665123c | |||
| ffc21c8622 | |||
| b46a51bc76 | |||
| 443afb1223 | |||
| 724878dc17 | |||
| 274404b9ea | |||
| 2214a5541f | |||
| 64de5fb732 | |||
| 4a4e6e40a9 | |||
| ccde1b6538 | |||
| d90785a61a | |||
| 227c603ffe | |||
| 361ff95f7f | |||
| f3fe1b783b | |||
| 570cee0f62 | |||
| 5b6fd34a6f | |||
| 973fa08ac9 | |||
| 4a1f565f72 | |||
| 2ddf2611d9 | |||
| b3a3664578 |
10
.cargo/config.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[target.wasm32-unknown-emscripten]
|
||||||
|
# TODO: Document what the fuck this is.
|
||||||
|
rustflags = [
|
||||||
|
"-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/",
|
||||||
|
]
|
||||||
229
.github/workflows/deploy.yaml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Github Pages
|
name: Build
|
||||||
|
|
||||||
on: [push]
|
on: [push]
|
||||||
|
|
||||||
@@ -6,23 +6,220 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-github-pages:
|
wasm:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2 # repo checkout
|
- name: Checkout
|
||||||
- uses: mymindstorm/setup-emsdk@v11 # setup emscripten toolchain
|
uses: actions/checkout@v4
|
||||||
# with:
|
|
||||||
# version: 3.1.35
|
- name: Setup Emscripten SDK
|
||||||
- uses: actions-rs/toolchain@v1 # get rust toolchain for wasm
|
uses: mymindstorm/setup-emsdk@v14
|
||||||
|
with:
|
||||||
|
version: 3.1.43
|
||||||
|
|
||||||
|
- name: Setup Rust (WASM32 Emscripten)
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
|
||||||
target: wasm32-unknown-emscripten
|
target: wasm32-unknown-emscripten
|
||||||
override: true
|
|
||||||
- name: Rust Cache # cache the rust build artefacts
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v1
|
uses: Swatinem/rust-cache@v2
|
||||||
- name: Build # build
|
|
||||||
run: ./build.sh
|
- name: Install pnpm
|
||||||
- name: Deploy
|
uses: pnpm/action-setup@v3
|
||||||
uses: JamesIves/github-pages-deploy-action@v4
|
|
||||||
with:
|
with:
|
||||||
folder: dist
|
version: 8
|
||||||
|
run_install: true
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: ./scripts/build.sh -er # release mode, skip emsdk
|
||||||
|
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: "./dist/"
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
|
|
||||||
|
linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
TARGET: x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install SDL2 Packages
|
||||||
|
run: sudo apt-get install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev libsdl2-gfx-dev
|
||||||
|
|
||||||
|
- name: Setup Rust Toolchain (Linux)
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
target: ${{ env.TARGET }}
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --release
|
||||||
|
|
||||||
|
- name: Assemble Archive
|
||||||
|
run: |
|
||||||
|
mkdir /tmp/example/
|
||||||
|
cp ./target/release/pacman /tmp/example/
|
||||||
|
chmod a+x /tmp/example/pacman
|
||||||
|
mkdir /tmp/example/assets
|
||||||
|
cp ./assets/TerminalVector.ttf ./assets/fruit.png /tmp/example/assets
|
||||||
|
|
||||||
|
- name: Install Cargo Binstall
|
||||||
|
uses: cargo-bins/cargo-binstall@main
|
||||||
|
|
||||||
|
- name: Acquire Package Version
|
||||||
|
run: |
|
||||||
|
cargo binstall toml-cli -y
|
||||||
|
PACKAGE_VERSION=$(toml get ./Cargo.toml package.version --raw)
|
||||||
|
echo "PACKAGE_VERSION=${PACKAGE_VERSION}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: "pacman-${{ env.PACKAGE_VERSION }}-${{ env.TARGET }}"
|
||||||
|
path: /tmp/example/
|
||||||
|
retention-days: 7
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
macos:
|
||||||
|
runs-on: macos-13
|
||||||
|
env:
|
||||||
|
TARGET: x86_64-apple-darwin
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install SDL2 Packages
|
||||||
|
run: brew install sdl2 sdl2_image sdl2_mixer sdl2_ttf sdl2_gfx
|
||||||
|
|
||||||
|
- name: Setup Rust Toolchain (MacOS)
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
target: ${{ env.TARGET }}
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --release
|
||||||
|
|
||||||
|
- name: Assemble Archive
|
||||||
|
run: |
|
||||||
|
mkdir /tmp/example/
|
||||||
|
cp ./target/release/pacman /tmp/example/
|
||||||
|
mkdir /tmp/example/assets
|
||||||
|
cp ./assets/TerminalVector.ttf ./assets/fruit.png /tmp/example/assets
|
||||||
|
|
||||||
|
- name: Install Cargo Binstall
|
||||||
|
uses: cargo-bins/cargo-binstall@main
|
||||||
|
|
||||||
|
- name: Acquire Package Version
|
||||||
|
run: |
|
||||||
|
cargo binstall toml-cli -y
|
||||||
|
PACKAGE_VERSION=$(toml get ./Cargo.toml package.version --raw)
|
||||||
|
echo "PACKAGE_VERSION=${PACKAGE_VERSION}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: "pacman-${{ env.PACKAGE_VERSION }}-${{ env.TARGET }}"
|
||||||
|
path: /tmp/example/
|
||||||
|
retention-days: 7
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
windows:
|
||||||
|
env:
|
||||||
|
TARGET: x86_64-pc-windows-gnu
|
||||||
|
SDL2: 2.30.2
|
||||||
|
SDL2_TTF: 2.22.0
|
||||||
|
SDL2_MIXER: 2.8.0
|
||||||
|
SDL2_IMAGE: 2.8.2
|
||||||
|
# SDL2_GFX: 1.0.4
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download SDL2 Libraries
|
||||||
|
run: |
|
||||||
|
curl -L "https://github.com/libsdl-org/SDL/releases/download/release-${{ env.SDL2 }}/SDL2-devel-${{ env.SDL2 }}-VC.zip" -o "sdl2_devel.zip"
|
||||||
|
curl -L "https://github.com/libsdl-org/SDL_mixer/releases/download/release-${{ env.SDL2_MIXER }}/SDL2_mixer-devel-${{ env.SDL2_MIXER }}-VC.zip" -o "sdl2_mixer_devel.zip"
|
||||||
|
curl -L "https://github.com/libsdl-org/SDL_ttf/releases/download/release-${{ env.SDL2_TTF }}/SDL2_ttf-devel-${{ env.SDL2_TTF }}-VC.zip" -o "sdl2_ttf_devel.zip"
|
||||||
|
curl -L "https://github.com/libsdl-org/SDL_image/releases/download/release-${{ env.SDL2_IMAGE }}/SDL2_image-devel-${{ env.SDL2_IMAGE }}-VC.zip" -o "sdl2_image_devel.zip"
|
||||||
|
|
||||||
|
- name: Extract SDL2 DLLs
|
||||||
|
run: |
|
||||||
|
7z x ./sdl2_devel.zip -o"./tmp/"
|
||||||
|
mv ./tmp/SDL2-${{ env.SDL2 }}/lib/x64/SDL2.dll ./
|
||||||
|
mv ./tmp/SDL2-${{ env.SDL2 }}/lib/x64/SDL2.lib ./
|
||||||
|
|
||||||
|
7z x ./sdl2_mixer_devel.zip -o"./tmp/"
|
||||||
|
mv ./tmp/SDL2_mixer-${{ env.SDL2_MIXER }}/lib/x64/SDL2_mixer.dll ./
|
||||||
|
mv ./tmp/SDL2_mixer-${{ env.SDL2_MIXER }}/lib/x64/SDL2_mixer.lib ./
|
||||||
|
|
||||||
|
7z x ./sdl2_ttf_devel.zip -o"./tmp/"
|
||||||
|
mv ./tmp/SDL2_ttf-${{ env.SDL2_TTF }}/lib/x64/SDL2_ttf.dll ./
|
||||||
|
mv ./tmp/SDL2_ttf-${{ env.SDL2_TTF }}/lib/x64/SDL2_ttf.lib ./
|
||||||
|
|
||||||
|
7z x ./sdl2_image_devel.zip -o"./tmp/"
|
||||||
|
mv ./tmp/SDL2_image-${{ env.SDL2_IMAGE }}/lib/x64/SDL2_image.dll ./
|
||||||
|
mv ./tmp/SDL2_image-${{ env.SDL2_IMAGE }}/lib/x64/SDL2_image.lib ./
|
||||||
|
|
||||||
|
- name: Install SDL2_gfx
|
||||||
|
run: |
|
||||||
|
C:\vcpkg\vcpkg.exe install sdl2-gfx:x64-windows-release
|
||||||
|
cp C:\vcpkg\packages\sdl2-gfx_x64-windows-release\bin\SDL2_gfx.dll ./
|
||||||
|
cp C:\vcpkg\packages\sdl2-gfx_x64-windows-release\lib\SDL2_gfx.lib ./
|
||||||
|
|
||||||
|
- name: Setup Rust (Windows)
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ env.TARGET }}
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --release
|
||||||
|
|
||||||
|
- name: Prepare Archive
|
||||||
|
run: |
|
||||||
|
New-Item -Type Directory ./release/
|
||||||
|
Move-Item -Path ./target/release/pacman.exe -Destination ./release/
|
||||||
|
Move-Item -Path ./SDL2.dll, ./SDL2_image.dll, ./SDL2_ttf.dll, ./SDL2_mixer.dll, ./SDL2_gfx.dll -Destination ./release/
|
||||||
|
New-Item -Type Directory ./release/assets/
|
||||||
|
Move-Item -Path ./assets/TerminalVector.ttf, ./assets/fruit.png -Destination ./release/assets/
|
||||||
|
|
||||||
|
- name: Install Cargo Binstall
|
||||||
|
uses: cargo-bins/cargo-binstall@main
|
||||||
|
|
||||||
|
- name: Acquire Package Version
|
||||||
|
run: |
|
||||||
|
cargo binstall toml-cli -y
|
||||||
|
PACKAGE_VERSION=$(toml get ./Cargo.toml package.version --raw)
|
||||||
|
echo "PACKAGE_VERSION=${PACKAGE_VERSION}" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: "pacman-${{ env.PACKAGE_VERSION }}-${{ env.TARGET }}"
|
||||||
|
path: ./release/
|
||||||
|
retention-days: 7
|
||||||
|
if-no-files-found: error
|
||||||
|
|||||||
4
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
/target
|
/target
|
||||||
.idea
|
/dist
|
||||||
|
.idea
|
||||||
|
*.dll
|
||||||
7
BUILD.md
Normal 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
|
||||||
283
Cargo.lock
generated
@@ -3,10 +3,12 @@
|
|||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "Pac-Man"
|
name = "aho-corasick"
|
||||||
version = "0.1.0"
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sdl2",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -33,6 +35,129 @@ version = "0.2.147"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
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]]
|
[[package]]
|
||||||
name = "sdl2"
|
name = "sdl2"
|
||||||
version = "0.35.2"
|
version = "0.35.2"
|
||||||
@@ -56,8 +181,160 @@ dependencies = [
|
|||||||
"version-compare",
|
"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]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
|
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"
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "Pac-Man"
|
name = "pacman"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
lazy_static = "1.4.0"
|
||||||
sdl2 = { version = "0.35", features = ["image", "ttf", "mixer"] }
|
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"]}
|
||||||
35
IMPLEMENTATION.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Implementation
|
||||||
|
|
||||||
|
A document detailing the implementation the project from rendering, to game logic, to build systems.
|
||||||
|
|
||||||
|
## Rendering
|
||||||
|
|
||||||
|
1. Map
|
||||||
|
- May require procedural text generation later on (cacheable?)
|
||||||
|
2. Pacman
|
||||||
|
3. Ghosts
|
||||||
|
- Requires colors
|
||||||
|
4. Items
|
||||||
|
5. Interface
|
||||||
|
- Requires fonts
|
||||||
|
|
||||||
|
## Grid System
|
||||||
|
|
||||||
|
1. How does the grid system work?
|
||||||
|
|
||||||
|
The grid is 28 x 36 (although, the map texture is 28 x 37), and each cell is 24x24 (pixels).
|
||||||
|
Many of the walls in the map texture only occupy a portion of the cell, so some items are able to render across multiple cells.
|
||||||
|
24x24 assets include pellets, the energizer, and the map itself ()
|
||||||
|
|
||||||
|
2. What constraints must be enforced on Ghosts and PacMan?
|
||||||
|
|
||||||
|
3. How do movement transitions work?
|
||||||
|
|
||||||
|
All entities store a precise position, and a direction. This position is only used for animation, rendering, and collision purposes. Otherwise, a separate 'cell position' (which is 24 times less precise, owing to the fact that it is based on the entity's position within the grid).
|
||||||
|
|
||||||
|
When an entity is transitioning between cells, movement directions are acknowledged, but won't take effect until the next cell has been entered completely.
|
||||||
|
|
||||||
|
4. Between transitions, how does collision detection work?
|
||||||
|
|
||||||
|
It appears the original implementation used cell-level detection.
|
||||||
|
I worry this may be prone to division errors. Make sure to use rounding (50% >=).
|
||||||
71
README.md
@@ -2,11 +2,78 @@
|
|||||||
|
|
||||||
If the title doesn't clue you in, I'm remaking Pac-Man with SDL and Rust.
|
If the title doesn't clue you in, I'm remaking Pac-Man with SDL and Rust.
|
||||||
|
|
||||||
The project is *extremely* early in development, but check back in a week, and maybe I'll have something cool to look
|
The project is _extremely_ early in development, but check back in a week, and maybe I'll have something cool to look
|
||||||
at.
|
at.
|
||||||
|
|
||||||
## Feature Targets
|
## Feature Targets
|
||||||
|
|
||||||
- Near-perfect replication of logic, scoring, graphics, sound, and behaviors.
|
- Near-perfect replication of logic, scoring, graphics, sound, and behaviors.
|
||||||
- Written in Rust, buildable on Windows, Linux, Mac and WebAssembly.
|
- Written in Rust, buildable on Windows, Linux, Mac and WebAssembly.
|
||||||
- Online demo, playable in a browser.
|
- Online demo, playable in a browser.
|
||||||
|
- Automatic build system, with releases for Windows, Linux, and Mac & Web-Assembly.
|
||||||
|
- Debug tooling
|
||||||
|
- Game state visualization
|
||||||
|
- Game speed controls + pausing
|
||||||
|
- Log tracing
|
||||||
|
- Performance details
|
||||||
|
|
||||||
|
## Experimental Ideas
|
||||||
|
|
||||||
|
- Perfected Ghost Algorithms
|
||||||
|
- 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
|
||||||
|
|
||||||
|
Besides SDL2, the following extensions are required: Image, Mixer, and TTF.
|
||||||
|
|
||||||
|
### Ubuntu
|
||||||
|
|
||||||
|
On Ubuntu, you can install the required packages with the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
On Windows, installation requires either building from source (not covered), or downloading the pre-built binaries.
|
||||||
|
|
||||||
|
The latest releases can be found here:
|
||||||
|
|
||||||
|
- [SDL2](https://github.com/libsdl-org/SDL/releases/latest/)
|
||||||
|
- [SDL2_image](https://github.com/libsdl-org/SDL_image/releases/latest/)
|
||||||
|
- [SDL2_mixer](https://github.com/libsdl-org/SDL_mixer/releases/latest/)
|
||||||
|
- [SDL2_ttf](https://github.com/libsdl-org/SDL_ttf/releases/latest/)
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo build
|
||||||
|
```
|
||||||
|
|
||||||
|
During development, you can easily run the project with:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run
|
||||||
|
cargo run -q # Quiet mode, no logging
|
||||||
|
cargo run --release # Release mode, optimized
|
||||||
|
```
|
||||||
BIN
SDL2_image.dll
BIN
SDL2_mixer.dll
BIN
SDL2_ttf.dll
333
STORY.md
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
## A Long Break
|
||||||
|
|
||||||
|
After hitting a wall with an issue with Emscripten where the tab would freeze after switching tabs (making it into a background tab), I decided to take a break from the project. A couple months went by without anything going on.
|
||||||
|
|
||||||
|
## Revisiting
|
||||||
|
|
||||||
|
I decided to revisit the project because I didn't want to see this project die. It's actually a lot of fun, and has a very interesting stack, with a simple premise, and a lot of potential for expansion.
|
||||||
|
|
||||||
|
Unfortunately, the issue above still lingered. I did a lot of testing, and concluded that I needed to create a simple example with as much stripped away as possible. All I learned from this was that the freeze occurred the moment that the 'Hidden' event (for the Window) was fired. After that, the rendered would take 0 nanoseconds to render, and some script for Asyncify would keep spinning in the background.
|
||||||
|
|
||||||
|
I tried to ask around but didn't get anywhere, but one reply on my post gave me the idea to back away from Emscripten 1.39.20 (several years old at this point).
|
||||||
|
|
||||||
|
## Emscripten Callback Main Loop
|
||||||
|
|
||||||
|
I looked into as many examples online (not that many), and came across an Emscripten callback loop exposed in C. Some were basic and all over the place, some were advanced, but also imbibed extremely annoying static lifetime requirements.
|
||||||
|
|
||||||
|
- I tried my best to satisfy and work with these lifetimes, but it was a nightmare.
|
||||||
|
- Instead, I tried to simplify and move away from this annoying Emscripten callback loop, but simpler ones had issues, crashing with `invalid renderer` errors.
|
||||||
|
- This guy named Greg Buchholz apparently was the creator of this special Emscripten bindings with the static lifetimes, and it was done to solve this issue with `invalid renderer`.
|
||||||
|
- [GitHub](https://github.com/gregbuchholz), [Repository](https://github.com/gregbuchholz/RuSDLem), [StackOverflow](https://stackoverflow.com/questions/69748049/rust-sdl2-emscripten-and-invalid-renderer-panic), [Forum Post](https://users.rust-lang.org/t/sdl2-emscripten-asmjs-and-invalid-renderer-panic/66567/2)
|
||||||
|
|
||||||
|
With this in mind, it seemed like I was at a dead end AGAIN; either I had to deal with the static lifetimes (I am not that good at Rust), or I had to deal with Asyncify.
|
||||||
|
|
||||||
|
But this did help me narrow my search even more for a good example. I needed to find a repository with Rust, SDL2, Emscripten, and `TextureCreator`.
|
||||||
|
|
||||||
|
`TextureCreator` was key, as the static lifetimes issue was most encumbering when dealing with borrows and lifetimes of `TextureCreator` inside the `main` loop closure.
|
||||||
|
|
||||||
|
## Return to Asyncify
|
||||||
|
|
||||||
|
I found [one such repository](https://github.com/KyleMiles/Rust-SDL-Emscripten-Template/), and interestingly, it used `latest` Emscripten (not a specific target like 1.39.20), and was new enough (2 years old, but still new enough) to be relevant.
|
||||||
|
|
||||||
|
Even more interesting, it didn't use the `main` loop closure, but instead used Emscripten's *Asyncify* feature to handle the main loop.
|
||||||
|
|
||||||
|
But, unlike my original project which called `std::thread::sleep` directly, it used bindings into Emscripten's functions like `emscripten_sleep`.
|
||||||
|
|
||||||
|
Even better, it had an example of script execution (JavaScript) bindings, which I could use to handle all sorts of things. I tested it out, and it worked.
|
||||||
|
|
||||||
|
## Instant::now() 32-bit Byte Cutoff
|
||||||
|
|
||||||
|
Unfortunately while trying to get basic FPS timings working, I got divide by zero errors when trying to calculate the time difference between two `Instant` times.
|
||||||
|
|
||||||
|
This was weird, and honestly, I'm confused as to why the 2-year old sample code 'worked' at the time, but not now.
|
||||||
|
|
||||||
|
After a bit of time, I noted that the `Instant` times were printing with only the whole seconds changing, and the nanoseconds were always 0.
|
||||||
|
|
||||||
|
```
|
||||||
|
Instant { tv_sec: 0, tv_nsec: 0 }
|
||||||
|
Instant { tv_sec: 1, tv_nsec: 0 }
|
||||||
|
Instant { tv_sec: 2, tv_nsec: 0 }
|
||||||
|
Instant { tv_sec: 3, tv_nsec: 0 }
|
||||||
|
Instant { tv_sec: 4, tv_nsec: 0 }
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
This was super weird, but I stumbled upon [an issue on GitHub](https://github.com/rust-lang/rust/issues/113852) that mentioned the exact situation I was in, as well as providing a patch solution (`emscripten_get_now`).
|
||||||
|
|
||||||
|
## VSync Gotcha
|
||||||
|
|
||||||
|
After getting the timing working, I noticed that the rendering was extremely slow. I was getting 60 FPS, but I wasn't sleeping at all.
|
||||||
|
|
||||||
|
Normally when rendering occurs, you want to sleep for the remaining time so that your game calculations can occur at a consistent rate (60 FPS for example).
|
||||||
|
|
||||||
|
If your rendering time is less than the sleep time, you can just sleep for the remaining time. But if your rendering time is greater than the sleep time, you encounter lag, the FPS starts to drop.
|
||||||
|
|
||||||
|
This was a confusing issue as I knew it couldn't be a coincidence that the rendering time was exactly ~16ms (60 FPS) every time.
|
||||||
|
|
||||||
|
After a little bit though, I found the `present_vsync` function in the SDL2 render initialization. This was causing the rendering to try and time the canvas present() to the monitor's refresh rate (60 FPS).
|
||||||
|
|
||||||
|
Maybe I could have skipped my custom timing and just used this, but I don't know if it would be platform-independent, what would happen on 120 FPS displays, etc.
|
||||||
|
|
||||||
|
## Emscripten v.s. SDL2-TTF
|
||||||
|
|
||||||
|
While working on the next extension of SDL2 for my test repository, SDL2-TTF had some pretty annoying issues. It would build fine, but it would raise a runtime error: `indirect call to null`.
|
||||||
|
|
||||||
|
Luckily, I had a recently updated repository to copy off of, and the working fix was to lower the EMSDK version to `3.1.43`.
|
||||||
|
|
||||||
|
[Source](https://github.com/aelred/tetris/blob/0ad88153db1ca7962b42277504c0f7f9f3c675a9/tetris-sdl/src/main.rs#L34)
|
||||||
|
```rust
|
||||||
|
static FONT_DATA: &[u8] = include_bytes!("../assets/TerminalVector.ttf");
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "emscripten"))]
|
||||||
|
fn ttf_context() -> ttf::Sdl2TtfContext {
|
||||||
|
ttf::init().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "emscripten")]
|
||||||
|
fn ttf_context() -> &'static ttf::Sdl2TtfContext {
|
||||||
|
// Deliberately leak so we get a static lifetime
|
||||||
|
Box::leak(Box::new(ttf::init().unwrap()))
|
||||||
|
}
|
||||||
|
|
||||||
|
const FONT_MULTIPLE: u16 = 9;
|
||||||
|
|
||||||
|
// Funny division is done here to round to nearest multiple of FONT_MULTIPLE
|
||||||
|
const FONT_SIZE: u16 = (WINDOW_HEIGHT / 32) as u16 / FONT_MULTIPLE * FONT_MULTIPLE;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
...
|
||||||
|
|
||||||
|
let font_data = RWops::from_bytes(FONT_DATA).unwrap();
|
||||||
|
let font_size = max(FONT_SIZE, FONT_MULTIPLE);
|
||||||
|
let font = ttf_context
|
||||||
|
.load_font_from_rwops(font_data, font_size)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
I don't particularly understand why loading from memory is used, but it's a neat trick. I tested normal font loading afterwards, and it seems to be totally fine.
|
||||||
|
|
||||||
|
On to the Mixer extension, then.
|
||||||
|
|
||||||
|
## Mixer and GFX
|
||||||
|
|
||||||
|
Mixer was relatively easy, I don't remember anything special about it.
|
||||||
|
|
||||||
|
As it happens, neither was SDL GFX, except for me finding that getting it compiling on Windows would soon be difficult; `SDL2_gfx` is not currently being updated, nor is it managed by the SDL team. This meant that no releases of development libraries including DLLs or LIB files were going to be available.
|
||||||
|
|
||||||
|
When I added in GFX, I wanted to add some mouse interaction since that currently wasn't being done anywhere in the demo, but I also wanted the ability for the mouse to be hidden until used.
|
||||||
|
|
||||||
|
Detecting whether the mouse was focusing the window or not wasn't super easy, and I'm still not sure that it's working perfectly, but at the very least Emscripten seems to support what I'm trying to do. I should look into asynchronous Javascript callbacks, see what Emscripten supports.
|
||||||
|
|
||||||
|
## Styling with PostCSS + Tailwind
|
||||||
|
|
||||||
|
I'm big on using Tailwind, and while this project probably could have done without it, I didn't want to forego my favorite tool.
|
||||||
|
|
||||||
|
But I also didn't want to include some big framework on this, like Astro, so I looked for the smallest way to include Tailwind.
|
||||||
|
|
||||||
|
After fiddling and failing to find Hugo suitable, I stuck to plain HTML & the PostCSS method, which worked great. It's definitely not that fast for rapid development, but it works well enough.
|
||||||
|
|
||||||
|
The only thing I'm unsatisfied with is why `postcss-cli` wasn't working when executed from `pnpm`. It works just fine from `pnpx`, but it has to download and setup the whole package on *every single invocation*, which is super slow. And probably expensive, in the long run.
|
||||||
|
|
||||||
|
## Cross-platform Builds
|
||||||
|
|
||||||
|
With the next step of the demo project, I needed to get builds for every OS running, that's one down out of the four targets I'm gunning for.
|
||||||
|
|
||||||
|
Linux was the easiest, as usual, with `apt` providing access to all the development libraries of SDL & the associated extensions, including `SDL2_gfx`.
|
||||||
|
|
||||||
|
There's also no requirement for providing sidecar DLLs like Windows needs, so that worked well.
|
||||||
|
The hardest part was figuring out the most satisfying way to zip and load all the assets together, but luckily the artifact uploader provides it's own zip implementation; albeit I may need to modify it to add further system hinting (`.tar.gz` for Linux, `.dmg` for MacOS, `.zip` for Windows).
|
||||||
|
|
||||||
|
## SDL2 on Windows
|
||||||
|
|
||||||
|
SDL2 on Windows has to be one of the least fun development cycles; setting up the environment is pretty painful as there's almost no guides for Rust users to figure out each requirement. You'll learn fast, and this knowledge is hands on experience that will probably be applicable later on in C++ development, but I'm sure a fair number of Rust users like myself have no idea why a DLL or LIB file is necessary at all.
|
||||||
|
|
||||||
|
To be honest, I still don't.
|
||||||
|
|
||||||
|
Regardless, SDL2 needs a LIB file for compliation to be available in the root directory, and each extension has there own.
|
||||||
|
|
||||||
|
Once the EXE is compiled, the working directory needs to contain a DLL file for execution, too. Each extension has it's own as well.
|
||||||
|
|
||||||
|
This sounds easy, but acquiring these DLLs and LIB files is not easy. At the very least, the SDL-supported extensions have releases available containing
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
So I got to creating a build step involving the download of each of these libraries. I'm no expert with `curl`, but I had it figured out eventually.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Download SDL2 Libraries
|
||||||
|
run: |
|
||||||
|
curl -L "https://github.com/libsdl-org/SDL/releases/download/release-${{ env.SDL2 }}/SDL2-devel-${{ env.SDL2 }}-VC.zip" -o "sdl2_devel.zip"
|
||||||
|
curl -L "https://github.com/libsdl-org/SDL_mixer/releases/download/release-${{ env.SDL2_MIXER }}/SDL2_mixer-devel-${{ env.SDL2_MIXER }}-VC.zip" -o "sdl2_mixer_devel.zip"
|
||||||
|
curl -L "https://github.com/libsdl-org/SDL_ttf/releases/download/release-${{ env.SDL2_TTF }}/SDL2_ttf-devel-${{ env.SDL2_TTF }}-VC.zip" -o "sdl2_ttf_devel.zip"
|
||||||
|
curl -L "https://github.com/libsdl-org/SDL_image/releases/download/release-${{ env.SDL2_IMAGE }}/SDL2_image-devel-${{ env.SDL2_IMAGE }}-VC.zip" -o "sdl2_image_devel.zip"
|
||||||
|
```
|
||||||
|
|
||||||
|
I did take a lot of care in making sure that versions were specified externally in different variables, which took a couple tries while I learned how interpolation works with GitHub Actions.
|
||||||
|
|
||||||
|
Additionally, I realized that `LIB` files were required for compliation after this, so I had to painfully fix all the files to use the `-devel-` version. Speifically the one with `-VC` appended.
|
||||||
|
|
||||||
|
I still do not know what VC means here. Perhaps it is related to `vcpkg` somehow.
|
||||||
|
|
||||||
|
The next step was to extract the files I needed from the `.zip`s, but that proved quite hard. I'm a lover of precision and using tools to the best of my knowledge, so I wanted to finely take just the DLL and ZIP I needed from these archives, and nothing else.
|
||||||
|
|
||||||
|
While I was able to get working commands to do this on Linux, finely finding the exact DLL and placing it in `pwd`, I was not able to replicate it on the Windows-imaged GitHub Runner;
|
||||||
|
|
||||||
|
When specifying the `-o` flag meaning 'output directory here' like `-o./tmp` (yes, there is no space in between), it would always error with `Too short switch: -o`. I was unable to find meaningful discussions on Google.
|
||||||
|
|
||||||
|
My Linux machine did not complain, and I wasn't yet ready to switch OSes for an error like this, so I just extracted everything and then `mv`'d the items into `pwd`.
|
||||||
|
|
||||||
|
I knew what lay ahead with `SDL2_gfx`, so I tested whether the compilation error changed, and luckily, it was only erroring on the missing `SDL2_gfx.lib` at this point.
|
||||||
|
|
||||||
|
While reading discussions online, I came across [a reddit post](https://www.reddit.com/r/rust_gamedev/comments/am84q9/using_sdl2_gfx_on_windows/efk6uwq/) talking about `vcpkg`. I'd heard of it, but never used the program before. It seemed like it could provide `SDL2_gfx` for me without hassle.
|
||||||
|
|
||||||
|
And that was partly true.
|
||||||
|
|
||||||
|
The primary 'boon' of `vcpkg` here was that it setup and compiled `SDL2_gfx` without the hassle of messing with the compiler, options, or most importantly: dependencies.
|
||||||
|
|
||||||
|
I didn't know it at the time, but `SDL2_gfx` depended on `SDL2` directly, and so I'd have to setup and compile both projects, if I was hoping to do this 'manually'.
|
||||||
|
|
||||||
|
## VCPKG for SDL2_GFX
|
||||||
|
|
||||||
|
I tried to use the GitHub-provided environment variables relating to VCPKG's installation location, but nothing really worked here. I was on the correcti mage (`windows-latest` for Windows 2022 Enterprise on GitHub's Runner Images), but nothing seemed to work.
|
||||||
|
|
||||||
|
[This comment](https://github.com/actions/runner-images/issues/6376#issuecomment-1781269302) seemed to describe the exact same experience I was happening, several months ago.
|
||||||
|
|
||||||
|
Alas, I simply tried `C:\vcpkg\` and it worked, providing me the ability to install `SDL2_gfx`.
|
||||||
|
|
||||||
|
As it were though, the hard part wasn't going to be compiling, but locating the DLLs and LIB files for movement. No matter where I looked online or in the logs, nothing was obvious about the location of my files.
|
||||||
|
|
||||||
|
In retrospect, a recursive `Get-ChildItem` looking for `DLL` or `LIB` files probably would've worked well, but.. yeah...
|
||||||
|
|
||||||
|
After a couple attempts with various test commits, I couldn't find it, and just switched to Windows to install and compile it myself, so I could locate the file manually.
|
||||||
|
|
||||||
|
> Note: VCPKG is annoying to install, the executable provided by Visual Studio Community does not permit classic-mode usage, so you'll still need to clone and bootstrap VCPKG (instructions in the repository README).
|
||||||
|
|
||||||
|
As it happens, they were placed in
|
||||||
|
- `$VCPKG_ROOT\packages\sdl2-gfx_x64-windows-release\bin\SDL2_gfx.dll` and
|
||||||
|
- `$VCPKG_ROOT\packages\sdl2-gfx_x64-windows-release\lib\SDL2_gfx.lib` respectively.
|
||||||
|
|
||||||
|
This brings me to one issue, and one fix; while compiling you're required to specify that the build is for 64 bit systems manually, on each invocation of VCPKG (while in classic mode, which I am).
|
||||||
|
|
||||||
|
On top of that, they'll be built in debug mode (with extra symbols and such) by default, which I am not interested in.
|
||||||
|
|
||||||
|
To get the x64 Release build of a package, append `:x64-windows-release` to it, as in `sdl2-gfx:x64-windows-release` for `sdl2-gfx`.
|
||||||
|
|
||||||
|
After getting this sorted, I struggled a little bit in using the `mv` (`Move-Item`) command in Powershell, as I battled with the comma delimited files when moving multiple files to a given destination. Dumb.
|
||||||
|
|
||||||
|
This is also the point at which I renamed the executable from `pacman` to `spiritus` to differentiate the two projects. The name is just my play on the word 'sprite'.
|
||||||
|
|
||||||
|
## Console Window Hiding
|
||||||
|
|
||||||
|
When launching the demo app, I saw a console window pop up, even though I launched it from the File Explorer; this is not the behavior I was interested in.
|
||||||
|
|
||||||
|
I believe that apps launched from File Explorer shouldn't have a console window available unless...
|
||||||
|
|
||||||
|
- It's a CLI app by nature, and it uses the Console Window.
|
||||||
|
- It has a specific debugging flag passed into it, perhaps by a Shortcut file.
|
||||||
|
- The console window is required for the nature of the app, or it is the preferred method of log inspection.
|
||||||
|
- It's a debug build.
|
||||||
|
|
||||||
|
But if it's launched from the console, then it should either
|
||||||
|
|
||||||
|
- Detach and relinquish control of the console back to the user.
|
||||||
|
- Use the console actively in it's logging.
|
||||||
|
|
||||||
|
Most programs I know and use follow this general consensus. Naturally, mine must too.
|
||||||
|
|
||||||
|
But, when searching for a solution online, it seemed what I want doesn't really exist; I implemented the closest approximation.
|
||||||
|
|
||||||
|
If `stdout` is detected to be a `tty` (an active console), the console window won't be hidden. Otherwise, it will be hidden.
|
||||||
|
|
||||||
|
Unfortunately, this results in the millisecond flash of a black console window appearing.
|
||||||
|
|
||||||
|
## Updating Deprecated Actions
|
||||||
|
|
||||||
|
As it were, most of the actions I were using were deprecated in some way. It didn't feel like I was using super old actions, but I guess I was. Luckily, most of them were simply just updating the version (`2` or `3` to `4` or `5`).
|
||||||
|
|
||||||
|
`actions-rs@toolchain` was different though, and was officially deprecated, the GitHub repository archived. Couldn't find a good reason why, but the repository was untouched in 4 years, so maybe that's why...
|
||||||
|
|
||||||
|
I found `dtolnay@rust-toolchain` and switched, it more or less was perfect with no differences. I think it's sorta neat that the Rust version is specified using the version of the action. I'd be worried though of a changing feature set across different action versions...
|
||||||
|
|
||||||
|
I guess a well designed GitHub Action shouldn't change much, including a Rust toolchain action.
|
||||||
|
|
||||||
|
## Artifact Naming
|
||||||
|
|
||||||
|
Perhaps it's super unnecessary and won't be appreciated, but I wanted the artifact files produced by my script to have semantic meaning in it's version and target.
|
||||||
|
|
||||||
|
For each OS, I extracted the targets (`x86_64-unknown-linux-gnu`, `x86_64-apple-darwin`...) into an environment variable scoped at the job level (love that).
|
||||||
|
|
||||||
|
I looked into ways to get the package version, but nothing obvious jumped out at me. I did come across `toml-cli` though, a Rust-based CLI program analogous to `jq` (for JSON).
|
||||||
|
|
||||||
|
It worked great, but it was sorta slow; compiling `toml-cli` added an extra 20-60 seconds for each job. I'd heard of a Rust project to speed up builds by providing prebuilt executables though; it's called `binstall`.
|
||||||
|
|
||||||
|
Even cooler, it was had an action available to easily add it to my build script, and so I had `toml-cli` installing and available 10x faster!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Perhaps I could use some special bash commands to acquire the package version, but it'd be a lot of work and maintenance to get it working in both bash and Powershell, maintaining it across four jobs.
|
||||||
|
|
||||||
|
This is both cool, fast, and easy!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
I was thinking of a github-pages artifact name that aligns with the others, but I think that'd be stupid AND overkill.
|
||||||
|
|
||||||
|
Perhaps at the least I'll look into a 32-bit build for Windows, just for demonstration purposes.
|
||||||
|
|
||||||
|
|
||||||
|
[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/
|
||||||
BIN
assets/24/energizer.png
Normal file
|
After Width: | Height: | Size: 174 B |
BIN
assets/24/pellet.png
Normal file
|
After Width: | Height: | Size: 158 B |
BIN
assets/32/fruit.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/32/game_over.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/32/ghost_body.png
Normal file
|
After Width: | Height: | Size: 528 B |
BIN
assets/32/ghost_eyes.png
Normal file
|
After Width: | Height: | Size: 394 B |
BIN
assets/32/life.png
Normal file
|
After Width: | Height: | Size: 228 B |
BIN
assets/32/pacman.png
Normal file
|
After Width: | Height: | Size: 370 B |
BIN
assets/door.png
Normal file
|
After Width: | Height: | Size: 90 B |
27
assets/index.html
Normal 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>
|
||||||
10
build.sh
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -eux
|
|
||||||
|
|
||||||
cargo build --target=wasm32-unknown-emscripten --release
|
|
||||||
|
|
||||||
mkdir -p dist
|
|
||||||
|
|
||||||
cp target/wasm32-unknown-emscripten/release/rust_sdl2_wasm.wasm dist
|
|
||||||
cp target/wasm32-unknown-emscripten/release/rust-sdl2-wasm.js dist
|
|
||||||
cp index.html dist
|
|
||||||
76
scripts/build.sh
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
release='false'
|
||||||
|
serve='false'
|
||||||
|
skip_emsdk='false'
|
||||||
|
clean='false'
|
||||||
|
|
||||||
|
print_usage() {
|
||||||
|
printf "Usage: -erdsc\n"
|
||||||
|
printf " -e: Skip EMSDK setup (GitHub workflow only)\n"
|
||||||
|
printf " -r: Build in release mode\n"
|
||||||
|
printf " -d: Build in debug mode\n"
|
||||||
|
printf " -s: Serve the WASM files once built\n"
|
||||||
|
printf " -c: Clean the target/dist directory\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
while getopts 'erdsc' flag; do
|
||||||
|
case "${flag}" in
|
||||||
|
e) skip_emsdk='true' ;;
|
||||||
|
r) release='true' ;;
|
||||||
|
d) release='false' ;; # doesn't actually do anything, but last flag wins
|
||||||
|
s) serve='true' ;;
|
||||||
|
c) clean='true' ;;
|
||||||
|
*)
|
||||||
|
print_usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$clean" = 'true' ]; then
|
||||||
|
echo "Cleaning target directory"
|
||||||
|
cargo clean
|
||||||
|
rm -rf ./dist/
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$skip_emsdk" = 'false' ]; then
|
||||||
|
echo "Activating Emscripten"
|
||||||
|
# SDL2-TTF requires 3.1.43, fails to build on latest
|
||||||
|
./../emsdk/emsdk activate 3.1.43
|
||||||
|
source ../emsdk/emsdk_env.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building WASM with Emscripten"
|
||||||
|
build_type='debug'
|
||||||
|
if [ "$release" = 'true' ]; then
|
||||||
|
cargo build --target=wasm32-unknown-emscripten --release
|
||||||
|
build_type='release'
|
||||||
|
else
|
||||||
|
cargo build --target=wasm32-unknown-emscripten
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Generating CSS"
|
||||||
|
pnpx postcss-cli ./assets/styles.scss -o ./assets/build.css
|
||||||
|
|
||||||
|
echo "Copying WASM files"
|
||||||
|
mkdir -p dist
|
||||||
|
output_folder="target/wasm32-unknown-emscripten/$build_type"
|
||||||
|
cp assets/index.html dist
|
||||||
|
|
||||||
|
cp assets/*.woff* dist
|
||||||
|
cp assets/build.css dist
|
||||||
|
cp assets/favicon.ico dist
|
||||||
|
cp $output_folder/spiritus.wasm dist
|
||||||
|
cp $output_folder/spiritus.js dist
|
||||||
|
# only if .data file exists
|
||||||
|
cp $output_folder/deps/spiritus.data dist
|
||||||
|
if [ -f $output_folder/spiritus.wasm.map ]; then
|
||||||
|
cp $output_folder/spiritus.wasm.map dist
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$serve" = 'true' ]; then
|
||||||
|
echo "Serving WASM with Emscripten"
|
||||||
|
python3 -m http.server -d ./dist/ 8080
|
||||||
|
fi
|
||||||
132
src/animation.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/board.rs
Normal file
51
src/constants.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
pub const BOARD_WIDTH: u32 = 28;
|
||||||
|
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 + 6); // Map texture is 6 cells taller (3 above, 3 below) than the grid
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
pub enum MapTile {
|
||||||
|
Empty,
|
||||||
|
Wall,
|
||||||
|
Pellet,
|
||||||
|
PowerPellet,
|
||||||
|
StartingPosition(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const RAW_BOARD: [&str; BOARD_HEIGHT as usize] = [
|
||||||
|
"############################",
|
||||||
|
"#............##............#",
|
||||||
|
"#.####.#####.##.#####.####.#",
|
||||||
|
"#o####.#####.##.#####.####o#",
|
||||||
|
"#.####.#####.##.#####.####.#",
|
||||||
|
"#..........................#",
|
||||||
|
"#.####.##.########.##.####.#",
|
||||||
|
"#.####.##.########.##.####.#",
|
||||||
|
"#......##....##....##......#",
|
||||||
|
"######.##### ## #####.######",
|
||||||
|
" #.##### ## #####.# ",
|
||||||
|
" #.## 1 ##.# ",
|
||||||
|
" #.## ###==### ##.# ",
|
||||||
|
"######.## # # ##.######",
|
||||||
|
" . #2 3 4 # . ",
|
||||||
|
"######.## # # ##.######",
|
||||||
|
" #.## ######## ##.# ",
|
||||||
|
" #.## ##.# ",
|
||||||
|
" #.## ######## ##.# ",
|
||||||
|
"######.## ######## ##.######",
|
||||||
|
"#............##............#",
|
||||||
|
"#.####.#####.##.#####.####.#",
|
||||||
|
"#.####.#####.##.#####.####.#",
|
||||||
|
"#o..##.......0 .......##..o#",
|
||||||
|
"###.##.##.########.##.##.###",
|
||||||
|
"###.##.##.########.##.##.###",
|
||||||
|
"#......##....##....##......#",
|
||||||
|
"#.##########.##.##########.#",
|
||||||
|
"#.##########.##.##########.#",
|
||||||
|
"#..........................#",
|
||||||
|
"############################",
|
||||||
|
];
|
||||||
43
src/direction.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/entity.rs
Normal 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);
|
||||||
|
}
|
||||||
123
src/game.rs
Normal file
@@ -0,0 +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_HEIGHT, BOARD_WIDTH, RAW_BOARD};
|
||||||
|
use crate::direction::Direction;
|
||||||
|
use crate::entity::Entity;
|
||||||
|
use crate::map::Map;
|
||||||
|
use crate::pacman::Pacman;
|
||||||
|
|
||||||
|
pub struct Game<'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_creator: &'a TextureCreator<WindowContext>,
|
||||||
|
) -> Game<'a> {
|
||||||
|
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,
|
||||||
|
pacman: pacman,
|
||||||
|
debug: false,
|
||||||
|
map: map,
|
||||||
|
map_texture: texture_creator
|
||||||
|
.load_texture("assets/map.png")
|
||||||
|
.expect("Could not load pacman texture"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.map_texture, None, None)
|
||||||
|
.expect("Could not render texture on canvas");
|
||||||
|
|
||||||
|
// Render the pacman
|
||||||
|
self.pacman.render(self.canvas);
|
||||||
|
|
||||||
|
// Draw a grid
|
||||||
|
if self.debug {
|
||||||
|
for x in 0..BOARD_WIDTH {
|
||||||
|
for y in 0..BOARD_HEIGHT {
|
||||||
|
let tile = 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
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
173
src/main.rs
@@ -1,45 +1,172 @@
|
|||||||
use sdl2::pixels::Color;
|
use crate::constants::{WINDOW_HEIGHT, WINDOW_WIDTH};
|
||||||
use sdl2::event::Event;
|
use crate::game::Game;
|
||||||
use sdl2::image::LoadTexture;
|
use sdl2::event::{Event, WindowEvent};
|
||||||
use sdl2::keyboard::{Keycode, Mod};
|
use sdl2::keyboard::Keycode;
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
use crate::constants::{WINDOW_WIDTH, WINDOW_HEIGHT};
|
use tracing::event;
|
||||||
|
use tracing_error::ErrorLayer;
|
||||||
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
|
||||||
|
mod animation;
|
||||||
mod constants;
|
mod constants;
|
||||||
mod board;
|
mod direction;
|
||||||
|
mod entity;
|
||||||
mod game;
|
mod game;
|
||||||
|
mod map;
|
||||||
|
mod modulation;
|
||||||
|
mod pacman;
|
||||||
|
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
let sdl_context = sdl2::init().unwrap();
|
let sdl_context = sdl2::init().unwrap();
|
||||||
let video_subsystem = sdl_context.video().unwrap();
|
let video_subsystem = sdl_context.video().unwrap();
|
||||||
|
|
||||||
let window = video_subsystem.window("Pac-Man", WINDOW_WIDTH, WINDOW_HEIGHT)
|
// 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()
|
.position_centered()
|
||||||
.build()
|
.build()
|
||||||
.expect("Could not initialize window");
|
.expect("Could not initialize window");
|
||||||
|
|
||||||
let mut canvas = window.into_canvas().build().expect("Could not build canvas");
|
let mut canvas = window
|
||||||
let texture_creator= canvas.texture_creator();
|
.into_canvas()
|
||||||
|
.build()
|
||||||
|
.expect("Could not build canvas");
|
||||||
|
|
||||||
let map_texture = texture_creator.load_texture("assets/map.png").expect("Could not load pacman texture");
|
canvas
|
||||||
canvas.copy(&map_texture, None, None).expect("Could not render texture on canvas");
|
.set_logical_size(WINDOW_WIDTH, WINDOW_HEIGHT)
|
||||||
|
.expect("Could not set logical size");
|
||||||
|
|
||||||
let mut event_pump = sdl_context.event_pump().expect("Could not get SDL EventPump");
|
let texture_creator = canvas.texture_creator();
|
||||||
'main: loop {
|
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() {
|
for event in event_pump.poll_iter() {
|
||||||
match event {
|
match event {
|
||||||
Event::Quit { .. } |
|
Event::Window { win_event, .. } => match win_event {
|
||||||
Event::KeyDown { keycode: Some(Keycode::Q), .. } => {
|
WindowEvent::Hidden => {
|
||||||
break 'main;
|
event!(tracing::Level::DEBUG, "Window hidden");
|
||||||
}
|
shown = false;
|
||||||
event @ Event::KeyDown { .. } => {
|
}
|
||||||
println!("{:?}", event);
|
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),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
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());
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.present();
|
// TODO: Proper pausing implementation that does not interfere with statistic gathering
|
||||||
::std::thread::sleep(Duration::from_millis(10));
|
if !paused {
|
||||||
|
game.tick();
|
||||||
|
game.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if !main_loop() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/map.rs
Normal 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
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||