Orb Runner: a complete 3D arcade game in ~150 lines of GDScript, world built in
code so there are no binary scenes to merge-conflict on. Ships the non-threaded
export (runs everywhere incl. iOS Safari); flip thread_support in
export_presets.cfg for the threaded build, which hanzo.app already serves the
COOP/COEP for when the project declares framework=godot.
The export basename is fingerprinted (orb-<sha8>.html) because every sibling
asset inherits it — that is what makes index.js/.wasm/.pck cacheable forever
instead of stale-forever.
Why this exists: static-SPA builders cannot ship a game. Proving hanzo.app can
meant finding what actually stopped it, and three things did:
1. POST /v1/projects/:slug/deploy carries the entire build in ONE request body and
the public edge caps a body at 4 MiB (measured: 4194304 accepted, 4198400 gets
the opaque fasthttp "Error when parsing request"). Godot emits a 43 MB engine
wasm; Unity 22 MB + 5.7 MB of data. The artifact lane cannot express a game at
all, so these ship through the release lane instead: stage to the object store,
then POST /v1/sites/:slug/publish promotes it server-side with no body.
2. Engine loaders emit NON-fingerprinted filenames (index.js, <name>.loader.js), and
clients/sites.CacheControlFor only marks a content-hashed name immutable. A
redeploy therefore leaves the CDN and the browser running the OLD loader against
the NEW wasm. That is not a visible error: emscripten just prints "still waiting
on run dependencies" forever and the canvas stays 300x150. Cost hours to find.
Every template here fingerprints its own asset names so the case cannot recur.
3. A cross-origin-isolated document (COEP: require-corp) BLOCKS a plain cross-origin
script, so the shared a.hanzo.ai runtime needs crossorigin="anonymous" on exactly
the pages that opt into isolation — game pages.
Co-authored-by: Hanzo Dev <[email protected]>
2.4 KiB
Orb Runner — Godot 4 web export
A complete 3D arcade game in ~150 lines of GDScript, exported to WebAssembly. Roll the sphere, collect orbs, don't fall off the arena. The whole world is built in code, so the repo has no binary scenes to merge-conflict on.
Live: https://godot-arcade.hanzo.app
Build
Needs Godot 4.4 + the matching export templates (~/.local/share/godot/export_templates/4.4.stable).
godot --headless --import .
# fingerprint the export basename: every sibling asset inherits it, so index.js/.wasm/.pck
# become content-addressed names the CDN can cache forever without ever going stale
V=$(cat world.gd main.tscn project.godot | sha256sum | cut -c1-8)
godot --headless --export-release "Web" build/orb-$V.html
cp build/orb-$V.html build/index.html
export_presets.cfg ships thread_support=false: the non-threaded export runs on every
browser including iOS Safari. Flip it to true for the threaded build — hanzo.app already
serves the COOP/COEP that SharedArrayBuffer requires when the project declares
"framework": "godot".
Deploy to hanzo.app
An engine build is far bigger than the 4 MiB the public edge accepts in one request body, so games ship through the release lane, not the artifact lane: put the built tree in your object space, then publish it server-side.
# 1. declare the engine — `unity`, `unreal`, `godot` make clients/sites serve the
# site cross-origin-isolated (COOP/COEP) so multithreaded WASM gets SharedArrayBuffer
curl -X POST https://api.hanzo.ai/v1/projects -H "Authorization: Bearer $TOKEN" \
-H "X-Org-Id: $ORG" -H "Content-Type: application/json" \
-d "{\"slug\":\"my-game\",\"name\":\"my-game\",\"framework\":\"godot\"}"
# 2. stage the build to s3.hanzo.ai under <org>/_stage/my-game/
# 3. promote it into an immutable content-addressed release + flip the pointer
curl -X POST https://api.hanzo.ai/v1/sites/my-game/publish -H "Authorization: Bearer $TOKEN" \
-H "X-Org-Id: $ORG" -H "Content-Type: application/json" -d "{\"source\":\"_stage/my-game\"}"
Fingerprint your asset names. The site server serves a content-hashed filename
immutable and everything else with a plain max-age. A redeploy that reuses the same
asset name leaves browsers and the CDN running the OLD loader against the NEW binary —
for a WASM app that is a silent, permanent hang with no error, not a visible failure.
License
MIT (this game). Godot Engine is MIT, (c) the Godot Engine contributors.