Phaser Games skill for Codex
Clean scenes, pooled sprites, crisp pixels. Codex checks which Phaser major the project uses, then structures the game as Boot, Menu, Play and parallel HUD scenes, loads atlases once, runs gameplay on pooled Arcade Physics groups and wires Tiled maps with property-based collision. It removes listeners on scene shutdown so restarts do not leak, fixes blurry pixel art at the config level, and maps v3 FX, pipelines and tint calls to their v4 replacements.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- phaser-games Installs to ~/.agents/skills/phaser-games/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install phaser-games The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project phaser-games Installs to .agents/skills/phaser-games/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/phaser-games.zipunzip -o phaser-games.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) phaser-games PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $phaser-games, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds 2D browser games with Phaser 3 and Phaser 4 - game config and scaling, scene lifecycle and transitions, asset preloading and atlases, Arcade Physics bodies, colliders and groups, Tiled tilemaps, animations, input, object pooling and Vite builds - and handles the Phaser 3 to 4 migration (filters replacing FX and pipelines, tint modes, Vector2). Use when starting or extending a Phaser game, wiring a level from Tiled, fixing collisions that tunnel or stick, leaking listeners between scene restarts, blurry pixel art, or upgrading phaser in package.json.
Use it when
- Building or extending a 2D browser game on Phaser
- Loading levels from Tiled and setting up tile collisions
- Scene restarts leak listeners or the game slows down over time
- Moving a project from Phaser 3 to Phaser 4
Use something else when
- You want a rendering library without a game framework: use pixijs-graphics
- The game is 3D in the browser: use babylonjs
How to check the result
npm run build succeeds, every scene plays, three level restarts leave listener and object counts unchanged, and window resizes keep pixels sharp.
The mistake it prevents
Adding listeners to game.events or the registry inside a scene and never removing them on SHUTDOWN, so each restart doubles the handlers.
Done well means: each screen is its own Scene with clean startup and shutdown, assets load once in a boot or preload scene, gameplay runs on Arcade Physics with pooled objects, the canvas scales to any window without blur, and restarting a level leaks no listeners, timers or tweens.
Before writing code
- Read
AGENTS.mdandpackage.json. Thephasermajor version decides the API surface: 3.x (3.90 is the final v3 line) or 4.x (stable since April 2026). - Check the build: most projects use the official Vite template (
npm run dev,npm run build). Installing packages needs network; ask if the sandbox blocks it. - List scenes (
src/scenes/), the asset folder (public/assets/) and any Tiled maps (.tmj/.json).
Game config
import Phaser from "phaser";
import { Boot } from "./scenes/Boot";
import { Play } from "./scenes/Play";
import { Hud } from "./scenes/Hud";
new Phaser.Game({
type: Phaser.AUTO,
parent: "game",
width: 480, // design resolution; scale manager fits it to the window
height: 270,
pixelArt: true, // nearest-neighbour sampling for crisp pixels
backgroundColor: "#1d1f27",
scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH },
physics: { default: "arcade", arcade: { gravity: { x: 0, y: 900 }, debug: false } },
scene: [Boot, Play, Hud],
});
- Pick a small design resolution for pixel art (320x180, 480x270, 640x360) so integer
scaling to 1080p works;
Phaser.Scale.FITkeeps aspect ratio with letterboxing. - Toggle
arcade.debugfrom a URL flag during development to see bodies.
Scene structure
| Scene | Job |
|---|---|
Boot | Load everything (or a loading bar plus everything), create global animations, then this.scene.start("Menu") |
Menu | Title, options |
Play | World, physics, gameplay |
Hud | Launched in parallel with this.scene.launch("Hud"); reads state through events |
export class Play extends Phaser.Scene {
private player!: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
private bullets!: Phaser.Physics.Arcade.Group;
private cursors!: Phaser.Types.Input.Keyboard.CursorKeys;
constructor() { super("Play"); }
init(data: { level: number }) { this.registry.set("level", data.level ?? 1); }
create() {
const map = this.make.tilemap({ key: "level1" });
const tiles = map.addTilesetImage("dungeon", "tiles")!; // name as in Tiled, texture key
const ground = map.createLayer("Ground", tiles)!;
ground.setCollisionByProperty({ collides: true });
this.player = this.physics.add.sprite(64, 64, "hero", 0);
this.player.setCollideWorldBounds(true);
this.physics.add.collider(this.player, ground);
this.bullets = this.physics.add.group({ maxSize: 40, allowGravity: false });
this.cursors = this.input.keyboard!.createCursorKeys();
this.cameras.main.startFollow(this.player, true, 0.1, 0.1);
this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
this.physics.world.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
this.events.once(Phaser.Scenes.Events.SHUTDOWN, this.cleanup, this);
}
update(_time: number, delta: number) {
const speed = 160;
const body = this.player.body;
body.setVelocityX(this.cursors.left.isDown ? -speed : this.cursors.right.isDown ? speed : 0);
if (Phaser.Input.Keyboard.JustDown(this.cursors.up) && body.blocked.down) body.setVelocityY(-330);
}
fire(x: number, y: number, vx: number) {
const b = this.bullets.get(x, y, "bullet") as Phaser.Physics.Arcade.Image | null;
if (!b) return; // pool exhausted
b.setActive(true).setVisible(true);
b.body!.reset(x, y);
b.setVelocityX(vx);
this.time.delayedCall(800, () => this.bullets.killAndHide(b));
}
private cleanup() {
// remove listeners this scene added to objects that outlive it (game.events, registry)
this.registry.events.off("changedata-score", undefined, this);
}
}
Lifecycle rules
init(data)thenpreload()thencreate();update(time, delta)every frame.this.scene.start(key, data)stops the current scene;this.scene.restart(data)rerunsinit/createon the same instance, so class fields keep stale values unless reset ininit.- Scene-owned systems (
this.time,this.tweens,this.input, display list) are cleared on shutdown. Listeners you add tothis.game.events,this.registry.events,windowordocumentare not; remove them onSHUTDOWN.
Loading assets
preload() {
const bar = this.add.rectangle(240, 135, 200, 6, 0xffffff).setScale(0, 1);
this.load.setPath("assets");
this.load.atlas("hero", "hero.png", "hero.json"); // TexturePacker / free-tex-packer
this.load.image("tiles", "tiles/dungeon.png");
this.load.tilemapTiledJSON("level1", "maps/level1.tmj");
this.load.audio("jump", ["sfx/jump.ogg", "sfx/jump.mp3"]);
this.load.on("progress", (p: number) => bar.setScale(p, 1));
}
- Pack frames into atlases; hundreds of loose images cost draw calls and requests.
- Create animations once (Boot) with
this.anims.create; they are global to the game. - Audio cannot play before the first user gesture in browsers; Phaser unlocks on first input, so put a “click to start” on the menu.
- Tiled: embed tilesets in the map (or ship the
.tsxand load it), keep the tileset name in Tiled identical to the first argument ofaddTilesetImage, and add 1px extrusion to tilesets to avoid seams when the camera moves at fractional positions.
Arcade Physics decisions
| Need | Use |
|---|---|
| Solid collision with separation | physics.add.collider(a, b, cb?) |
| Overlap detection only (pickups, hitboxes) | physics.add.overlap(a, b, cb) |
| Many same-type objects | Physics group with maxSize; get() / killAndHide() |
| Moving platforms | Immovable body, body.setImmovable(true) and allowGravity = false |
| Slopes, rotation, joints, stacking | Matter.js (physics.default: "matter"), not Arcade |
- Fast bullets tunnel through thin tiles. Fix with thicker collision tiles, lower speeds,
a raycast check, or by increasing
arcade.fps(costs CPU). body.blocked.downis set by tile and world-bound collisions;body.touching.downby other bodies. Ground checks usually needblocked.down || touching.down.- Resize bodies with
body.setSize(w, h)andbody.setOffset(x, y)to match art, not the frame.
Phaser 3 to 4 migration
Standard sprites, text, tilemaps, cameras (scrollX, zoom) and scenes mostly carry over.
Plan work for:
| Phaser 3 | Phaser 4 |
|---|---|
preFX / postFX, masks | Unified Filters (obj.filters.internal, filters.external) |
BitmapMask | Mask filter via filters.internal.addMask(...) |
Custom WebGL pipelines, setPipeline("Light2D") | Render nodes; setLighting(true) |
setTintFill(c) | setTint(c).setTintMode(Phaser.TintModes.FILL) |
Geom.Point | Phaser.Math.Vector2 |
Math.TAU (was PI/2), Math.PI2 | Math.TAU is now 2 PI; PI2 removed |
Mesh, Plane, 3D camera plugins | Removed |
roundPixels default true | Default false; set it or use per-object vertexRoundMode |
| RenderTexture draws immediate | Buffered; call render() |
Read the migration guide shipped in the package (node_modules/phaser/changelog/v4/) for
the full list. Upgrade on a branch, fix type errors, then play every scene.
Gotchas
- Blurry pixel art: missing
pixelArt: true, a non-integer camera zoom, or a CSS size that is not a whole multiple of the design resolution. - Using
deltaincorrectly: Arcade velocities are already per second; only manual movement needs* delta / 1000. setCollisionByPropertyneeds the property on tiles in the tileset, not on the layer.this.input.keyboardis null when the keyboard plugin is disabled; guard or assert.- Large worlds: call
setScrollFactor(0)on HUD objects or put them in the parallel Hud scene.
Verification
npm run build # TypeScript and bundling must succeed
npm run dev # play every scene; restart the level 3 times
- After three restarts,
this.game.events.listenerCount(...)and object counts are unchanged. - Arcade debug on: bodies line up with sprites and tiles.
- Resize the browser window and toggle fullscreen: no blur, no stretched aspect.
- If you cannot open a browser here, say so and give the user this checklist.
Hand-off summary
**Phaser:** 3.90 (no v4 APIs used) | Vite build passing
**Scenes:** Boot (atlas + anims), Play (Tiled level1, pooled bullets max 40), Hud (parallel)
**Fixes:** listener leak on restart (registry events removed on SHUTDOWN); bullet tunnelling via thicker wall tiles
**Manual test:** level restart x3, window resize, fullscreen What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Phaser Games"
short_description: "Clean scenes, pooled sprites, crisp pixels"
default_prompt: "$phaser-games Make a Phaser platformer level from this Tiled map with collectible coins" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.