| name | phaser-asset-pipeline |
| description | This skill should be used when setting up or changing the build-time asset pipeline of a Phaser 4 game — packing raw art in assets/src into public/assets (free-tex-packer-core texture atlases, audiosprite audio sprites, BMFont bitmap fonts) and the codegen step that emits typed keys to src/assets.ts so a missing or renamed key is a compile error. Use it when adding source art, wiring packing into the build, regenerating typed keys, or eliminating raw string asset keys. Pairs with the official loading-assets skill, phaser-build-deploy, and phaser-services. |
Phaser 4 Asset Pipeline
Overview
Runtime assets are generated, not hand-placed. Raw source art lives in
assets/src/** (PNGs, audio, font sources); a build step packs it into
public/assets/** (atlases, audio sprites, bitmap fonts) and codegens typed
key constants into src/assets.ts. The payoff: a missing or renamed asset is a
compile error, not a green square or silent-missing audio at runtime. No raw
string asset / scene / event keys anywhere — they all come from the generated
module (the official loading-assets skill covers loading those keys at runtime).
Layout
| Path | Role |
|---|
assets/src/sprites/** | Raw individual PNGs — the art source of truth |
assets/src/audio/** | Raw audio clips (wav/ogg sources) |
assets/src/fonts/** | Font sources for BMFont generation |
public/assets/** | Generated packed output (atlases, audiosprites, fonts, pack.json) — git-ignored or committed as a build artifact |
src/assets.ts | Generated typed key constants — never edited by hand |
scripts/pack-assets.mjs | The pipeline driver (runs the three packers + codegen) |
Step 1: texture atlases with free-tex-packer-core
free-tex-packer-core packs many PNGs into atlas pages plus a JSON manifest, in
a Node script (no GUI, runs in CI):
import { packAsync } from "free-tex-packer-core";
import { glob } from "glob";
import { readFile, writeFile } from "node:fs/promises";
const images = await Promise.all(
(await glob("assets/src/sprites/**/*.png")).map(async p => ({
path: p.replace("assets/src/sprites/", ""), contents: await readFile(p),
})));
const files = await packAsync(images, {
textureName: "game", width: 2048, height: 2048,
padding: 2, allowRotation: false, allowTrim: true,
exporter: "JsonHash", removeFileExtension: true,
});
for (const f of files) await writeFile(`public/assets/atlases/${f.name}`, f.buffer ?? f.);
The frame names in the manifest become the atlas frame keys you reference as
generated constants. Atlas everything that renders together — loose images break
sprite batching (the official loading-assets skill).
Step 2: audio sprites with audiosprite
audiosprite concatenates short SFX into one file (in multiple codecs) plus a
JSON map of { start, end } per clip — one request, one decode, instead of dozens:
"pack:audio": "audiosprite --output public/assets/audio/sfx --export ogg,m4a --format howler2 assets/src/audio/*.wav"
Provide at least two codecs (ogg + m4a) so every browser can play one. Load
with this.load.audioSprite(Audio.Sfx, "audio/sfx.json") and play by sprite key.
Step 3: bitmap fonts with BMFont
BitmapText is the performant choice for high-churn text (scores, timers) —
the official loading-assets skill / [[phaser-i18n]]. Generate the .fnt (XML/JSON) + PNG page
from a font source with a BMFont tool (msdf-bmfont-xml or the bmfont CLI) in
the same script:
"pack:font": "msdf-bmfont -o public/assets/fonts/ui --font-size 42 --texture-size 1024 1024 assets/src/fonts/ui.ttf"
Load with this.load.bitmapFont(Font.UI, "fonts/ui.png", "fonts/ui.fnt").
Step 4: codegen typed keys into src/assets.ts
After packing, walk the generated manifests and emit as const key maps. This is
the step that turns runtime failures into compile failures:
const atlas = JSON.parse(await readFile("public/assets/atlases/game.json", "utf8"));
const frames = Object.keys(atlas.frames);
const sfx = Object.keys(JSON.parse(await readFile("public/assets/audio/sfx.json","utf8")).spritemap);
const out = `// AUTO-GENERATED by scripts/pack-assets.mjs — do not edit.
export const Tex = { GameAtlas: "game" } as const;
export const Frame = { ${frames.map(k => `${toIdent(k)}: "${k}"`).join(", ")} } as const;
export const Audio = { Sfx: "sfx" } as const;
export const Sfx = { ${sfx.map(s => `${toIdent(s)}: "${s}"`).join(", ")} } as const;
export const Font = { UI: "ui" } as const;
export const SceneKeys = { Boot:"Boot", Preloader:"Preloader", MainMenu:"MainMenu", Game:"Game" } as const;
export const GameEvent = { ScoreChanged:"score-changed", EnemyDied:"enemy-died" } as const;
`;
await writeFile("src/assets.ts", out);
Scenes import Tex, Frame, Sfx, Font, SceneKeys, GameEvent — never an
inline string. Rename art, re-run the pipeline, and every now-stale reference
fails bun run typecheck immediately. (Scene/event keys are kept in the same
generated module so they share the no-raw-string discipline; edit them via the
codegen template, not by hand.)
Step 5: wire into the build
The pipeline runs before dev and before build so generated output is never stale:
"scripts": {
"assets": "node scripts/pack-assets.mjs",
"predev": "bun run assets",
"prebuild": "bun run assets",
"dev": "vite",
"build": "vite build"
}
For fast iteration, a --watch mode on the pack script re-packs changed source
art. In CI the pack step runs once before vite build; the build then hashes the
generated files for immutable caching ([[phaser-build-deploy]]).
Project conventions
assets/src/** is the source of truth; public/assets/** and src/assets.ts
are generated — treat them as build output (re-runnable, not hand-edited).
- One atlas per render group, one audiosprite per SFX set, BMFont for hot text.
- A contract test asserts every constant in
src/assets.ts resolves to an entry
in the generated pack.json/manifests ([[phaser-testing]]).
- The generated
src/assets.ts is the single source of asset/scene/event keys —
the no-raw-string-keys rule depends on it existing.
Verification
The pipeline is verified by deleting a source PNG and re-running bun run assets:
the corresponding generated constant disappears and bun run typecheck fails at
every use site (proving keys are compile-checked). Then boot the game and confirm
the atlas/audiosprite/font load with no FILE_LOAD_ERROR and render/play
correctly.