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.
- name
phaser-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.
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.jsonfor 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 increate()orupdate().
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
| Scene | Job |
|---|---|
Boot | Load the tiny assets the loading screen needs; set global settings |
Preload | Load everything else with a progress bar; create animations once |
Menu | Title, settings; first user gesture unlocks audio |
Game | World, physics, player, enemies |
UI | HUD 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’sevents. Avoid importing scene instances into each other. - With Vite, put assets under
public/assets/and use relative paths; setbase: './'invite.config.tsso 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);
}
| Need | Use |
|---|---|
| Solid collision with separation | physics.add.collider(a, b) |
| Detection only (pickups, triggers, hitboxes) | physics.add.overlap(a, b, cb) |
| Immovable level pieces | staticGroup(); call refreshBody() after scaling or moving one |
| Moving platforms | Dynamic body with setImmovable(true) and body.allowGravity = false |
| Rotated or polygon shapes, joints, stacking | Matter.js physics instead of Arcade |
- Velocities are pixels per second; do not multiply them by
delta. Usedelta(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 withPhaser.Geom.Intersects. body.blocked.downis set by tile and world-bound collisions;body.touching.downby 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 withsetScrollFactor(0). - A virtual joystick needs a dedicated pointer: track
pointer.idfrompointerdowntopointerup, and size touch targets at least 48 CSS px. - Fullscreen must be requested inside a user gesture: call
this.scale.startFullscreen()in apointeruphandler. 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, ...)whenthis.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 ininit().
Gotchas
thisinside 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
addTilesetImagemust match the name in Tiled. - Texture bleeding (lines between tiles) when the camera zooms: extrude tilesets by 1 px
and set
roundPixels: truein 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.setSizeorrefreshBodyfor static bodies.
Verification
npm run build, servedist/with a static server, and load it: console free of errors, no 404s in the network tab.- Arcade
debug: truepass: bodies match the art, colliders fire for the intended pairs. - Test on a real phone in both orientations: touch controls, audio after first tap, no page scroll, pause on tab switch.
- Play for five minutes with the Performance panel recording: steady frame rate, no sawtooth memory growth from unpooled objects.
- 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.