A Twin-Stick-Shooter. In the browser. Zero build.
Geometry-Wars-Arcade mit 20K Partikeln, deterministischem 60Hz-Loop und Edge-Leaderboards. Pure ES Modules — jeder Fork läuft mit python -m http.server.
Why this exists.
Retro-Arcade-Fans und Gelegenheitsspieler wollen direkt im Browser — ohne Install, ohne Account, ohne Ladezeit — einen mechanisch tiefen Twin-Stick-Shooter mit Leaderboards, Daily Challenges und Cosmetics. Existierende Lösungen reichen nicht: Entweder fehlt die Persistenz (anonyme itch.io-Builds) oder die Performance (Canvas2D-Klone ruckeln bei zehntausenden Partikeln).
Shattergrid löst beides: Three.js-WebGL-Renderer mit pooled Particle-Systems, deterministischem Fixed-Timestep-Loop und einem schlanken Cloudflare-Backend für Scores, Daily-Seeds und Cosmetics — alles kostenlos im Free Tier.
The box it had to fit in.
How it runs.
Four deliberate choices.
Cloudflare Workers + D1 statt Node/Express + Postgres.
ES Modules + Import Maps statt Bundler.
js/lib/three/docker compose up in Nginx-Alpine, und Diff-Reviews zeigen echte Quelldateien, keine transpilierten Outputs. Jeder Fork läuft lokal mit python -m http.server.Singleton-Manager statt DI-Framework.
storageManager, statsTracker, achievementManager, cosmeticManager, leaderboardManager, accessibilityManagerStorageManager-Key sauber.Polymorpher Spawner statt Mode-Enum-Switch.
activeSpawner-Pattern: SpawnManager (Endless) und WaveManager (Survival) teilen ein Interfaceif (mode === 'wave')-Switches durchs game.jsThings that were not obvious.
Anti-Cheat ohne Server-Sim
SHA-256-Hash über (score, kills, survivalTime, maxCombo, maxMultiplier, timestamp, secret). Der Worker verifiziert den Hash, checkt pro-IP-Rate-Limit (30 s) und flaggt statistische Outlier (Score > 5× aktuellem p99 derselben Spielmodus-Klasse).Kein User-Account nötig, kein Replay-Stream — reicht für ein Hobby-Game, um 95 % der Fire-and-Forget-Cheater draußen zu halten, ohne die UX zu bremsen. Wer mehr will, kann immer noch den Secret aus dem JS extrahieren — aber dann ist der Score eben markiert und fliegt aus dem Leaderboard.
Dynamischer Soundtrack aus 4 Stems
StemPlayer lädt drums / bass / melody / atmosphere parallel, startet sie gleichzeitig am Audio-Clock, und blendet jeden Stem per GainNode ein, sobald eine intensity-Schwelle (berechnet aus enemyCount + combo + multiplier) erreicht wird.Edge-Case:
AudioContext muss nach User-Gesture resume()d werden (Browser-Policy). Fallback auf single bgmusic.mp3, wenn Stems fehlen oder decodeAudioData scheitert — das Spiel bleibt hörbar, auch wenn die dynamische Schicht down ist.Colorblind beim Enemy-Init
EnemyBase-Instanzierung über eine Lookup-Map (Protanopia / Deuteranopia / Tritanopia) — nicht pro Frame in einem Fragment-Shader.Spart GPU-Budget für Bloom / Chromatic-Aberration, und Partikel erben die neuen Farben automatisch, weil sie dem Parent-Color folgen. Reduced-Motion deaktiviert zusätzlich Shake + Bloom + Aberration und senkt Partikel-Dichte auf 25 %.
Fixed-Timestep mit Accumulator
requestAnimationFrame-Render. Auf 144-Hz-Monitoren rendert das Spiel 144 FPS, simuliert aber deterministisch 60 FPS.Konsequenz: Daily-Challenge-Seeds (
mulberry32-PRNG mit Datums-Seed) produzieren auf jeder Hardware exakt dieselben Spawn-Pattern — Speedruns und Daily-Rankings sind vergleichbar, egal ob 60- oder 240-Hz-Monitor.What's running.
What I learned.
Zero-Build ist Delivery-Feature.
Keine node_modules, keine Transpilation — das heißt auch: keine Supply-Chain-Sorgen, kein npm-Audit-Lärm, keine Lock-File-Merge-Konflikte. Ein statisches Repo, das per docker compose up in ein Nginx-Alpine-Image rollt. Bei der nächsten Hobby-Codebase bleibe ich bei ESM + Import Maps, solange es keine harten node_modules-Abhängigkeiten gibt.
Determinismus vor Skalierung.
Fixed-Timestep + PRNG-Seeds sind billig zu bauen und lösen auf einmal drei Probleme: Daily-Challenges, Speedrun-Fairness, Replay-Fähigkeit für später. Das war einer der günstigsten Architektur-Moves im Projekt und ist jetzt der erste Punkt auf meiner Checkliste für jedes zukünftige Game-Projekt.