Phaser Game Development
Build fast, polished 2D browser games using Phaser 3's scene-based architecture and physics systems.
Philosophy: Games as Living Systems
Games are not static UIs—they are dynamic systems where entities interact, state evolves, and player input drives everything. Before writing code, think architecturally.
Before building, ask:
-
What scenes does this game need? (Boot, Menu, Game, Pause, GameOver)
-
What entities exist and how do they interact?
-
What state must persist across scenes?
-
What physics model fits? (Arcade for speed, Matter for realism)
-
What input methods will players use?
Core principles:
-
Scene-First Architecture: Structure games around scenes, not global state
-
Composition Over Inheritance: Build entities from game objects and components
-
Physics-Aware Design: Choose physics system before coding collisions
-
Asset Pipeline Discipline: Preload everything, reference by key
-
Frame-Rate Independence: Use delta time, not frame counting
Game Configuration
Every Phaser game starts with a configuration object.
Minimal Configuration
const config = { type: Phaser.AUTO, // WebGL with Canvas fallback width: 800, height: 600, scene: [BootScene, GameScene] };
const game = new Phaser.Game(config);
Full Configuration Pattern
const config = { type: Phaser.AUTO, width: 800, height: 600, parent: 'game-container', // DOM element ID backgroundColor: '#2d2d2d',
scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH },
physics: { default: 'arcade', arcade: { gravity: { y: 300 }, debug: false // Enable during development } },
scene: [BootScene, MenuScene, GameScene, GameOverScene] };
Physics System Choice
System Use When
Arcade Platformers, shooters, most 2D games. Fast, simple AABB collisions
Matter Physics puzzles, ragdolls, realistic collisions. Slower, more accurate
None Menu scenes, visual novels, card games
Scene Architecture
Scenes are the fundamental organizational unit. Each scene has a lifecycle.
Scene Lifecycle Methods
class GameScene extends Phaser.Scene { constructor() { super('GameScene'); // Scene key for reference }
init(data) { // Called first. Receive data from previous scene this.level = data.level || 1; }
preload() { // Load assets. Runs before create() this.load.image('player', 'assets/player.png'); this.load.spritesheet('enemy', 'assets/enemy.png', { frameWidth: 32, frameHeight: 32 }); }
create() { // Set up game objects, physics, input this.player = this.physics.add.sprite(100, 100, 'player'); this.cursors = this.input.keyboard.createCursorKeys(); }
update(time, delta) { // Game loop. Called every frame // delta = milliseconds since last frame this.player.x += this.speed * (delta / 1000); } }
Scene Transitions
// Start a new scene (stops current) this.scene.start('GameOverScene', { score: this.score });
// Launch scene in parallel (both run) this.scene.launch('UIScene');
// Pause/resume scenes this.scene.pause('GameScene'); this.scene.resume('GameScene');
// Stop a scene this.scene.stop('UIScene');
Recommended Scene Structure
scenes/ ├── BootScene.js # Asset loading, progress bar ├── MenuScene.js # Title screen, options ├── GameScene.js # Main gameplay ├── UIScene.js # HUD overlay (launched parallel) ├── PauseScene.js # Pause menu overlay └── GameOverScene.js # End screen, restart option
Game Objects
Everything visible in Phaser is a Game Object.
Common Game Objects
// Images (static) this.add.image(400, 300, 'background');
// Sprites (can animate, physics-enabled) const player = this.add.sprite(100, 100, 'player');
// Text const score = this.add.text(16, 16, 'Score: 0', { fontSize: '32px', fill: '#fff' });
// Graphics (draw shapes) const graphics = this.add.graphics(); graphics.fillStyle(0xff0000); graphics.fillRect(100, 100, 50, 50);
// Containers (group objects) const container = this.add.container(400, 300, [sprite1, sprite2]);
// Tilemaps const map = this.make.tilemap({ key: 'level1' });
Sprite Creation Patterns
// Basic sprite const sprite = this.add.sprite(x, y, 'textureKey');
// Sprite with physics body const sprite = this.physics.add.sprite(x, y, 'textureKey');
// From spritesheet frame const sprite = this.add.sprite(x, y, 'sheet', frameIndex);
// From atlas const sprite = this.add.sprite(x, y, 'atlas', 'frameName');
Physics Systems
Arcade Physics (Recommended Default)
Fast, simple physics for most 2D games.
// Enable physics on sprite this.physics.add.sprite(x, y, 'player');
// Or add physics to existing sprite this.physics.add.existing(sprite);
// Configure body sprite.body.setVelocity(200, 0); sprite.body.setBounce(0.5); sprite.body.setCollideWorldBounds(true); sprite.body.setGravityY(300);
// Collision detection this.physics.add.collider(player, platforms); this.physics.add.overlap(player, coins, collectCoin, null, this);
function collectCoin(player, coin) { coin.disableBody(true, true); // Remove from physics and hide this.score += 10; }
Physics Groups
// Static group (platforms, walls) const platforms = this.physics.add.staticGroup(); platforms.create(400, 568, 'ground').setScale(2).refreshBody();
// Dynamic group (enemies, bullets) const enemies = this.physics.add.group({ key: 'enemy', repeat: 5, setXY: { x: 100, y: 0, stepX: 70 } });
enemies.children.iterate(enemy => { enemy.setBounce(Phaser.Math.FloatBetween(0.4, 0.8)); });
Matter Physics
For realistic physics simulations.
// Config physics: { default: 'matter', matter: { gravity: { y: 1 }, debug: true } }
// Create bodies const ball = this.matter.add.circle(400, 100, 25); const box = this.matter.add.rectangle(400, 400, 100, 50, { isStatic: true });
// Sprite with Matter body const player = this.matter.add.sprite(100, 100, 'player'); player.setFriction(0.005); player.setBounce(0.9);
Input Handling
Keyboard Input
// Cursor keys this.cursors = this.input.keyboard.createCursorKeys();
// In update() if (this.cursors.left.isDown) { player.setVelocityX(-160); } else if (this.cursors.right.isDown) { player.setVelocityX(160); }
if (this.cursors.up.isDown && player.body.touching.down) { player.setVelocityY(-330); // Jump }
// Custom keys this.spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
// Key events this.input.keyboard.on('keydown-SPACE', () => { this.fire(); });
Pointer/Mouse Input
// Click/tap this.input.on('pointerdown', (pointer) => { console.log(pointer.x, pointer.y); });
// Make object interactive sprite.setInteractive(); sprite.on('pointerdown', () => { sprite.setTint(0xff0000); }); sprite.on('pointerup', () => { sprite.clearTint(); });
// Drag this.input.setDraggable(sprite); this.input.on('drag', (pointer, obj, dragX, dragY) => { obj.x = dragX; obj.y = dragY; });
Animations
Creating Animations
// In create() - define once this.anims.create({ key: 'walk', frames: this.anims.generateFrameNumbers('player', { start: 0, end: 3 }), frameRate: 10, repeat: -1 // Loop forever });
this.anims.create({ key: 'jump', frames: [{ key: 'player', frame: 4 }], frameRate: 20 });
// From atlas this.anims.create({ key: 'explode', frames: this.anims.generateFrameNames('atlas', { prefix: 'explosion_', start: 1, end: 8, zeroPad: 2 }), frameRate: 16, hideOnComplete: true });
Playing Animations
// Play animation sprite.anims.play('walk', true); // true = ignore if already playing
// Play once sprite.anims.play('jump');
// Stop sprite.anims.stop();
// Animation events sprite.on('animationcomplete', (anim, frame) => { if (anim.key === 'die') { sprite.destroy(); } });
Asset Loading
Preload Patterns
preload() { // Images this.load.image('sky', 'assets/sky.png');
// Spritesheets this.load.spritesheet('player', 'assets/player.png', { frameWidth: 32, frameHeight: 48 });
// Atlases (TexturePacker) this.load.atlas('sprites', 'assets/sprites.png', 'assets/sprites.json');
// Tilemaps this.load.tilemapTiledJSON('map', 'assets/level1.json'); this.load.image('tiles', 'assets/tileset.png');
// Audio this.load.audio('bgm', 'assets/music.mp3'); this.load.audio('sfx', ['assets/sound.ogg', 'assets/sound.mp3']);
// Progress tracking
this.load.on('progress', (value) => {
console.log(Loading: ${Math.round(value * 100)}%);
});
}
Boot Scene Pattern
class BootScene extends Phaser.Scene { constructor() { super('BootScene'); }
preload() { // Loading bar const width = this.cameras.main.width; const height = this.cameras.main.height;
const progressBar = this.add.graphics();
const progressBox = this.add.graphics();
progressBox.fillStyle(0x222222, 0.8);
progressBox.fillRect(width/2 - 160, height/2 - 25, 320, 50);
this.load.on('progress', (value) => {
progressBar.clear();
progressBar.fillStyle(0xffffff, 1);
progressBar.fillRect(width/2 - 150, height/2 - 15, 300 * value, 30);
});
// Load all game assets here
this.load.image('player', 'assets/player.png');
// ... more assets
}
create() { this.scene.start('MenuScene'); } }
Tilemaps (Tiled Integration)
Loading and Creating
preload() { this.load.tilemapTiledJSON('map', 'assets/map.json'); this.load.image('tiles', 'assets/tileset.png'); }
create() { const map = this.make.tilemap({ key: 'map' }); const tileset = map.addTilesetImage('tileset-name-in-tiled', 'tiles');
// Create layers (match names from Tiled) const backgroundLayer = map.createLayer('Background', tileset, 0, 0); const groundLayer = map.createLayer('Ground', tileset, 0, 0);
// Enable collision on specific tiles groundLayer.setCollisionByProperty({ collides: true }); // Or by tile index groundLayer.setCollisionBetween(1, 100);
// Add collision with player this.physics.add.collider(this.player, groundLayer); }
Object Layers
// Spawn points from Tiled object layer const spawnPoint = map.findObject('Objects', obj => obj.name === 'spawn'); this.player = this.physics.add.sprite(spawnPoint.x, spawnPoint.y, 'player');
// Create objects from layer const coins = map.createFromObjects('Objects', { name: 'coin', key: 'coin' }); this.physics.world.enable(coins);
Project Structure
Recommended Organization
game/ ├── src/ │ ├── scenes/ │ │ ├── BootScene.js │ │ ├── MenuScene.js │ │ ├── GameScene.js │ │ └── UIScene.js │ ├── gameObjects/ │ │ ├── Player.js │ │ ├── Enemy.js │ │ └── Collectible.js │ ├── systems/ │ │ ├── InputManager.js │ │ └── AudioManager.js │ ├── config/ │ │ └── gameConfig.js │ └── main.js ├── assets/ │ ├── images/ │ ├── audio/ │ ├── tilemaps/ │ └── fonts/ ├── index.html └── package.json
ES Module Setup
// main.js import Phaser from 'phaser'; import BootScene from './scenes/BootScene'; import GameScene from './scenes/GameScene'; import { gameConfig } from './config/gameConfig';
const config = { ...gameConfig, scene: [BootScene, GameScene] };
new Phaser.Game(config);
Anti-Patterns to Avoid
❌ Global State Soup: Storing game state on window or module globals Why bad: Untrackable bugs, scene transitions break state Better: Use scene data, registries, or dedicated state managers
❌ Loading in Create: Loading assets in create() instead of preload()
Why bad: Assets may not be ready when referenced Better: Always load in preload() , use Boot scene for all assets
❌ Frame-Dependent Logic: Using frame count instead of delta time Why bad: Game speed varies with frame rate Better: this.speed * (delta / 1000) for consistent movement
❌ Physics Overkill: Using Matter for simple platformer collisions Why bad: Performance hit, unnecessary complexity Better: Arcade physics handles 90% of 2D game needs
❌ Monolithic Scenes: One giant scene with all game logic Why bad: Unmaintainable, hard to add features Better: Separate scenes for menus, gameplay, UI overlays
❌ Magic Numbers: Hardcoded values scattered in code Why bad: Impossible to balance, inconsistent Better: Config objects, constants files
❌ Ignoring Object Pooling: Creating/destroying objects every frame Why bad: Memory churn, garbage collection stutters Better: Use groups with setActive(false) / setVisible(false)
❌ Synchronous Asset Access: Assuming assets load instantly Why bad: Race conditions, undefined textures Better: Chain scene starts, use load events
❌ Assuming Spritesheet Frame Dimensions: Using guessed frame sizes without verifying Why bad: Wrong dimensions cause silent frame corruption; off-by-pixels compounds into broken visuals Better: Open asset file, measure frames, calculate with spacing/margin, verify math adds up
❌ Ignoring Spritesheet Spacing: Not specifying spacing for gapped spritesheets Why bad: Frames shift progressively; later frames read wrong pixel regions Better: Check source asset for gaps between frames; use spacing: N in loader config
❌ Hardcoding Nine-Slice Colors: Using single background color for all UI panel variants Why bad: Transparent frame edges reveal wrong color for different asset color schemes Better: Per-asset background color config; sample from center frame (frame 4)
❌ Nine-Slice with Padded Frames: Treating the full frame as the slice region when the art is centered/padded inside each tile Why bad: Edge tiles contribute interior fill, showing up as opaque “side bars” inside the panel Better: Trim tiles to their effective content bounds (alpha bbox) and composite/cache a texture; add ~1px overlap + disable smoothing to avoid seams
❌ Scaling Discontinuous UI Art: Stretching a cropped ribbon/banner row that contains internal transparent gaps Why bad: The transparent gutters get stretched, so the UI looks segmented or the fill disappears behind the frame. Better: Slice the asset into caps/center, stretch only the center, and stitch the pieces (with ~1px overlap + smoothing disabled) before rendering at pivot sizes.
Variation Guidance
IMPORTANT: Game implementations should vary based on:
-
Game Genre: Platformer physics differ from top-down shooter physics
-
Target Platform: Mobile needs touch input, desktop can use keyboard
-
Art Style: Pixel art uses nearest-neighbor scaling, HD art uses linear
-
Performance Needs: Many sprites → object pooling; few sprites → simple creation
-
Complexity: Simple games can inline; complex games need class hierarchies
Avoid converging on:
-
Always using 800x600 resolution
-
Always using Arcade physics
-
Always using the same scene structure
-
Copy-pasting boilerplate without adaptation
Quick Reference
Common Physics Properties
body.setVelocity(x, y) body.setVelocityX(x) body.setBounce(x, y) body.setGravityY(y) body.setCollideWorldBounds(true) body.setImmovable(true) // For static-like dynamic bodies body.setDrag(x, y) body.setMaxVelocity(x, y)
Useful Scene Properties
this.cameras.main // Main camera this.physics.world // Physics world this.input.keyboard // Keyboard manager this.sound // Audio manager this.time // Time/clock manager this.tweens // Tween manager this.anims // Animation manager this.registry // Cross-scene data store this.data // Scene-specific data store
Essential Events
// Scene events this.events.on('pause', callback) this.events.on('resume', callback) this.events.on('shutdown', callback)
// Physics events this.physics.world.on('worldbounds', callback)
// Game object events sprite.on('destroy', callback) sprite.on('animationcomplete', callback)
See Also
-
references/arcade-physics.md - Deep dive into Arcade physics
-
references/tilemaps.md - Advanced tilemap techniques
-
references/performance.md - Optimization strategies
-
references/spritesheets-nineslice.md - Spritesheet loading (spacing/margin), nine-slice UI panels, asset inspection
Remember
Phaser gives you powerful primitives—scenes, sprites, physics, input—but architecture is your responsibility.
Think in systems: What scenes do you need? What entities exist? How do they interact? Answer these questions before writing code, and your game will be maintainable as it grows.
Claude is capable of building complete, polished Phaser games. These guidelines illuminate the path—they don't fence it.