ideabrowser.com — find trending startup ideas with real demand
Try itnpx skills add https://github.com/chongdashu/phaserjs-tinyswords --skill phaser-gamedevBuild fast, polished 2D browser games using Phaser 3's scene-based architecture and physics 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:
Core principles:
Every Phaser game starts with a configuration object.
const config = {
type: Phaser.AUTO, // WebGL with Canvas fallback
width: 800,
height: 600,
scene: [BootScene, GameScene]
};
const game = new Phaser.Game(config);
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]
};
| 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 |
Scenes are the fundamental organizational unit. Each scene has a lifecycle.
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);
}
}
// 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');
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
Everything visible in Phaser is a Game Object.
// 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' });
// 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');
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;
}
// 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));
});
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);
// 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();
});
// 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;
});
// 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
});
// 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();
}
});
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)}%`);
});
}
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');
}
}
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);
}
// 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);
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
// 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);
❌ 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.
IMPORTANT: Game implementations should vary based on:
Avoid converging on:
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)
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
// 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)
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.