SHOOTER.PRG

ERA B · GAMES ◀ PREV CODE SHELF NEXT ▶
2021-02-14 → 2024-06-29 · 32 commits · solo · PRIVATE LOCAL · CODE→

WHAT IT IS.

A small Phaser 3 shooter I built to learn Phaser by reverse-engineering a single, very small idea: how does Galaga run so many waves with so little code? The answer turned out to be data, not code — a wave is a small set of curves, the curves are reused across stages, the enemies only need to know their position on a path and how long to hold it. The shooter's source is mostly that finding: level.js and paths.js carry the wave data and the path geometry, and gameEvents.js is the part that fires actions at scheduled times. The game itself is a 240×300 canvas (Phaser.Scale.FIT, width: 240, height: 300) — the small viewport is part of the study; it keeps the GPU budget honest so the wave logic has nowhere to hide.

I picked Galaga rather than Space Invaders because Galaga is the game where the wave is the design — the iconic enemy dives, the bonus-flag ship that splits off and rejoins, the formation-easing curves are the game. The shooter copies none of those directly. What I wanted to study was the engine underneath them: how few sprites you really need, how a path system can carry a hundred wave variations from a handful of Bezier curves, how the action scheduler that times enemy arrivals has to be its own module rather than buried in the scene. Everything else in the build — the parallax title screen, the joystix pixel font, the high-score strip — is scaffolding I added while I was in there.

THE ARC.

Two months of focused build, then three returns across three years. initial commit lands 2021-02-14 with one scene and a single enemy type. Two days of shot firing, refined collider, the alien-killing bullet, and then enemy ships moving around (with the first explosion.png). Working on paths (April 10, 2021) is the commit I went looking for: three docs/path_*.pdf files are checked in as the wave-geometry scratchwork — paths I drew by hand to convince myself that the same four curves could carry most of a stage. The following commits (paths, refactored, Single ship wave, Menu and gameover, Basic game working) turn that geometry into running code, with formation.js extracting the 48-enemy 6×8 grid setup. By April 11, 2021 the game plays end-to-end. The remaining 2021 commits are visual polish: parallax backdrop, aseprite-source art, the joystix monospace pixel font for the score strip.

Seventeen months of silence. December 2022 (Moved to parcel) is the first return: webpack, Babel and Jest all gone, art in art/.aseprite, the canvas resolution halved. January 2023 (Removed React, Updated to latest NPMs, Replaced sprites) is the second: the canvas game didn't need a UI tree, the sprite set is rotated to a Galaga-styled palette (art/galaga-.aseprite), and level.js becomes the file that owns the wave definitions. June 2024 is the third: Replaced parcel with esbuild is the bundler swap, then Refactoring shots and collision lifts paths.js out as its own module (163 lines), Accounting player lives adds the 252-line player-lives state, and the commit pattern that follows — a 177-line accounting commit, then a 3-line Fixing lives count — is the engineering move I came to trust: large rewrite followed by surgical delta. The closing commits (Refactored into distinct files, refactoring) split game.js into gameSetup.js, gameUpdate.js, gameEvents.js, gameScene.js, with coordinator.js as a reusable action scheduler (add_action(time, action) pushes [id, accumulated_ms, action], poll(delta) fires expired ones). 32 commits total, 9 of them on the wave/path layer.

Stack chips: Phaser 3.55 · esbuild · esbuild-copy-static-files · Parcel (past) · Webpack/Babel (past) · joystix monospace pixel font · aseprite source art.

WHAT I LEARNED.

  • A small canvas is a feature. Running at 240×300 with Phaser.Scale.FIT keeps the wave code honest — anything expensive shows up on screen at this resolution, and you can't hide bad logic behind a fast GPU.

  • The wave is data, not code. A small set of Bezier curves reused across stages carries more variation than a big set of scripts, and paths.js is the only file that has to grow as more waves are added. Everything else stays the same.

  • A scheduler for timed actions is its own module. The first version had enemy arrival buried in scene code; lifting it into coordinator.js (or gameEvents.js in the second iteration) made the wave timing legible in one place. The cost was one extra file; the payoff was the bug class where two timed events overlap going away.

  • Build systems are interchangeable; the game outlives them. Each of webpack → parcel → esbuild was a return-day refactor, not a rewrite — the gameplay state survived all three.

  • Returning to old code is cheap if the dependency surface stayed small. The whole build pipeline is now a 21-line esbuild-build.js, and the game's state lives in one Phaser scene. That combination is why the third return (June 2024) ended in a refactor rather than a re-implementation.

[ DEODORO ] > CODE / shooter EJECT → SHELF