
Phaser Gamedev
- 1.1k installs
- 38 repo stars
- Updated January 16, 2026
- chongdashu/phaserjs-tinyswords
phaser-gamedev is an agent skill for building 2D Phaser games with TinySwords assets, scenes, input, and game loop patterns.
About
The phaser-gamedev skill guides agents building 2D games with Phaser and the TinySwords asset pack. It covers scene setup, sprite loading, tilemaps, physics, input handling, animation states, camera follow, and UI overlays for browser games. Agents structure boot, preload, play, and game-over scenes with performant asset pipelines and responsive canvas sizing. The skill references TinySwords-specific layering, unit animation conventions, and map layout patterns while keeping game logic modular. Use when implementing Phaser gameplay, integrating TinySwords art, or refactoring an existing prototype into maintainable scene architecture.
- Phaser 2D game setup with TinySwords asset integration.
- Scene architecture for boot, preload, play, and game-over flows.
- Covers tilemaps, physics, input, animation, and camera patterns.
- Modular game logic separated from asset loading concerns.
- Browser canvas performance and responsive sizing guidance.
Phaser Gamedev by the numbers
- 1,056 all-time installs (skills.sh)
- +4 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #26 of 247 Game Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
phaser-gamedev capabilities & compatibility
- Capabilities
- phaser scene architecture · tinyswords asset integration · tilemap and physics setup · input and animation state handling · responsive canvas performance tuning
- Use cases
- frontend
What phaser-gamedev says it does
phaser-gamedev
npx skills add https://github.com/chongdashu/phaserjs-tinyswords --skill phaser-gamedevAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.1k |
|---|---|
| repo stars | ★ 38 |
| Security audit | 3 / 3 scanners passed |
| Last updated | January 16, 2026 |
| Repository | chongdashu/phaserjs-tinyswords ↗ |
How do I structure a Phaser game with TinySwords assets, scenes, physics, and input handling?
Build 2D Phaser games with TinySwords assets, scenes, input, and game loop patterns.
Who is it for?
Developers building or refactoring 2D browser games using Phaser and TinySwords art.
Skip if: Skip for 3D engines or non-Phaser game frameworks.
When should I use this skill?
User implements Phaser gameplay, TinySwords animations, or scene architecture for a 2D game.
What you get
Maintainable Phaser scenes with loaded sprites, gameplay systems, UI overlays, and performant browser rendering.
- Arcade Physics config
- collision body setup
- spatial partitioning pattern
Files
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: 1. Scene-First Architecture: Structure games around scenes, not global state 2. Composition Over Inheritance: Build entities from game objects and components 3. Physics-Aware Design: Choose physics system before coding collisions 4. Asset Pipeline Discipline: Preload everything, reference by key 5. 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.jsonES 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 storeEssential 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.
Arcade Physics Deep Dive
Comprehensive reference for Phaser 3 Arcade Physics system.
World Configuration
// In game config
physics: {
default: 'arcade',
arcade: {
gravity: { x: 0, y: 300 },
debug: true, // Show collision bodies
debugShowBody: true,
debugShowStaticBody: true,
debugShowVelocity: true,
debugBodyColor: 0xff00ff,
debugStaticBodyColor: 0x0000ff,
debugVelocityColor: 0x00ff00,
fps: 60, // Physics update rate
timeScale: 1, // 0.5 = half speed, 2 = double
checkCollision: {
up: true,
down: true,
left: true,
right: true
},
overlapBias: 4, // Overlap tolerance
tileBias: 16, // Tile collision bias
forceX: false, // Prioritize X-axis separation
maxEntries: 16, // Quadtree max per node
useTree: true // Spatial hash (false for >5000 bodies)
}
}Body Types
Dynamic Bodies
Move, respond to velocity, gravity, and collisions.
// Create with physics enabled
const player = this.physics.add.sprite(100, 100, 'player');
// Or add to existing sprite
const sprite = this.add.sprite(100, 100, 'player');
this.physics.add.existing(sprite);
// Body properties
player.body.setVelocity(100, -200);
player.body.setVelocityX(100);
player.body.setVelocityY(-200);
player.body.setBounce(0.5, 0.5);
player.body.setDrag(100, 100);
player.body.setFriction(0.5, 0.5);
player.body.setMaxVelocity(300, 400);
player.body.setGravityY(500); // Additional gravity
player.body.setAcceleration(100, 0);
player.body.setCollideWorldBounds(true);
player.body.onWorldBounds = true; // Enable worldbounds eventStatic Bodies
Immovable, no velocity or gravity response. Efficient for platforms.
// Create static group
const platforms = this.physics.add.staticGroup();
platforms.create(400, 568, 'ground');
// Or make existing body static
sprite.body.setImmovable(true);
sprite.body.moves = false;
// After scaling/moving static bodies
sprite.refreshBody();Body Sizing and Shape
// Custom body size
sprite.body.setSize(width, height, center);
sprite.body.setSize(32, 48, true); // Centered
// Offset body from sprite
sprite.body.setOffset(x, y);
// Circular body (for rolling objects)
sprite.body.setCircle(radius, offsetX, offsetY);
sprite.body.setCircle(16, 0, 0);Collision Detection
Collider vs Overlap
// Collider: Physical collision, objects separate
this.physics.add.collider(player, platforms);
this.physics.add.collider(player, enemies, hitEnemy, null, this);
// Overlap: Detect overlap without physical response
this.physics.add.overlap(player, coins, collectCoin, null, this);
function collectCoin(player, coin) {
coin.disableBody(true, true); // (disableGameObject, hideGameObject)
}Collision Callbacks
// Process callback (return false to skip collision)
function shouldCollide(player, enemy) {
return !player.isInvincible;
}
this.physics.add.collider(player, enemies, hitEnemy, shouldCollide, this);Collision Events
// World bounds event
this.physics.world.on('worldbounds', (body, up, down, left, right) => {
if (down) {
// Hit bottom of world
}
});
// Enable on body first
player.body.onWorldBounds = true;
player.body.setCollideWorldBounds(true);Groups
Static Groups
const platforms = this.physics.add.staticGroup();
// Add children
platforms.create(400, 568, 'ground');
platforms.createMultiple({
key: 'brick',
repeat: 10,
setXY: { x: 50, y: 300, stepX: 70 }
});
// After modifying
platforms.refresh();Dynamic Groups
const enemies = this.physics.add.group({
key: 'enemy',
repeat: 5,
setXY: { x: 100, y: 0, stepX: 100 }
});
// Group defaults
const bullets = this.physics.add.group({
defaultKey: 'bullet',
maxSize: 50,
runChildUpdate: true, // Call update() on children
collideWorldBounds: true,
velocityY: -300
});
// Iterate children
enemies.children.iterate(enemy => {
enemy.setBounce(0.5);
});
// Get first inactive (for pooling)
const bullet = bullets.get(x, y);
if (bullet) {
bullet.setActive(true).setVisible(true);
bullet.body.enable = true;
}Object Pooling
Reuse objects instead of create/destroy for performance.
// In create()
this.bulletPool = this.physics.add.group({
defaultKey: 'bullet',
maxSize: 100,
createCallback: (bullet) => {
bullet.setName('bullet' + this.bulletPool.getLength());
}
});
// Fire bullet
fire(x, y, velocityX, velocityY) {
const bullet = this.bulletPool.get(x, y);
if (bullet) {
bullet.setActive(true);
bullet.setVisible(true);
bullet.body.enable = true;
bullet.body.setVelocity(velocityX, velocityY);
}
}
// Return to pool
killBullet(bullet) {
bullet.setActive(false);
bullet.setVisible(false);
bullet.body.enable = false;
bullet.body.stop();
}Movement Patterns
Basic Movement
update(time, delta) {
const speed = 160;
player.setVelocity(0);
if (cursors.left.isDown) {
player.setVelocityX(-speed);
} else if (cursors.right.isDown) {
player.setVelocityX(speed);
}
if (cursors.up.isDown) {
player.setVelocityY(-speed);
} else if (cursors.down.isDown) {
player.setVelocityY(speed);
}
}Platformer Movement
update(time, delta) {
// Horizontal movement
if (cursors.left.isDown) {
player.setVelocityX(-160);
player.anims.play('walk', true);
player.flipX = true;
} else if (cursors.right.isDown) {
player.setVelocityX(160);
player.anims.play('walk', true);
player.flipX = false;
} else {
player.setVelocityX(0);
player.anims.play('idle', true);
}
// Jump (only when grounded)
if (cursors.up.isDown && player.body.blocked.down) {
player.setVelocityY(-330);
}
}Acceleration-Based Movement
const accel = 600;
const maxSpeed = 200;
const drag = 400;
player.body.setMaxVelocity(maxSpeed);
player.body.setDrag(drag, 0);
update() {
if (cursors.left.isDown) {
player.body.setAccelerationX(-accel);
} else if (cursors.right.isDown) {
player.body.setAccelerationX(accel);
} else {
player.body.setAccelerationX(0);
}
}Velocity Helpers
// Move to point
this.physics.moveTo(sprite, targetX, targetY, speed);
// Move to object
this.physics.moveToObject(sprite, target, speed);
// Accelerate to point
this.physics.accelerateTo(sprite, targetX, targetY, accel, maxSpeedX, maxSpeedY);
// Velocity from angle
this.physics.velocityFromAngle(angle, speed, outVelocity);
this.physics.velocityFromRotation(rotation, speed, outVelocity);Checking Collisions
// Touch flags (in update)
if (player.body.blocked.down) {
// On ground
}
if (player.body.blocked.left || player.body.blocked.right) {
// Hitting wall
}
// Touching another body
if (player.body.touching.down) {
// Standing on something
}
// Was touching last frame
if (player.body.wasTouching.down && !player.body.touching.down) {
// Just left ground
}
// Overlapping check
const overlapping = this.physics.overlap(player, enemy);
// Distance check
const distance = Phaser.Math.Distance.Between(
player.x, player.y, enemy.x, enemy.y
);World Bounds
// Set world bounds
this.physics.world.setBounds(0, 0, 3000, 600);
// Different bounds per side
this.physics.world.setBoundsCollision(true, true, true, false); // No bottom
// Camera follows player in larger world
this.cameras.main.setBounds(0, 0, 3000, 600);
this.cameras.main.startFollow(player);Debug Visualization
// Toggle debug at runtime
this.physics.world.drawDebug = true;
this.physics.world.debugGraphic.clear(); // Clear previous
// Custom debug rendering
const graphics = this.add.graphics();
this.physics.world.on('worldstep', () => {
graphics.clear();
enemies.children.iterate(enemy => {
graphics.strokeCircle(
enemy.body.center.x,
enemy.body.center.y,
enemy.body.halfWidth
);
});
});Common Patterns
One-Way Platforms
// In process callback
function oneWayPlatform(player, platform) {
// Only collide if player is falling and above platform
if (player.body.velocity.y > 0 &&
player.body.bottom <= platform.body.top + 10) {
return true;
}
return false;
}
this.physics.add.collider(player, platforms, null, oneWayPlatform, this);Knockback
function hitEnemy(player, enemy) {
const knockbackForce = 200;
const direction = player.x < enemy.x ? -1 : 1;
player.setVelocity(direction * knockbackForce, -knockbackForce);
player.isInvincible = true;
this.time.delayedCall(1000, () => {
player.isInvincible = false;
});
}Moving Platforms
// In create()
this.movingPlatform = this.physics.add.image(400, 400, 'platform');
this.movingPlatform.body.setImmovable(true);
this.movingPlatform.body.setAllowGravity(false);
// Tween for movement
this.tweens.add({
targets: this.movingPlatform,
x: 600,
duration: 2000,
ease: 'Sine.easeInOut',
yoyo: true,
repeat: -1
});
// In update() - move player with platform
if (player.body.touching.down &&
player.body.blocked.down) {
// Player is on platform
}Performance Optimization
Strategies for maintaining smooth 60fps in Phaser 3 games.
Object Pooling
The most impactful optimization for games with many spawning/despawning objects.
Why Pool?
Creating/destroying objects causes:
- Memory allocation overhead
- Garbage collection pauses (stutters)
- Texture rebinding costs
Implementation Pattern
class BulletPool {
constructor(scene) {
this.scene = scene;
this.pool = scene.physics.add.group({
defaultKey: 'bullet',
maxSize: 100,
runChildUpdate: true
});
}
spawn(x, y, velocityX, velocityY) {
const bullet = this.pool.get(x, y);
if (!bullet) return null; // Pool exhausted
bullet.setActive(true);
bullet.setVisible(true);
bullet.body.enable = true;
bullet.body.reset(x, y);
bullet.setVelocity(velocityX, velocityY);
return bullet;
}
kill(bullet) {
bullet.setActive(false);
bullet.setVisible(false);
bullet.body.enable = false;
bullet.body.stop();
}
}
// Usage
this.bulletPool = new BulletPool(this);
// Spawn
const bullet = this.bulletPool.spawn(player.x, player.y, 500, 0);
// Kill (in collision callback or update)
this.bulletPool.kill(bullet);Built-in Group Pooling
// Configure group for pooling
const enemies = this.physics.add.group({
maxSize: 50,
classType: Enemy,
createCallback: (enemy) => {
enemy.setName('enemy' + enemies.getLength());
},
removeCallback: (enemy) => {
enemy.setName('');
}
});
// Get inactive member (or create if under maxSize)
const enemy = enemies.get(x, y);
// Return to pool (don't destroy)
enemy.setActive(false);
enemy.setVisible(false);Texture Atlases
Combine sprites into atlases to reduce draw calls.
Why Atlases?
- Single texture bind per atlas
- Reduced HTTP requests
- Better GPU memory usage
Using TexturePacker
Export as Phaser 3 JSON Hash format.
// Load atlas
this.load.atlas('sprites', 'atlas/sprites.png', 'atlas/sprites.json');
// Use frames
this.add.sprite(x, y, 'sprites', 'player-idle-1');
// Animation from atlas
this.anims.create({
key: 'walk',
frames: this.anims.generateFrameNames('sprites', {
prefix: 'player-walk-',
start: 1,
end: 8,
zeroPad: 2
}),
frameRate: 10,
repeat: -1
});Camera Culling
Only render what's visible.
Automatic Culling
Phaser culls off-camera objects by default for:
- Sprites
- Images
- TileSprites
Disable if needed:
sprite.setScrollFactor(0); // Fixed to camera (no culling)Manual Culling for Custom Objects
update() {
const cam = this.cameras.main;
const bounds = cam.worldView;
this.enemies.children.iterate(enemy => {
if (Phaser.Geom.Rectangle.Contains(bounds, enemy.x, enemy.y)) {
enemy.setActive(true);
enemy.setVisible(true);
} else {
enemy.setActive(false);
enemy.setVisible(false);
}
});
}Physics Optimization
Reduce Collision Checks
// Only check relevant collisions
this.physics.add.collider(player, groundLayer);
this.physics.add.collider(enemies, groundLayer);
this.physics.add.overlap(player, enemies, hitEnemy);
// DON'T: enemies vs enemies if not needed
// this.physics.add.collider(enemies, enemies);Disable Physics When Not Needed
// Disable body temporarily
sprite.body.enable = false;
// Re-enable
sprite.body.enable = true;
// For off-screen objects
if (!cam.worldView.contains(enemy.x, enemy.y)) {
enemy.body.enable = false;
} else {
enemy.body.enable = true;
}Use Spatial Hash for Many Objects
physics: {
arcade: {
useTree: true, // Enable quadtree (default)
maxEntries: 16 // Tune for your object count
}
}
// For >5000 dynamic bodies, disable tree
// useTree: falseSimplify Collision Shapes
// Use circles for round objects (faster than rectangles)
sprite.body.setCircle(16);
// Reduce body size for tighter collisions
sprite.body.setSize(24, 32); // Smaller than spriteRendering Optimization
Batch Similar Sprites
Group sprites using same texture for batching:
// Good: All use same atlas
const coins = this.add.group({
key: 'atlas',
frame: 'coin',
repeat: 100
});
// Bad: Mixed textures break batchingReduce Blend Modes
// Normal blend mode is fastest
sprite.setBlendMode(Phaser.BlendModes.NORMAL);
// Avoid if possible:
// - ADD, MULTIPLY, SCREEN cause extra draw callsUse Static Images for Backgrounds
// TileSprite for repeating backgrounds (efficient)
this.add.tileSprite(0, 0, 800, 600, 'background').setOrigin(0);
// Don't animate large backgrounds in update()Limit Particle Count
const emitter = this.add.particles(x, y, 'particle', {
speed: 100,
lifespan: 500,
quantity: 2, // Particles per emit
maxParticles: 100, // Hard limit
frequency: 50 // ms between emits
});Memory Management
Destroy Unused Objects
// Properly destroy sprites
sprite.destroy();
// Clear groups
group.clear(true, true); // Remove from scene, destroy
// Scene cleanup
shutdown() {
this.enemies.destroy(true);
this.bulletPool.destroy(true);
}Unload Unused Assets
// Remove texture
this.textures.remove('unused-texture');
// Remove audio
this.sound.remove('unused-sound');
// In scene shutdown
shutdown() {
this.cache.tilemap.remove('level1');
}Monitor Memory
// Check texture memory (approximate)
console.log(this.textures.list);
// Chrome DevTools:
// - Memory tab for heap snapshots
// - Performance tab for frame timingUpdate Loop Optimization
Throttle Expensive Operations
create() {
this.lastAIUpdate = 0;
this.aiUpdateInterval = 100; // ms
}
update(time, delta) {
// Every frame (required for smooth movement)
this.updatePlayerMovement();
// Throttled (AI, pathfinding)
if (time - this.lastAIUpdate > this.aiUpdateInterval) {
this.updateEnemyAI();
this.lastAIUpdate = time;
}
}Avoid Creating Objects in Update
// BAD: Creates new object every frame
update() {
const velocity = { x: 100, y: 0 }; // GC pressure
sprite.setVelocity(velocity.x, velocity.y);
}
// GOOD: Reuse or use primitives
update() {
sprite.setVelocity(100, 0);
}
// Or pre-create
create() {
this.tempVec = new Phaser.Math.Vector2();
}
update() {
this.tempVec.set(100, 0);
sprite.body.velocity.copy(this.tempVec);
}Use Delta Time
// Framerate-independent movement
update(time, delta) {
const speed = 200; // pixels per second
sprite.x += speed * (delta / 1000);
}Profiling
Built-in Stats
const config = {
// ...
fps: {
target: 60,
forceSetTimeOut: false,
smoothStep: true
}
};
// Add FPS display
this.add.text(10, 10, '', { fontSize: '16px' }).setScrollFactor(0);
this.fpsText = this.add.text(10, 10, '');
update() {
this.fpsText.setText('FPS: ' + Math.round(this.game.loop.actualFps));
}Chrome DevTools
1. Performance Tab: Record gameplay, identify frame drops 2. Memory Tab: Track heap size, find leaks 3. Console: Phaser.GAMES[0].loop.actualFps
Common Bottlenecks
| Symptom | Likely Cause | Solution |
|---|---|---|
| Gradual slowdown | Memory leak | Check destroy() calls |
| Periodic stutters | GC pauses | Object pooling |
| Low FPS always | Too many objects | Culling, pooling |
| Spikes on spawn | Object creation | Pre-pool objects |
| Slow collisions | Too many checks | Spatial partitioning |
Quick Wins Checklist
- [ ] Use texture atlases (not individual images)
- [ ] Pool frequently spawned objects
- [ ] Disable physics for off-screen objects
- [ ] Use appropriate physics shapes (circles are faster)
- [ ] Throttle AI/pathfinding updates
- [ ] Avoid object creation in update loop
- [ ] Use delta time for movement
- [ ] Destroy objects when done
- [ ] Limit particle counts
- [ ] Profile before optimizing
Spritesheets and Nine-Slice UI Panels
Philosophy: Measure Before You Code
Spritesheet loading seems simple until it breaks. A few pixels off in frame size or a missing parameter causes silent corruption that manifests as broken visuals. Always inspect the source asset before writing loader code.
---
Spritesheet Loading
Basic Loading
this.load.spritesheet('player', 'assets/player.png', {
frameWidth: 32,
frameHeight: 48
});Spritesheets with Gaps (Spacing)
Many asset packs include spacing between frames for visual clarity in the source file:
// Asset is 448x448 with 3x3 grid of 144px frames and 8px gaps
this.load.spritesheet('ui-wood-table', 'assets/wood-table.png', {
frameWidth: 144,
frameHeight: 144,
spacing: 8 // Gap between frames
});Spritesheets with Margins
Some spritesheets have padding around the entire image:
this.load.spritesheet('icons', 'assets/icons.png', {
frameWidth: 32,
frameHeight: 32,
margin: 4, // Padding around entire sheet
spacing: 2 // Gap between frames
});Calculating Frame Dimensions
Formula:
imageWidth = (frameWidth × cols) + (spacing × (cols - 1)) + (margin × 2)
imageHeight = (frameHeight × rows) + (spacing × (rows - 1)) + (margin × 2)Example Calculation:
448px image with 3 columns:
- If spacing=0: 448/3 = 149.33 (not clean - wrong assumption)
- If spacing=8: (448 - 16) / 3 = 144 (clean - correct!)
Verify: 144*3 + 8*2 = 432 + 16 = 448 ✓---
Asset Inspection Protocol
Before writing spritesheet loader code:
1. Open the asset file in an image viewer or editor 2. Note total dimensions (e.g., 448×448 pixels) 3. Count rows and columns (e.g., 3×3 grid) 4. Measure one frame's actual size (zoom in, use pixel ruler) 5. Check for gaps between frames (that's your spacing value) 6. Check for padding around edges (that's your margin value) 7. Calculate and verify the dimensions add up
Red Flags
- Image dimension doesn't divide evenly by expected frame count
- Calculated frame size has decimals
- Visual inspection shows gaps between frame content
---
Nine-Slice UI Panels
Nine-slice (or 9-patch) panels allow UI elements to scale while preserving corner/edge details.
Frame Layout (3x3 Grid)
[0] [1] [2] ← Top row (corners 0,2 don't scale)
[3] [4] [5] ← Middle row (3,5 stretch vertically)
[6] [7] [8] ← Bottom row (corners 6,8 don't scale)
↑
1,4,7 stretch horizontallyManual Nine-Slice Assembly
When Phaser's built-in NineSlice doesn't work (e.g., frames with transparent edges):
showNineSlicePanel(framesKey, frameSize, panelWidth, panelHeight, bgColor) {
const centerX = this.cameras.main.width / 2;
const centerY = this.cameras.main.height / 2;
// Panel bounds
const left = centerX - panelWidth / 2;
const top = centerY - panelHeight / 2;
const right = left + panelWidth;
const bottom = top + panelHeight;
// Position corners inward from panel edges
const cornerInset = frameSize * 0.3;
const overlap = frameSize * 0.5; // Extra size to fill gaps
const container = this.add.container(0, 0);
// 1. Solid background (fills transparent gaps)
container.add(this.add.rectangle(centerX, centerY, panelWidth, panelHeight, bgColor));
// 2. Center fill (scaled to panel size)
const center = this.add.image(centerX, centerY, framesKey, 4);
center.setDisplaySize(panelWidth + overlap, panelHeight + overlap);
container.add(center);
// 3. Edges (stretched)
const topEdge = this.add.image(centerX, top + cornerInset, framesKey, 1);
topEdge.setDisplaySize(panelWidth + overlap, frameSize);
container.add(topEdge);
const bottomEdge = this.add.image(centerX, bottom - cornerInset, framesKey, 7);
bottomEdge.setDisplaySize(panelWidth + overlap, frameSize);
container.add(bottomEdge);
const leftEdge = this.add.image(left + cornerInset, centerY, framesKey, 3);
leftEdge.setDisplaySize(frameSize, panelHeight + overlap);
container.add(leftEdge);
const rightEdge = this.add.image(right - cornerInset, centerY, framesKey, 5);
rightEdge.setDisplaySize(frameSize, panelHeight + overlap);
container.add(rightEdge);
// 4. Corners (not scaled, positioned last/on top)
container.add(this.add.image(left + cornerInset, top + cornerInset, framesKey, 0));
container.add(this.add.image(right - cornerInset, top + cornerInset, framesKey, 2));
container.add(this.add.image(left + cornerInset, bottom - cornerInset, framesKey, 6));
container.add(this.add.image(right - cornerInset, bottom - cornerInset, framesKey, 8));
return container;
}Asset-Specific Parameters
Different assets need different values. Don't hardcode - configure per asset:
const UI_PANEL_CONFIG = {
'paper-regular': {
frameSize: 106,
spacing: 0,
cornerInset: 0.28,
overlap: 55,
bgColor: 0xF5E6C8 // Beige
},
'paper-special': {
frameSize: 106,
spacing: 0,
cornerInset: 0.28,
overlap: 55,
bgColor: 0x4A5568 // Dark blue-gray
},
'wood-table': {
frameSize: 144,
spacing: 8,
cornerInset: 0.35,
overlap: 80,
bgColor: 0x8B5A2B // Brown
}
};---
Common Pitfalls
1. Wrong Frame Dimensions
Symptom: Frames display corrupted, shifted, or partial content
Cause: Frame width/height doesn't match actual asset layout
Fix: Open asset, measure frames, calculate with spacing/margin
2. Missing Spacing Parameter
Symptom: First frame looks correct, subsequent frames are offset
Cause: Asset has gaps between frames that weren't specified
Fix: Add spacing: N to spritesheet config
3. Wrong Background Color for Nine-Slice
Symptom: Different color showing through transparent edges
Cause: Background fill color doesn't match asset's interior color
Fix: Sample color from center frame (frame 4), use per-asset config
4. Assuming Similar Assets Are Identical
Symptom: One variant works, another doesn't
Cause: Different assets in same pack may have different layouts
Example:
- Paper assets: 320×320, 106px frames, no spacing
- Wood Table: 448×448, 144px frames, 8px spacing
Fix: Inspect and configure each asset type individually
5. Internal Padding Inside Frames (“Side Bars”)
Symptom: Paper-like UI panels show opaque bands just inside the edges.
Cause: The art in each 3×3 cell is centered with transparent padding, so the edge tiles contribute interior fill when stretched.
Fix: Trim each tile to the actual painted bounds (alpha bbox), composite/cache a texture at the target size, and add a small overlap (≈1px, smoothing off) to avoid seams.
6. Scaling Discontinuous UI Art (Ribbons / Banners)
Symptom: A ribbon looks broken or the fill disappears when scaled.
Cause: The source row is actually three separate slices (left cap, center bar, right cap) with transparent gutters. Scaling the entire crop includes the gutters, making the UI look segmented or blank.
Fix: Treat it as a multi-slice (usually 3-slice). Draw left cap, stretched center, right cap into a canvas at the desired width; disable smoothing and include small seam overlaps so the stitched texture feels seamless.
function createRibbonSlice(scene, srcKey, width, height, row, slices) {
const key = `${srcKey}_3slice_${row}_${width}x${height}`;
if (scene.textures.exists(key)) return key;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
const sy = row * slices.frameH;
const leftW = Math.round(slices.left.w * (height / slices.frameH));
const rightW = Math.round(slices.right.w * (height / slices.frameH));
const centerW = Math.max(1, width - leftW - rightW);
const seam = 1;
const src = scene.textures.get(srcKey).getSourceImage();
ctx.drawImage(src, slices.left.x, sy, slices.left.w, slices.frameH, 0, 0, leftW + seam, height);
ctx.drawImage(src, slices.center.x, sy, slices.center.w, slices.frameH, leftW - seam, 0, centerW + seam * 2, height);
ctx.drawImage(src, slices.right.x, sy, slices.right.w, slices.frameH, leftW + centerW - seam, 0, rightW + seam, height);
scene.textures.addCanvas(key, canvas);
return key;
}5. Internal Padding Inside Frames (“Side Bars”)
Symptom: Paper-like panels show opaque vertical (or horizontal) “bands” just inside the left/right (or top/bottom) edges.
Cause: The art inside each 3×3 cell is centered with lots of transparent padding. Edge frames (3/5 or 1/7) often include a wide region of interior fill, so using the full frame as the slice region paints that fill into the panel as a visible band.
Fix: Build nine-slice panels from trimmed slices, not full frames: 1. Inspect the 9 frames and find the effective content bounds (alpha bounding box) per row/col. 2. Crop each tile to that effective region (removing padded interior space). 3. Composite/cache a single texture for the target panel size (canvas or RenderTexture). 4. Use a small overlap (≈1px) + disable smoothing to avoid seam lines.
---
Debugging Spritesheets
Raw Frame Visualization
Always include a test mode that displays extracted frames:
showRawFrames(key, frameCount) {
for (let i = 0; i < frameCount; i++) {
const col = i % 3;
const row = Math.floor(i / 3);
const x = 100 + col * 120;
const y = 100 + row * 120;
this.add.image(x, y, key, i);
this.add.text(x, y + 50, `${i}`, { fontSize: '12px' }).setOrigin(0.5);
}
}If frames look wrong here, the loader config is wrong. Fix it before proceeding.
Checklist When Frames Look Wrong
1. Open source asset in image editor 2. Measure actual frame dimensions 3. Check for gaps between frames 4. Check for padding around edges 5. Recalculate: does math add up to image dimensions? 6. Update loader with correct frameWidth, frameHeight, spacing, margin
---
Asset Documentation Template
Document each spritesheet's structure:
/**
* UI Panel Assets - Tiny Swords Pack
*
* Regular Paper: 320×320, 3×3 grid, 106px frames, no spacing
* Special Paper: 320×320, 3×3 grid, 106px frames, no spacing
* Wood Table: 448×448, 3×3 grid, 144px frames, 8px spacing
*
* All use nine-slice layout:
* [0][1][2] corners=0,2,6,8 (don't scale)
* [3][4][5] edges=1,3,5,7 (stretch one axis)
* [6][7][8] center=4 (scales both axes)
*/---
Remember
- Measure the asset before writing loader code
- Different assets need different configs even in the same pack
- Test raw frames first before building complex UI
- Document asset parameters for future reference
- A few pixels off cascades into completely broken rendering
Tilemaps Reference
Comprehensive guide for Phaser 3 tilemap integration with Tiled.
Tiled Setup Best Practices
Tileset Configuration
1. Use embedded tilesets (File > Export Tileset if external) 2. Set consistent tile size (16x16, 32x32, 64x64) 3. Add custom properties to tiles for collision, damage, etc. 4. Use tile collision editor for precise hitboxes
Layer Organization
Recommended layer structure (top to bottom in Tiled):
- Foreground (renders above player)
- Objects (spawn points, triggers)
- Player (reference layer, not exported)
- Enemies (object layer)
- Collectibles (object layer)
- Ground (collision layer)
- Background (decoration)Loading Tilemaps
JSON Format (Recommended)
preload() {
// Load tilemap JSON (exported from Tiled)
this.load.tilemapTiledJSON('level1', 'assets/tilemaps/level1.json');
// Load tileset image(s)
this.load.image('tiles', 'assets/tilesets/tileset.png');
// For multiple tilesets
this.load.image('terrain', 'assets/tilesets/terrain.png');
this.load.image('props', 'assets/tilesets/props.png');
}Tileset with Margins/Spacing
// If tileset has margin (border) or spacing (between tiles)
this.load.image('tiles', 'assets/tileset.png');
// Later, specify margin and spacing
const tileset = map.addTilesetImage('tileset-name', 'tiles',
tileWidth, tileHeight, margin, spacing);
// Example: 32x32 tiles with 1px margin and 2px spacing
const tileset = map.addTilesetImage('my-tiles', 'tiles', 32, 32, 1, 2);Creating the Map
Basic Setup
create() {
// Create tilemap from loaded JSON
const map = this.make.tilemap({ key: 'level1' });
// Add tileset image (name must match Tiled tileset name)
const tileset = map.addTilesetImage('tileset-name-in-tiled', 'tiles');
// Create layers (names must match Tiled layer names)
const bgLayer = map.createLayer('Background', tileset, 0, 0);
const groundLayer = map.createLayer('Ground', tileset, 0, 0);
const fgLayer = map.createLayer('Foreground', tileset, 0, 0);
// Foreground renders above player
fgLayer.setDepth(10);
}Multiple Tilesets
const terrainTileset = map.addTilesetImage('terrain', 'terrain-img');
const propsTileset = map.addTilesetImage('props', 'props-img');
// Layer can use multiple tilesets
const groundLayer = map.createLayer('Ground', [terrainTileset, propsTileset]);Collision Setup
By Tile Index
// Single tile
groundLayer.setCollision(1);
// Multiple tiles
groundLayer.setCollision([1, 2, 3, 4, 5]);
// Range of tiles
groundLayer.setCollisionBetween(1, 100);
// Exclude tiles
groundLayer.setCollisionByExclusion([-1]); // All except emptyBy Custom Property
Set properties in Tiled (Tileset > Edit Tileset > Select tile > Add property)
// Collide tiles with "collides: true" property
groundLayer.setCollisionByProperty({ collides: true });
// Multiple properties
groundLayer.setCollisionByProperty({ solid: true, type: 'wall' });Physics Collider
// Add physics collision
this.physics.add.collider(player, groundLayer);
// With callback
this.physics.add.collider(player, hazardLayer, onHazardHit, null, this);
function onHazardHit(player, tile) {
console.log('Hit hazard at', tile.x, tile.y);
player.damage(10);
}Object Layers
Reading Objects
// Get all objects from layer
const objectLayer = map.getObjectLayer('Objects');
const objects = objectLayer.objects;
objects.forEach(obj => {
console.log(obj.name, obj.x, obj.y, obj.properties);
});
// Find specific object
const spawnPoint = map.findObject('Objects', obj => obj.name === 'spawn');
player.setPosition(spawnPoint.x, spawnPoint.y);
// Filter objects
const enemies = map.filterObjects('Objects', obj => obj.type === 'enemy');Creating Sprites from Objects
// Create sprites from object layer
const coins = map.createFromObjects('Collectibles', {
name: 'coin', // Object name in Tiled
key: 'coin' // Texture key
});
// Add physics to all coins
this.physics.world.enable(coins);
coins.forEach(coin => {
coin.body.setAllowGravity(false);
});
// With custom class
const enemies = map.createFromObjects('Enemies', {
name: 'goblin',
classType: Goblin // Custom class extending Sprite
});Custom Properties on Objects
In Tiled, add custom properties to objects. Access in Phaser:
const obj = map.findObject('Objects', o => o.name === 'door');
// Access properties
const isLocked = obj.properties.find(p => p.name === 'locked').value;
const requiredKey = obj.properties.find(p => p.name === 'keyType').value;
// Helper function
function getProperty(obj, name) {
const prop = obj.properties?.find(p => p.name === name);
return prop ? prop.value : undefined;
}Tile Manipulation
Get/Set Tiles
// Get tile at world position
const tile = groundLayer.getTileAtWorldXY(pointer.x, pointer.y);
// Get tile at tile coordinates
const tile = groundLayer.getTileAt(tileX, tileY);
// Set tile
groundLayer.putTileAt(tileIndex, tileX, tileY);
// Remove tile
groundLayer.removeTileAt(tileX, tileY);
// Replace tiles
groundLayer.replaceByIndex(oldIndex, newIndex);Tile Properties
if (tile) {
console.log(tile.index); // Tile index
console.log(tile.x, tile.y); // Tile coordinates
console.log(tile.pixelX, tile.pixelY); // World position
console.log(tile.properties); // Custom properties
// Check collision
if (tile.collides) {
// This tile has collision
}
}Iterate Tiles
// Process each tile in layer
groundLayer.forEachTile(tile => {
if (tile.index === 77) { // Spike tile
// Replace with sprite
const spike = this.add.sprite(tile.getCenterX(), tile.getCenterY(), 'spike');
groundLayer.removeTileAt(tile.x, tile.y);
}
});
// With bounds
groundLayer.forEachTile(callback, context,
startX, startY, width, height, filteringOptions);Camera and World Bounds
// Set world bounds to map size
this.physics.world.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
// Camera bounds
this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
// Follow player
this.cameras.main.startFollow(player, true, 0.1, 0.1);
// Dead zones (player can move in center without camera moving)
this.cameras.main.setDeadzone(200, 100);Advanced Techniques
Parallax Scrolling
// Create layers with different scroll factors
const skyLayer = map.createLayer('Sky', tileset);
const cloudsLayer = map.createLayer('Clouds', tileset);
const mountainsLayer = map.createLayer('Mountains', tileset);
const groundLayer = map.createLayer('Ground', tileset);
// Closer layers scroll faster (1 = normal, <1 = slower)
skyLayer.setScrollFactor(0); // Fixed background
cloudsLayer.setScrollFactor(0.2);
mountainsLayer.setScrollFactor(0.5);
groundLayer.setScrollFactor(1); // Normal scrollingAnimated Tiles
Phaser doesn't natively animate tilemap tiles. Use plugin or manual approach:
// Manual approach: swap tile indices on timer
this.time.addEvent({
delay: 200,
callback: () => {
waterLayer.forEachTile(tile => {
if (tile.index >= 10 && tile.index <= 13) {
tile.index = 10 + ((tile.index - 10 + 1) % 4);
}
});
},
loop: true
});Procedural Tilemap
// Create blank tilemap
const map = this.make.tilemap({
tileWidth: 32,
tileHeight: 32,
width: 100,
height: 50
});
const tileset = map.addTilesetImage('tiles');
const layer = map.createBlankLayer('Ground', tileset);
// Fill programmatically
for (let x = 0; x < map.width; x++) {
for (let y = 0; y < map.height; y++) {
if (y === map.height - 1) {
layer.putTileAt(1, x, y); // Ground
} else if (Math.random() < 0.1) {
layer.putTileAt(2, x, y); // Random obstacle
}
}
}
layer.setCollision([1, 2]);Weighted Random Tiles
function getWeightedTile() {
const tiles = [
{ index: 1, weight: 10 }, // Grass (common)
{ index: 2, weight: 3 }, // Flower (uncommon)
{ index: 3, weight: 1 } // Mushroom (rare)
];
const total = tiles.reduce((sum, t) => sum + t.weight, 0);
let random = Math.random() * total;
for (const tile of tiles) {
random -= tile.weight;
if (random <= 0) return tile.index;
}
return tiles[0].index;
}Debugging Tilemaps
// Render tile coordinates
groundLayer.forEachTile(tile => {
if (tile.index !== -1) {
this.add.text(tile.pixelX, tile.pixelY, `${tile.x},${tile.y}`, {
fontSize: '10px'
}).setOrigin(0);
}
});
// Debug collision tiles
groundLayer.renderDebug(this.add.graphics(), {
tileColor: null,
collidingTileColor: new Phaser.Display.Color(255, 0, 0, 100),
faceColor: new Phaser.Display.Color(0, 255, 0, 255)
});Related skills
FAQ
Which engine does phaser-gamedev target?
Phaser for 2D browser games with TinySwords asset conventions.
What scene flow is recommended?
Boot, preload, play, and game-over scenes with separated asset loading and game logic.
What gameplay systems are covered?
Tilemaps, physics, input, animation states, camera follow, and UI overlays.
Is Phaser Gamedev safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.