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.

  • Game Development
  • $phaser-games
  • 205 lines
  • 2 files
  • ~2,200 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- phaser-games

Installs to ~/.agents/skills/phaser-games/ for every project on this machine.

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.

~/.agents/skills/phaser-games/SKILL.md

--- name: phaser-games description: Builds 2D browser games with Phaser 3 and Phaser 4 - game config and scaling, scene lifecycle and transitions, asset preloading and atlases, Arcade… license: MIT ---

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

  1. Read AGENTS.md and package.json. The phaser major version decides the API surface: 3.x (3.90 is the final v3 line) or 4.x (stable since April 2026).
  2. 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.
  3. 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.FIT keeps aspect ratio with letterboxing.
  • Toggle arcade.debug from a URL flag during development to see bodies.

Scene structure

SceneJob
BootLoad everything (or a loading bar plus everything), create global animations, then this.scene.start("Menu")
MenuTitle, options
PlayWorld, physics, gameplay
HudLaunched 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) then preload() then create(); update(time, delta) every frame.
  • this.scene.start(key, data) stops the current scene; this.scene.restart(data) reruns init/create on the same instance, so class fields keep stale values unless reset in init.
  • Scene-owned systems (this.time, this.tweens, this.input, display list) are cleared on shutdown. Listeners you add to this.game.events, this.registry.events, window or document are not; remove them on SHUTDOWN.

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 .tsx and load it), keep the tileset name in Tiled identical to the first argument of addTilesetImage, and add 1px extrusion to tilesets to avoid seams when the camera moves at fractional positions.

Arcade Physics decisions

NeedUse
Solid collision with separationphysics.add.collider(a, b, cb?)
Overlap detection only (pickups, hitboxes)physics.add.overlap(a, b, cb)
Many same-type objectsPhysics group with maxSize; get() / killAndHide()
Moving platformsImmovable body, body.setImmovable(true) and allowGravity = false
Slopes, rotation, joints, stackingMatter.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.down is set by tile and world-bound collisions; body.touching.down by other bodies. Ground checks usually need blocked.down || touching.down.
  • Resize bodies with body.setSize(w, h) and body.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 3Phaser 4
preFX / postFX, masksUnified Filters (obj.filters.internal, filters.external)
BitmapMaskMask filter via filters.internal.addMask(...)
Custom WebGL pipelines, setPipeline("Light2D")Render nodes; setLighting(true)
setTintFill(c)setTint(c).setTintMode(Phaser.TintModes.FILL)
Geom.PointPhaser.Math.Vector2
Math.TAU (was PI/2), Math.PI2Math.TAU is now 2 PI; PI2 removed
Mesh, Plane, 3D camera pluginsRemoved
roundPixels default trueDefault false; set it or use per-object vertexRoundMode
RenderTexture draws immediateBuffered; 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 delta incorrectly: Arcade velocities are already per second; only manual movement needs * delta / 1000.
  • setCollisionByProperty needs the property on tiles in the tileset, not on the layer.
  • this.input.keyboard is 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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem