From skillry-gaming-interactive-media
Use when building, debugging, or architecting browser games with Phaser 3 — covering Scene lifecycle, physics systems, sprite/atlas management, tilemap loading, input handling, and Vite-based TypeScript project setup.
How this skill is triggered — by the user, by Claude, or both
Slash command
/skillry-gaming-interactive-media:251-phaser-game-developmentThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Provide concrete, production-ready Phaser 3 patterns in TypeScript: Scene class structure, Arcade and Matter physics, texture atlas workflows, tilemap loading from Tiled, keyboard/pointer input, camera control, and a Vite dev environment. Surfaces real code you can paste and extend rather than documentation summaries.
Provide concrete, production-ready Phaser 3 patterns in TypeScript: Scene class structure, Arcade and Matter physics, texture atlas workflows, tilemap loading from Tiled, keyboard/pointer input, camera control, and a Vite dev environment. Surfaces real code you can paste and extend rather than documentation summaries.
.tmx / .json tilemap with collision layers.threejs-webgl-patterns instead.npm create vite@latest my-game -- --template vanilla-ts
cd my-game
npm install phaser
npm install -D @types/node
vite.config.ts:
import { defineConfig } from 'vite'
export default defineConfig({
base: './',
build: {
assetsDir: 'assets',
chunkSizeWarningLimit: 2048,
},
server: { port: 3000 },
})
src/main.ts — Phaser game config:
import Phaser from 'phaser'
import { BootScene } from './scenes/BootScene'
import { PreloadScene } from './scenes/PreloadScene'
import { GameScene } from './scenes/GameScene'
import { UIScene } from './scenes/UIScene'
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO, // AUTO → WebGL if available, fallback Canvas
width: 1280,
height: 720,
backgroundColor: '#1a1a2e',
physics: {
default: 'arcade',
arcade: { gravity: { x: 0, y: 600 }, debug: import.meta.env.DEV },
},
scene: [BootScene, PreloadScene, GameScene, UIScene],
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
}
new Phaser.Game(config)
// src/scenes/GameScene.ts
import Phaser from 'phaser'
export class GameScene extends Phaser.Scene {
private player!: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody
private platforms!: Phaser.Physics.Arcade.StaticGroup
private cursors!: Phaser.Types.Input.Keyboard.CursorKeys
constructor() {
super({ key: 'GameScene' })
}
// preload: called once — load assets before create
preload(): void {
// Assets already loaded in PreloadScene; nothing needed here
// unless this scene has unique assets
}
// create: called once after preload — build world, wire physics, input
create(): void {
// Tilemap
const map = this.make.tilemap({ key: 'level1' })
const tiles = map.addTilesetImage('terrain', 'terrain-tiles')!
const groundLayer = map.createLayer('Ground', tiles, 0, 0)!
groundLayer.setCollisionByProperty({ collides: true })
// Player from atlas
this.player = this.physics.add.sprite(100, 450, 'hero', 'idle_0')
this.player.setCollideWorldBounds(true)
this.player.setGravityY(200)
// Collision between player and tilemap layer
this.physics.add.collider(this.player, groundLayer)
// Animations from atlas frames
this.anims.create({
key: 'run',
frames: this.anims.generateFrameNames('hero', {
prefix: 'run_', start: 0, end: 7, zeroPad: 0,
}),
frameRate: 12,
repeat: -1,
})
// Camera
this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels)
this.cameras.main.startFollow(this.player, true, 0.1, 0.1)
// Input
this.cursors = this.input.keyboard!.createCursorKeys()
// Pass data to overlay UI scene
this.scene.launch('UIScene', { gameScene: this })
}
// update: called every frame — game logic only, no asset loading
update(_time: number, _delta: number): void {
const onGround = this.player.body.blocked.down
if (this.cursors.left.isDown) {
this.player.setVelocityX(-220)
this.player.setFlipX(true)
this.player.anims.play('run', true)
} else if (this.cursors.right.isDown) {
this.player.setVelocityX(220)
this.player.setFlipX(false)
this.player.anims.play('run', true)
} else {
this.player.setVelocityX(0)
this.player.anims.play('idle', true)
}
if (this.cursors.up.isDown && onGround) {
this.player.setVelocityY(-520)
}
}
}
// src/scenes/PreloadScene.ts
import Phaser from 'phaser'
export class PreloadScene extends Phaser.Scene {
constructor() { super({ key: 'PreloadScene' }) }
preload(): void {
// Progress bar
const bar = this.add.graphics()
this.load.on('progress', (v: number) => {
bar.clear().fillStyle(0x4ade80).fillRect(100, 360, 1080 * v, 20)
})
this.load.on('complete', () => bar.destroy())
// Texture atlas (TexturePacker / Aseprite export)
this.load.atlas('hero', 'assets/hero.png', 'assets/hero.json')
// Tilemap (Tiled JSON export)
this.load.tilemapTiledJSON('level1', 'assets/maps/level1.json')
this.load.image('terrain-tiles', 'assets/tiles/terrain.png')
// Audio
this.load.audio('jump', ['assets/sfx/jump.ogg', 'assets/sfx/jump.mp3'])
}
create(): void {
this.scene.start('GameScene')
}
}
// Enemy group with Arcade physics
const enemies = this.physics.add.group({
classType: Phaser.Physics.Arcade.Sprite,
defaultKey: 'enemy',
defaultFrame: 'walk_0',
maxSize: 20,
})
const enemy = enemies.get(400, 300) as Phaser.Physics.Arcade.Sprite
enemy.setActive(true).setVisible(true)
enemy.setVelocityX(-80)
// Overlap (no bounce): player picks up coin
this.physics.add.overlap(this.player, coins, (_player, coin) => {
(coin as Phaser.Physics.Arcade.Sprite).disableBody(true, true)
this.events.emit('coinCollected')
})
// Collider with callback: player stomps enemy
this.physics.add.collider(this.player, enemies, (player, enemy) => {
const p = player as Phaser.Types.Physics.Arcade.SpriteWithDynamicBody
if (p.body.velocity.y > 0 && p.y < (enemy as Phaser.Physics.Arcade.Sprite).y) {
(enemy as Phaser.Physics.Arcade.Sprite).disableBody(true, true)
p.setVelocityY(-300) // bounce off enemy
}
})
// components/GameCanvas.tsx
'use client'
import { useEffect, useRef } from 'react'
export default function GameCanvas() {
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
let game: import('phaser').Game | null = null
async function init() {
const Phaser = (await import('phaser')).default
const { GameScene } = await import('@/game/scenes/GameScene')
game = new Phaser.Game({
type: Phaser.AUTO,
width: 1280, height: 720,
parent: containerRef.current!,
scene: [GameScene],
})
}
init()
return () => { game?.destroy(true) }
}, [])
return <div ref={containerRef} className="w-full aspect-video" />
}
Phaser.AUTO selected — confirm WebGL in Chrome DevTools → canvas context.this.load.atlas(key, png, json).setCollisionByProperty({ collides: true }) matches the Tiled layer property name.update() reads input but never loads assets.preload() only in one scene (PreloadScene) — other scenes start after 'complete'.import.meta.env.DEV — never ships to production.game.destroy(true) called on React component unmount.['.ogg', '.mp3'].# Install Phaser 3 (latest stable)
npm install phaser
# Run dev server with HMR
npm run dev
# Production build
npm run build
# Preview production build locally
npm run preview
# Check bundle size (Phaser is ~1 MB minified)
npx vite-bundle-visualizer
When reviewing or generating Phaser code, deliver:
constructor({ key }), preload, create, update signatures.game.destroy cleanup).preload — defer to create.ref to the Game instance.this.physics.world is defined before calling physics methods in create.group.get() / group.killAndHide() — never destroy() pooled sprites.Done means: a working Scene class compiles with tsc --noEmit, a dev server starts at localhost:3000, physics collisions are wired and verified in the debug overlay (in DEV), and any React integration mounts/unmounts cleanly without game memory leaks.
npx claudepluginhub fluxonlab/skillry --plugin skillry-gaming-interactive-mediaCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.