Skip to content

Commit 4e85e17

Browse files
maxfridbeclaude
andcommitted
Add browser (WASM) build target with GitHub Pages demo
- build_web.sh: wasm32-unknown-unknown release build, version-matched wasm-bindgen-cli auto-install, packages target/web_dist static site - web/index.html: canvas host page (game name templated via {{GAME_NAME}}) - Cargo.toml: webgl2 bevy feature; getrandom wasm_js feature for the ahash-pulled getrandom 0.3 (plus RUSTFLAGS cfg in script and CI) - lib.rs: bind window to #game-canvas, fit_canvas_to_parent (wasm only) - CI: web job builds + zips the site into the release and deploys it to GitHub Pages as a live demo; setup_env.sh checks the wasm toolchain - README: demo link, web row/scripts/quick-start, load-bearing wasm notes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 37db0b7 commit 4e85e17

7 files changed

Lines changed: 166 additions & 3 deletions

File tree

.github/workflows/release.yml

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,62 @@ jobs:
8080
name: windows
8181
path: "*.zip"
8282

83+
web:
84+
needs: meta
85+
runs-on: ubuntu-latest
86+
steps:
87+
- uses: actions/checkout@v4
88+
- uses: dtolnay/rust-toolchain@stable
89+
with:
90+
targets: wasm32-unknown-unknown
91+
- uses: Swatinem/rust-cache@v2
92+
- name: Build
93+
env:
94+
RUSTFLAGS: --cfg getrandom_backend="wasm_js"
95+
run: cargo build --release --target wasm32-unknown-unknown
96+
- name: Install wasm-bindgen-cli (prebuilt, version-matched to Cargo.lock)
97+
run: |
98+
WBV=$(grep -A1 '^name = "wasm-bindgen"$' Cargo.lock | grep '^version' | cut -d'"' -f2)
99+
echo "wasm-bindgen crate version: $WBV"
100+
curl -sL "https://github.com/rustwasm/wasm-bindgen/releases/download/$WBV/wasm-bindgen-$WBV-x86_64-unknown-linux-musl.tar.gz" | tar xz
101+
sudo mv "wasm-bindgen-$WBV-x86_64-unknown-linux-musl/wasm-bindgen" /usr/local/bin/
102+
wasm-bindgen --version
103+
- name: Package
104+
run: |
105+
GAME='${{ needs.meta.outputs.game }}'
106+
V='${{ needs.meta.outputs.version }}'
107+
mkdir -p dist
108+
wasm-bindgen --no-typescript --target web \
109+
--out-dir dist --out-name "$GAME" \
110+
"target/wasm32-unknown-unknown/release/$GAME.wasm"
111+
sed "s/{{GAME_NAME}}/$GAME/g" web/index.html > dist/index.html
112+
(cd dist && zip -r "../$GAME-web-v$V.zip" .)
113+
- uses: actions/upload-artifact@v4
114+
with:
115+
name: web
116+
path: "*.zip"
117+
- uses: actions/upload-pages-artifact@v3
118+
with:
119+
path: dist
120+
121+
# Publishes the web build as a live demo at https://<user>.github.io/<repo>/
122+
pages:
123+
needs: web
124+
runs-on: ubuntu-latest
125+
permissions:
126+
pages: write
127+
id-token: write
128+
environment:
129+
name: github-pages
130+
url: ${{ steps.deployment.outputs.page_url }}
131+
steps:
132+
- name: Enable GitHub Pages (first run only)
133+
uses: actions/configure-pages@v5
134+
with:
135+
enablement: true
136+
- id: deployment
137+
uses: actions/deploy-pages@v4
138+
83139
android:
84140
needs: meta
85141
runs-on: ubuntu-latest
@@ -154,7 +210,7 @@ jobs:
154210
path: "*.apk"
155211

156212
release:
157-
needs: [meta, linux, windows, android]
213+
needs: [meta, linux, windows, android, web]
158214
runs-on: ubuntu-latest
159215
steps:
160216
- uses: actions/download-artifact@v4

Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ edition = "2024"
99
# android-native-activity -> required for the cargo-apk Android path
1010
# x11 -> required for native Linux windowing
1111
# multi_threaded -> desktop performance (ignored on wasm)
12+
# webgl2 -> required for the browser/WASM build
1213
bevy = { version = "0.15.2", default-features = false, features = [
1314
"android-native-activity",
1415
"bevy_asset",
@@ -31,6 +32,7 @@ bevy = { version = "0.15.2", default-features = false, features = [
3132
"png",
3233
"smaa_luts",
3334
"tonemapping_luts",
35+
"webgl2",
3436
"x11"
3537
] }
3638
# Embeds the assets/ folder into the binary/APK so no runtime file IO is
@@ -40,6 +42,13 @@ bevy_embedded_assets = "0.12"
4042
# Bevy's audio backend on Android (links against libc++_shared.so).
4143
cpal = { version = "0.15", features = ["oboe-shared-stdcxx"] }
4244

45+
# Browser build: ahash (via bevy) pulls getrandom 0.3, which refuses to
46+
# compile for wasm32-unknown-unknown unless the JS backend is enabled —
47+
# this feature plus RUSTFLAGS --cfg getrandom_backend="wasm_js" (set by
48+
# build_web.sh and the CI web job).
49+
[target.'cfg(target_arch = "wasm32")'.dependencies]
50+
getrandom = { version = "0.3", features = ["wasm_js"] }
51+
4352
# The library build is what Android consumes (cdylib for cargo-apk /
4453
# NativeActivity, staticlib for other embedding scenarios). The rlib lets
4554
# the desktop binary below reuse the same code.

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# GameBase
22

3-
A minimal Bevy (Rust) starter game that already ships to **Linux**, **Windows**, and **Android (APK)** from a single Linux dev machine. The build methodology was extracted from the Julian2 train game project.
3+
A minimal Bevy (Rust) starter game that already ships to **Linux**, **Windows**, **Android (APK)**, and the **browser (WASM)** from a single Linux dev machine. The build methodology was extracted from the Julian2 train game project.
4+
5+
**[▶ Play the demo in your browser](https://maxfridbe.github.io/GameBase/)** — deployed automatically to GitHub Pages by the release workflow.
46

57
The base game is a sphere you drive around a 3D plane:
68

@@ -28,6 +30,7 @@ src/main.rs -> tiny desktop wrapper: fn main() { game_base::run_game() }
2830
| Windows | cross-compile from Linux with MinGW (`x86_64-pc-windows-gnu`) | `target/windows_dist/` (exe + assets, zip and ship) |
2931
| Android A (**primary**) | `cargo apk` builds the **cdylib** into an APK per ABI; NativeActivity, no Java code at all | `target/{debug,release}/apk/*.apk` |
3032
| Android B (alternative) | `cargo ndk` drops the cdylib into `app/src/main/jniLibs/`, then Gradle wraps it with a Java `GameActivity` into one universal APK | `app/build/outputs/apk/debug/app-debug.apk` |
33+
| Browser (WASM) | compile the bin to `wasm32-unknown-unknown` (webgl2 feature), `wasm-bindgen` generates the JS glue, `web/index.html` hosts the canvas | `target/web_dist/` (static site — serve anywhere) |
3134

3235
Key load-bearing details (easy to lose, hard to rediscover):
3336

@@ -37,6 +40,7 @@ Key load-bearing details (easy to lose, hard to rediscover):
3740
- Two APKs are built on purpose in Path A: **x86_64** for the desktop emulator, **arm64-v8a** for real phones. Path B builds one fat APK containing both.
3841
- Emulator GPU emulation matters for wgpu/Vulkan: `swangle_indirect` (run_emulator.sh) is the most stable; `start_new_emulator.sh` is the SwiftShader/CPU fallback for hosts whose GPU driver crashes the emulator.
3942
- `game.env` centralizes the game identity + Android SDK paths; every script sources it.
43+
- Browser build details: `bevy_embedded_assets` means the `.wasm` is fully self-contained (no asset fetch issues on static hosts); `wasm-bindgen-cli` must exactly match the `wasm-bindgen` crate version in `Cargo.lock` (`build_web.sh` auto-installs the right one); `getrandom` 0.3 (pulled via ahash/bevy) needs the `wasm_js` feature **and** `RUSTFLAGS=--cfg getrandom_backend="wasm_js"` — both are wired in already; the window is bound to the `#game-canvas` element in `web/index.html`.
4044

4145
## Scripts
4246

@@ -46,6 +50,7 @@ Key load-bearing details (easy to lose, hard to rediscover):
4650
| `setup_env.sh` | Check, then install only what's missing: system packages (apt or dnf detected automatically), Rust + cross targets, MinGW-w64, cargo-apk/cargo-ndk, Android SDK/NDK 26, debug keystore |
4751
| `run_linux.sh [debug]` | Build + run natively on Linux |
4852
| `build_windows.sh` | Cross-compile Windows release, package exe + assets into `target/windows_dist/` |
53+
| `build_web.sh` | Build the browser version into `target/web_dist/` (compile to wasm, run wasm-bindgen, add `web/index.html`) |
4954
| `build_cargo_apk.sh` / `_debug.sh` | Path A: build emulator (x86_64) + phone (ARM64) APKs |
5055
| `deploy_cargo_apk.sh` | Install Path A APK to running emulator, launch, follow logcat |
5156
| `deploy_phone.sh` | Install Path A ARM64 APK to USB phone, launch, follow logcat |
@@ -67,6 +72,7 @@ Key load-bearing details (easy to lose, hard to rediscover):
6772
- `<game>-linux-x86_64-v<version>.tar.gz` (binary + assets)
6873
- `<game>-windows-x86_64-v<version>.zip` (exe + assets, MinGW cross-compiled)
6974
- `<game>-android-arm64-v<version>.apk` (phones) and `<game>-android-x86_64-v<version>.apk` (emulator)
75+
- `<game>-web-v<version>.zip` (static site) — the same build is also deployed to **GitHub Pages** as the live demo (first run: if the `pages` job fails, enable Pages once under repo Settings → Pages → Source: GitHub Actions)
7076

7177
Pushing again without bumping the version updates the existing release for that tag rather than creating a new one. CI signs APKs with a freshly generated debug keystore — replace that step with a real keystore (repo secret) before shipping to a store.
7278

@@ -77,6 +83,8 @@ Pushing again without bumping the version updates the existing release for that
7783
./setup_env.sh # once per machine; installs only the missing pieces (apt or dnf)
7884
./run_linux.sh # play on Linux
7985
./build_windows.sh # produce target/windows_dist/ for Windows
86+
./build_web.sh # produce target/web_dist/ for the browser
87+
python3 -m http.server -d target/web_dist 8080 # ...then play at localhost:8080
8088
./run_emulator.sh # boot the Android emulator...
8189
./build_cargo_apk_debug.sh && ./deploy_cargo_apk.sh # ...and play in it
8290
./build_cargo_apk.sh && ./deploy_phone.sh # play on a USB phone
@@ -114,6 +122,8 @@ GRADLE_PACKAGE="org.yourstudio.my_game" # = app/build.gradle applicationId
114122

115123
**3. App icon** — replace `assets/android-res/mipmap-mdpi/ic_launcher.png` (used by both Android paths).
116124

125+
**3b. Demo link** — the browser demo deploys to `https://<your-user>.github.io/<your-repo>/`; update the link at the top of this README. (`web/index.html` needs no changes — `build_web.sh` fills in the game name.)
126+
117127
**4. Only if you use Path B (Gradle):**
118128

119129
- `app/build.gradle``namespace` and `applicationId`
@@ -142,4 +152,5 @@ sed -i "s/Game Base/$NEWLABEL/g" Cargo.toml src/lib.rs
142152
| Android NDK | 26.1.10909125 | referenced by every script + setup |
143153
| AGP / Gradle | 8.4.0 / 8.6 | Path B only |
144154
| games-activity | 4.4.0 | must stay compatible with bevy's `android-activity` crate |
155+
| wasm-bindgen-cli | = `wasm-bindgen` in Cargo.lock | hard requirement; `build_web.sh`/CI resolve it automatically |
145156
| minSdk / target/compileSdk | 30 / 33 / 34 | Path B only; cargo-apk defaults handle Path A |

build_web.sh

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/bin/bash
2+
# Build the browser (WASM) version: compile to wasm32-unknown-unknown, run
3+
# wasm-bindgen to generate the JS glue, and package a static site folder.
4+
# Assets are embedded in the .wasm via bevy_embedded_assets, so the output
5+
# is fully self-contained and works from any static file host.
6+
cd "$(dirname "$0")"
7+
source ./game.env
8+
set -e
9+
10+
echo "=== Preparing WASM Build Environment ==="
11+
rustup target add wasm32-unknown-unknown
12+
13+
echo "=== Building for wasm32-unknown-unknown (Release) ==="
14+
# getrandom needs to be told to use the browser's crypto API (see Cargo.toml)
15+
export RUSTFLAGS="${RUSTFLAGS:+$RUSTFLAGS }--cfg getrandom_backend=\"wasm_js\""
16+
cargo build --release --target wasm32-unknown-unknown
17+
18+
# wasm-bindgen-cli MUST match the wasm-bindgen crate version in Cargo.lock.
19+
WBV=$(grep -A1 '^name = "wasm-bindgen"$' Cargo.lock | grep '^version' | cut -d'"' -f2)
20+
if ! command -v wasm-bindgen &>/dev/null || [ "$(wasm-bindgen --version | awk '{print $2}')" != "$WBV" ]; then
21+
echo "Installing wasm-bindgen-cli $WBV (must match the wasm-bindgen crate)..."
22+
cargo install wasm-bindgen-cli --version "$WBV" --locked --force
23+
fi
24+
25+
DIST_DIR="target/web_dist"
26+
echo "Packaging into $DIST_DIR..."
27+
rm -rf "$DIST_DIR"
28+
mkdir -p "$DIST_DIR"
29+
30+
wasm-bindgen --no-typescript --target web \
31+
--out-dir "$DIST_DIR" --out-name "$GAME_NAME" \
32+
"target/wasm32-unknown-unknown/release/$GAME_NAME.wasm"
33+
34+
sed "s/{{GAME_NAME}}/$GAME_NAME/g" web/index.html > "$DIST_DIR/index.html"
35+
36+
echo ""
37+
echo "Done! The web build is ready in $DIST_DIR"
38+
echo " Size: $(ls -lh "$DIST_DIR/${GAME_NAME}_bg.wasm" | awk '{print $5}') (wasm)"
39+
echo "Try it locally: python3 -m http.server -d $DIST_DIR 8080"
40+
echo "Then open: http://localhost:8080"

setup_env.sh

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,15 @@ else
6161
bad "rust target x86_64-pc-windows-gnu"
6262
fi
6363

64-
echo "=== 4. Android APK builds (build_cargo_apk*.sh / buildanddeploy.sh) ==="
64+
echo "=== 4. Browser/WASM build (build_web.sh) ==="
65+
if have rustup && rustup target list --installed 2>/dev/null | grep -q wasm32-unknown-unknown; then
66+
ok "rust target wasm32-unknown-unknown"
67+
else
68+
bad "rust target wasm32-unknown-unknown"
69+
fi
70+
have wasm-bindgen && ok "wasm-bindgen-cli" || bad "wasm-bindgen-cli (build_web.sh auto-installs the version matching Cargo.lock)"
71+
72+
echo "=== 5. Android APK builds (build_cargo_apk*.sh / buildanddeploy.sh) ==="
6573
for t in aarch64-linux-android x86_64-linux-android; do
6674
if have rustup && rustup target list --installed 2>/dev/null | grep -q "$t"; then
6775
ok "rust target $t"
@@ -122,10 +130,13 @@ fi
122130

123131
echo "Adding cross-compile targets to Rust..."
124132
rustup target add x86_64-pc-windows-gnu
133+
rustup target add wasm32-unknown-unknown
125134
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android
126135

127136
have cargo-apk || cargo install cargo-apk
128137
have cargo-ndk || cargo install cargo-ndk
138+
# build_web.sh re-installs the exact version matching Cargo.lock if needed
139+
have wasm-bindgen || cargo install wasm-bindgen-cli
129140

130141
# Android SDK/NDK Setup
131142
if [ ! -d "$ANDROID_HOME/ndk/$NDK_VERSION" ] || [ ! -x "$ANDROID_HOME/platform-tools/adb" ]; then

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ pub fn run_game() {
4444
.add_plugins(DefaultPlugins.set(WindowPlugin {
4545
primary_window: Some(Window {
4646
title: "Game Base".into(),
47+
// Browser build: render into the canvas provided by
48+
// web/index.html and track its CSS size. Ignored on native.
49+
canvas: Some("#game-canvas".into()),
50+
fit_canvas_to_parent: true,
4751
..default()
4852
}),
4953
..default()

web/index.html

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>{{GAME_NAME}}</title>
7+
<style>
8+
html, body { margin: 0; padding: 0; height: 100%; background: #0a0d17; overflow: hidden; }
9+
canvas { display: block; width: 100%; height: 100%; outline: none; }
10+
#loading {
11+
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
12+
color: #cbd3e8; font: 16px system-ui, sans-serif; pointer-events: none;
13+
}
14+
</style>
15+
</head>
16+
<body>
17+
<div id="loading">Loading&hellip;</div>
18+
<canvas id="game-canvas"></canvas>
19+
<script type="module">
20+
import init from './{{GAME_NAME}}.js';
21+
init()
22+
.catch((error) => {
23+
// Bevy uses an exception to hand control to the browser's
24+
// event loop on startup; it is not a real error.
25+
if (!error.message.startsWith("Using exceptions for control flow,")) {
26+
throw error;
27+
}
28+
})
29+
.finally(() => document.getElementById('loading').remove());
30+
</script>
31+
</body>
32+
</html>

0 commit comments

Comments
 (0)