Phaser Games skill for Claude Code

HTML5 games that load clean and play on phones. Claude structures Phaser games into Boot, Preload, Game and UI scenes, loads every asset once with a progress bar, and wires Arcade Physics colliders and overlaps for exactly the pairs that need them. It pools projectiles, scales the canvas for any screen, handles touch input and mobile audio that stays locked until the first tap, and cleans up listeners so scene restarts do not leak.

  • namephaser-games
  • size223 lines · ≈2,400 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Builds Phaser web games with a Boot, Preload, Game, and UI scene structure, Arcade Physics bodies and colliders, Tiled tilemaps, texture atlases, object pools, responsive scaling, and touch-friendly mobile input with audio that starts after the first tap. Use when writing Phaser 3 or Phaser 4 scenes in JavaScript or TypeScript, when sprites fall through platforms, assets fail to load, the game blurs or misfits on phones, audio stays silent on iOS, or when packaging an HTML5 game for itch.io or a web portal.

Try asking

  • My player falls through the Tiled platforms after the first jump, fix the collisions
  • Add touch controls with a virtual joystick and jump button for mobile
  • Restarting the level makes the score update twice, find the leak

Use it when

  • Writing Phaser 3 or Phaser 4 scenes in JS or TypeScript
  • Sprites tunnel, stick or ignore collisions in Arcade Physics
  • A web game needs mobile touch, scaling or audio fixes
  • Packaging an HTML5 game for itch.io or a portal

Skip it when

  • 3D browser scenes with WebGL or WebGPU — use threejs-scenes
  • Engine-independent collision math and timestep design — use game-physics

How to check the result

The production build serves with no console errors or 404s, debug bodies line up with the art, and a real phone plays with touch and audio after the first tap.

The common mistake it prevents

Registering listeners on game-wide emitters without removing them on scene shutdown, so every restart doubles the callbacks.

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

--- name: phaser-games description: Builds Phaser web games with a Boot, Preload, Game, and UI scene structure, Arcade Physics bodies and colliders, Tiled tilemaps, texture… license: MIT ---

Done well means: scenes have one job each, every asset loads in a preload scene with a progress bar, physics objects collide on the intended pairs only, spawned objects come from pools, the canvas scales to any screen, and the game works with touch alone.

Ground rules

  • Check package.json for the Phaser major version. Phaser 3.90 and Phaser 4 share the Scene, Loader, Arcade Physics, Tilemap, and Input APIs used below. Phaser 4 replaced the WebGL renderer internals, so custom pipelines, post FX, and masks from Phaser 3 need porting; read the official migration notes before touching rendering code.
  • Start new projects from the official template CLI (npm create @phaserjs/game@latest) with Vite and TypeScript, unless the repo already has a build setup.
  • Keep game rules (scoring, inventory, wave logic) in plain TS modules with no Phaser imports, so they can be unit tested with Vitest.
  • Never put gameplay logic in preload(); never load assets in create() or update().

Workflow

- [ ] 1. Read the game config, scene list and asset folder layout
- [ ] 2. Locate the scene and objects involved; note physics groups and colliders
- [ ] 3. Implement; pool anything spawned more than a few times per second
- [ ] 4. Run with arcade debug: true to check bodies and collisions
- [ ] 5. Test on a phone (or device emulation + real touch) for scale, input, audio
- [ ] 6. Production build, serve dist/ locally, check console and network tab

Game config

import Phaser from 'phaser';
import { Boot } from './scenes/Boot';
import { Preload } from './scenes/Preload';
import { Game } from './scenes/Game';
import { UI } from './scenes/UI';

new Phaser.Game({
  type: Phaser.AUTO,
  parent: 'game',
  width: 960,
  height: 540,                          // design resolution; scaling handles the rest
  backgroundColor: '#14161f',
  pixelArt: true,                       // nearest-neighbour filtering for pixel art
  scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH },
  physics: { default: 'arcade', arcade: { gravity: { x: 0, y: 900 }, debug: false } },
  input: { activePointers: 3 },         // multi-touch: joystick + two buttons
  scene: [Boot, Preload, Game, UI],
});

Add touch-action: none and margin: 0 on the page and canvas container so mobile browsers do not scroll or zoom during play.

Scene structure

SceneJob
BootLoad the tiny assets the loading screen needs; set global settings
PreloadLoad everything else with a progress bar; create animations once
MenuTitle, settings; first user gesture unlocks audio
GameWorld, physics, player, enemies
UIHUD launched in parallel (this.scene.launch('UI')), unaffected by camera zoom
export class Preload extends Phaser.Scene {
  constructor() { super('Preload'); }

  preload() {
    const bar = this.add.rectangle(480, 270, 4, 24, 0xffffff);
    this.load.on('progress', (p: number) => { bar.width = 4 + 400 * p; });
    this.load.on('loaderror', (file: Phaser.Loader.File) => console.error('Failed', file.key, file.src));

    this.load.setPath('assets');
    this.load.atlas('sprites', 'sprites.png', 'sprites.json');
    this.load.tilemapTiledJSON('level1', 'maps/level1.json');
    this.load.image('tiles', 'maps/tiles.png');
    this.load.audio('jump', ['sfx/jump.ogg', 'sfx/jump.mp3']);
  }

  create() {
    this.anims.create({
      key: 'player-run',
      frames: this.anims.generateFrameNames('sprites', { prefix: 'run_', start: 0, end: 7, zeroPad: 2 }),
      frameRate: 12,
      repeat: -1,
    });
    this.scene.start('Menu');
  }
}
  • Animations and textures are global; create them once in Preload, not in every Game.create() (duplicate keys warn and leak).
  • Cross-scene communication: this.registry (game-wide data manager with change events) or the emitting scene’s events. Avoid importing scene instances into each other.
  • With Vite, put assets under public/assets/ and use relative paths; set base: './' in vite.config.ts so the build works from a subfolder (itch.io, portals).

Arcade Physics

create() {
  const map = this.make.tilemap({ key: 'level1' });
  const tileset = map.addTilesetImage('tiles', 'tiles');   // name in Tiled, texture key
  const ground = map.createLayer('Ground', tileset!, 0, 0)!;
  ground.setCollisionByProperty({ collides: true });

  this.player = this.physics.add.sprite(64, 64, 'sprites', 'idle_00');
  this.player.setCollideWorldBounds(true);
  this.player.body.setSize(14, 28).setOffset(9, 4);      // fit the body to the art

  this.coins = this.physics.add.staticGroup();
  this.physics.add.collider(this.player, ground);
  this.physics.add.overlap(this.player, this.coins, this.collectCoin, undefined, this);

  this.physics.world.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
  this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
  this.cameras.main.startFollow(this.player, true, 0.1, 0.1);
}

update() {
  const onFloor = this.player.body.blocked.down;
  const dir = (this.keys.right.isDown ? 1 : 0) - (this.keys.left.isDown ? 1 : 0);
  this.player.setVelocityX(dir * 180);
  if (Phaser.Input.Keyboard.JustDown(this.keys.jump) && onFloor) this.player.setVelocityY(-420);
}
NeedUse
Solid collision with separationphysics.add.collider(a, b)
Detection only (pickups, triggers, hitboxes)physics.add.overlap(a, b, cb)
Immovable level piecesstaticGroup(); call refreshBody() after scaling or moving one
Moving platformsDynamic body with setImmovable(true) and body.allowGravity = false
Rotated or polygon shapes, joints, stackingMatter.js physics instead of Arcade
  • Velocities are pixels per second; do not multiply them by delta. Use delta (milliseconds) only for your own manual movement or timers.
  • Fast projectiles tunnel through thin tiles. Raise arcade.fps (e.g. 120), make tiles or bodies thicker, or ray-test with Phaser.Geom.Intersects.
  • body.blocked.down is set by tile and world-bound collisions; body.touching.down by body-to-body collisions. Check both for “on ground” when standing on sprites.

Pooling

this.bullets = this.physics.add.group({ classType: Bullet, maxSize: 40, runChildUpdate: true });

fire(x: number, y: number) {
  const b = this.bullets.get(x, y) as Bullet | null;    // null when pool is exhausted
  if (!b) return;
  b.enableBody(true, x, y, true, true);
  b.setVelocityX(600);
}

// in Bullet.update(): when off-screen
this.disableBody(true, true);                            // returns it to the pool

Never destroy() and recreate objects every shot; it causes GC pauses on mobile.

Mobile input, scaling, and audio

  • Use pointer events (this.input.on('pointerdown', ...)) so mouse and touch share code. On-screen buttons: interactive images in the UI scene with setScrollFactor(0).
  • A virtual joystick needs a dedicated pointer: track pointer.id from pointerdown to pointerup, and size touch targets at least 48 CSS px.
  • Fullscreen must be requested inside a user gesture: call this.scale.startFullscreen() in a pointerup handler. iOS Safari on iPhone does not support element fullscreen; design for the browser chrome.
  • Audio is locked until the first user gesture. Start music after a tap on the menu, or wait for this.sound.once(Phaser.Sound.Events.UNLOCKED, ...) when this.sound.locked.
  • Handle this.game.events.on(Phaser.Core.Events.BLUR, ...) to pause when the tab hides.
  • Text blurs on high-DPI screens with FIT scaling; call text.setResolution(window.devicePixelRatio) or use bitmap fonts.
  • Lock or adapt orientation: listen to this.scale.on('orientationchange', ...) and show a “rotate device” overlay instead of a squashed game.

Scene lifecycle and leaks

  • Listeners on long-lived emitters (this.game.events, this.registry.events, window) survive scene restarts. Remove them on shutdown:
this.registry.events.on('changedata-score', this.onScore, this);
this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
  this.registry.events.off('changedata-score', this.onScore, this);
});
  • Scene-owned timers, tweens and display objects are cleaned automatically on restart.
  • Class fields keep values across this.scene.restart(); reset state in init().

Gotchas

  • this inside callbacks: pass the context argument (overlap(a, b, cb, undefined, this)) or use arrow functions.
  • Tiled: embed tilesets in the map (or load external ones separately) and export as JSON; the tileset name passed to addTilesetImage must match the name in Tiled.
  • Texture bleeding (lines between tiles) when the camera zooms: extrude tilesets by 1 px and set roundPixels: true in the render config.
  • Audio formats: ship OGG plus MP3 (or M4A) so every browser has a decodable file.
  • Changing a sprite’s scale does not resize its body until you call body.setSize or refreshBody for static bodies.

Verification

  1. npm run build, serve dist/ with a static server, and load it: console free of errors, no 404s in the network tab.
  2. Arcade debug: true pass: bodies match the art, colliders fire for the intended pairs.
  3. Test on a real phone in both orientations: touch controls, audio after first tap, no page scroll, pause on tab switch.
  4. Play for five minutes with the Performance panel recording: steady frame rate, no sawtooth memory growth from unpooled objects.
  5. Unit tests for the pure game-rule modules pass (npx vitest run).

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc