Phaser is a fast, free, and fun open-source HTML5 game framework that renders with WebGL and Canvas across desktop and mobile web browsers. This guide is the complete, hands-on reference for building games with Phaser 4 — from your first sprite on screen to physics, animation, input, audio, cameras, and a full multi-scene game loop.
You write your game in JavaScript or TypeScript, and Phaser handles the rendering, game loop, asset loading, input, sound, and physics for you. Games run in the browser and can be wrapped for iOS, Android, Steam, YouTube Playables, Discord Activities, and more using third-party tools.
Note
This guide documents Phaser 4.1.0 ("Salusa"). Phaser 4 is a major release built on a brand-new WebGL renderer. If you're coming from Phaser 3, the public API is mostly familiar, but several things changed under the hood — see Migrating from Phaser 3 near the end.
Table of Contents
- Quick Start
- Installation
- Your First Game, Step by Step
- Core Concepts
- Configuring Your Game
- Working with Scenes
- Loading Assets
- Game Objects: Sprites and Images
- Text and Bitmap Text
- Animations
- Tweens
- Arcade Physics
- Input: Keyboard, Mouse, Touch, Gamepad
- Cameras
- Audio and Sound
- Groups, Containers, and Layers
- Common Workflows (Recipes)
- Advanced Topics
- How It Works
- Migrating from Phaser 3
- Troubleshooting
- Frequently Asked Questions
- Quick Reference
Quick Start
The smallest possible Phaser game loads an image and displays it. Create an index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My Phaser Game</title>
<script src="//cdn.jsdelivr.net/npm/phaser@4.1.0/dist/phaser.js"></script>
</head>
<body>
<script>
class MyScene extends Phaser.Scene {
preload() {
this.load.image('logo', 'assets/logo.png');
}
create() {
this.add.image(400, 300, 'logo');
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
scene: MyScene
};
const game = new Phaser.Game(config);
</script>
</body>
</html>
Open index.html in a browser (served over HTTP — see the warning below) and you'll see your logo centered on an 800×600 canvas.
Warning
Loading assets from the local filesystem (
file://) will usually fail because of browser CORS restrictions. Always run your game from a local web server. The quickest option isnpx serveorpython3 -m http.serverin your project folder, then openhttp://localhost:8000.
Tip
Phaser.AUTOtells Phaser to use WebGL if the browser supports it and fall back to Canvas if not. This is the recommended renderer setting for almost every game.
Installation
There are three ways to add Phaser to a project: a CDN script tag, an npm install, or the official scaffolding tool. Pick the one that matches how you build.
CDN (no build step)
Drop a script tag into your HTML. Phaser becomes available as the global Phaser.
<!-- Full, readable build -->
<script src="//cdn.jsdelivr.net/npm/phaser@4.1.0/dist/phaser.js"></script>
<!-- Minified production build -->
<script src="//cdn.jsdelivr.net/npm/phaser@4.1.0/dist/phaser.min.js"></script>
Phaser is also on Cloudflare's cdnjs:
<script src="https://cdnjs.cloudflare.com/ajax/libs/phaser/4.1.0/phaser.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/phaser/4.1.0/phaser.min.js"></script>
npm (bundlers / frameworks)
Install from npm and import it into your project:
npm install phaser
import Phaser from 'phaser';
This works with Vite, Webpack, Rollup, Parcel, ESBuild, and every major front-end framework (React, Vue, Angular, Svelte, SolidJS, Next.js).
create-phaser-game (scaffolding)
The fastest way to a fully configured project. The CLI presents an interactive selection of official templates and demo games:
npm create @phaserjs/game@latest
# or
npx @phaserjs/create-game@latest
# or with other package managers:
yarn create @phaserjs/game
pnpm create @phaserjs/game@latest
bun create @phaserjs/game@latest
Answer a few questions and it downloads and configures the right template for your chosen framework and bundler, in JavaScript or TypeScript.
Build sizes
Don't be alarmed if the full phaser.js file is over 8 MB — over 84% of that is inline documentation (JSDoc comments and type annotations) that powers your IDE autocomplete and the TypeScript definitions. It is not shipped to your players.
| Build | Raw size | Compressed (gzip) |
|---|---|---|
phaser.js (with docs) |
8.23 MB | — |
phaser.min.js (full) |
1.29 MB | 345 KB |
phaser-arcade.min.js |
1.18 MB | 313 KB |
The minified production build is 345 KB gzipped with every feature included — smaller than most title-screen images.
TypeScript
Full TypeScript definitions ship inside the package's types folder and are referenced by the types entry in package.json, so editors like VS Code detect them automatically. Depending on your project, you may need to add the following to tsconfig.json:
{
"compilerOptions": {
"lib": ["es6", "dom", "dom.iterable", "scripthost"],
"typeRoots": ["./node_modules/phaser/types"],
"types": ["Phaser"]
}
}
Alternative builds
If you don't need every feature, smaller pre-built entry points reduce bundle size:
| Build file | What it includes |
|---|---|
phaser.js |
Everything (Arcade + Matter physics, all features) |
phaser-arcade-physics.js |
Excludes Matter.js physics (Arcade only) |
phaser-no-physics.js |
Excludes both Arcade and Matter physics |
phaser-core.js |
Minimal core (basic game objects, no Geom/Tilemaps/full math) |
phaser-esm.js |
ES module build for tree-shaking bundlers |
Tip
If your game only uses Arcade Physics (the simpler, faster of the two physics systems), importing
phaser-arcade-physics.jssaves roughly 30 KB gzipped over the full build.
Your First Game, Step by Step
This walkthrough builds a tiny but complete game: a player sprite you move with the arrow keys, with platforms and gravity. It introduces the four ideas you'll use in every Phaser project — the game config, scenes, the loader, and game objects.
1. Create the Game and a Scene
Everything starts with a game config passed to new Phaser.Game(), and at least one Scene.
class GameScene extends Phaser.Scene {
constructor() {
super('GameScene'); // a unique key for this scene
}
preload() { /* load assets */ }
create() { /* build the level */ }
update() { /* per-frame logic */ }
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
physics: {
default: 'arcade',
arcade: { gravity: { y: 300 } }
},
scene: GameScene
};
const game = new Phaser.Game(config);
2. Load your assets in preload()
The preload() method queues files. Phaser downloads them all before create() runs.
preload() {
this.load.image('sky', 'assets/sky.png');
this.load.image('ground', 'assets/platform.png');
this.load.spritesheet('dude', 'assets/dude.png', {
frameWidth: 32,
frameHeight: 48
});
}
3. Build the level in create()
create() {
// Background — top-left origin would be (0,0); image origin defaults to center
this.add.image(400, 300, 'sky');
// Static platforms the player can stand on
this.platforms = this.physics.add.staticGroup();
this.platforms.create(400, 568, 'ground').setScale(2).refreshBody();
this.platforms.create(600, 400, 'ground');
this.platforms.create(50, 250, 'ground');
// The player — a physics sprite affected by gravity
this.player = this.physics.add.sprite(100, 450, 'dude');
this.player.setBounce(0.2);
this.player.setCollideWorldBounds(true);
// Make the player land on platforms
this.physics.add.collider(this.player, this.platforms);
// Read keyboard arrow keys
this.cursors = this.input.keyboard.createCursorKeys();
}
4. Move the player in update()
update() {
if (this.cursors.left.isDown) {
this.player.setVelocityX(-160);
} else if (this.cursors.right.isDown) {
this.player.setVelocityX(160);
} else {
this.player.setVelocityX(0);
}
// Jump only when touching the ground
if (this.cursors.up.isDown && this.player.body.touching.down) {
this.player.setVelocityY(-330);
}
}
That's a working platformer skeleton. The rest of this guide expands each of these systems in depth.
Tip
The official "Making your first Phaser Game" tutorial walks through a complete version of this game with scoring and enemies. There are over 700 tutorials and 2,000+ code examples on the Phaser website.
Core Concepts
Phaser is organized around a handful of concepts. Understanding these makes everything else fall into place.
The Game
Phaser.Game is the single top-level controller. You create exactly one per game. It parses your config, creates the renderer, sets up all the global systems (input, sound, textures, animations, scenes), and runs the main loop. You rarely touch it directly after creation — you work through Scenes instead.
Scenes
A Scene is the fundamental organizational unit. Think of scenes as the screens or states of your game: a boot scene, a preloader, a main menu, the gameplay level, a pause overlay, a game-over screen. Each Scene has:
- Its own lifecycle methods:
init(),preload(),create(),update(). - Its own display list, cameras, input handling, and loader.
- A set of injected systems accessed via
this.(for examplethis.add,this.load,this.physics,this.tweens).
Multiple scenes can run at once — for example, a gameplay scene with a separate UI overlay scene on top.
Game Objects
Anything you put on screen is a Game Object: a Sprite, an Image, Text, a Graphics object, a particle emitter, a tilemap layer. You create them through the Game Object Factory (this.add.*), which builds the object and adds it to the scene's display list.
this.add.image(x, y, 'texture'); // a static image
this.add.sprite(x, y, 'texture'); // a sprite (supports animation)
this.add.text(x, y, 'Hello'); // canvas text
The Game Loop
Every frame, Phaser:
- Updates global managers (input, sound).
- Calls
update(time, delta)on each active scene. - Renders every visible scene through the renderer.
You put per-frame logic in your scene's update() method. The delta value (milliseconds since the last frame) lets you write frame-rate-independent movement.
Textures and the Cache
Loaded assets live in global caches shared by every scene. Images and sprite sheets go into the Texture Manager (this.textures); JSON, audio, XML, and other data go into typed sub-caches under this.cache (this.cache.json, this.cache.audio, and so on). Because they're global, an image loaded in one scene is instantly available in every other scene.
Coordinate System
- The canvas origin
(0, 0)is the top-left corner;xincreases right,yincreases down. - Most game objects have their origin (anchor point) at their center
(0.5, 0.5)by default — sothis.add.image(400, 300, ...)places the image's center at(400, 300). (Text is the exception: its origin defaults to top-left.) - Rotation is measured clockwise;
angleis in degrees,rotationin radians.
Configuring Your Game
The game config object you pass to new Phaser.Game(config) controls every fundamental aspect of your game. Most properties have sensible defaults, so you only set what you need.
The most common options
const config = {
type: Phaser.AUTO, // renderer: AUTO, WEBGL, CANVAS, or HEADLESS
width: 800, // canvas width in pixels (default 1024)
height: 600, // canvas height in pixels (default 768)
parent: 'game-container', // id of the DOM element to inject the canvas into
backgroundColor: '#2d2d2d',
scene: [BootScene, GameScene], // one scene or an array
physics: {
default: 'arcade',
arcade: { gravity: { y: 300 }, debug: false }
}
};
Renderer types
| Constant | Value | Behavior |
|---|---|---|
Phaser.AUTO |
0 |
WebGL if supported, otherwise falls back to Canvas. Recommended. |
Phaser.CANVAS |
1 |
Force the Canvas renderer. |
Phaser.WEBGL |
2 |
Force WebGL. No fallback — fails if WebGL is unavailable. |
Phaser.HEADLESS |
3 |
No rendering (for unit testing; still requires a DOM). |
Caution
Using
Phaser.WEBGLdirectly will not fall back to Canvas if WebGL is unavailable, and the game can fail silently. PreferPhaser.AUTOunless you have a specific reason to force a renderer.
Pixel-art games
For crisp, non-blurry pixel art, set pixelArt: true. Phaser automatically disables texture smoothing and snaps to whole pixels.
const config = {
type: Phaser.AUTO,
width: 320,
height: 240,
pixelArt: true,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
zoom: Phaser.Scale.ZOOM_2X
},
scene: MyScene
};
Setting pixelArt: true automatically sets antialias: false, antialiasGL: false, and roundPixels: true.
Tip
Phaser 4 adds
smoothPixelArt(WebGL only). It preserves blocky pixels but smooths the edges between them when scaled up — a nicer look for some art styles. Setting it forcesantialias: trueandpixelArt: false.
Scaling and responsiveness
The scale sub-object controls how the canvas resizes to fit its container. The base game size stays constant; the scale mode decides how it's displayed.
const config = {
type: Phaser.AUTO,
scale: {
mode: Phaser.Scale.FIT,
parent: 'game-container',
autoCenter: Phaser.Scale.CENTER_BOTH,
width: 1280,
height: 720,
min: { width: 640, height: 360 },
max: { width: 1920, height: 1080 }
}
};
Scale modes:
| Constant | Behavior |
|---|---|
Phaser.Scale.NONE |
No scaling. Canvas stays at config size. |
Phaser.Scale.WIDTH_CONTROLS_HEIGHT |
Height adjusts based on width. |
Phaser.Scale.HEIGHT_CONTROLS_WIDTH |
Width adjusts based on height. |
Phaser.Scale.FIT |
Fit inside the parent, keeping aspect ratio (letterboxing). |
Phaser.Scale.ENVELOP |
Cover the parent, keeping aspect ratio (may overflow). |
Phaser.Scale.RESIZE |
Canvas resizes to fill the parent, ignoring aspect ratio. |
Phaser.Scale.EXPAND |
New in v4 — RESIZE-style visible area combined with FIT canvas scaling. |
Center modes: Phaser.Scale.NO_CENTER, CENTER_BOTH, CENTER_HORIZONTALLY, CENTER_VERTICALLY.
Zoom constants: Phaser.Scale.NO_ZOOM (1), ZOOM_2X (2), ZOOM_4X (4), MAX_ZOOM (-1, largest integer zoom that fits).
Frame rate
const config = {
fps: {
target: 60, // advisory only
limit: 30, // actually caps the frame rate (0 = no cap)
forceSetTimeOut: false,
smoothStep: true
}
};
Note
fps.targetis informational and does not enforce a frame rate. Usefps.limitto actually cap it. The browser's display refresh rate is always the upper bound — settinglimit: 120on a 60 Hz display still gives 60 FPS.
Transparent canvas over HTML
const config = {
transparent: true, // forces backgroundColor to rgba(0,0,0,0)
parent: 'game-container'
};
Disabling input subsystems
const config = {
input: {
keyboard: true,
mouse: true,
touch: false,
gamepad: false,
activePointers: 1
},
disableContextMenu: true // disable right-click menu over the canvas
};
For the complete list of every config option and its default, see the Admin Guide's Configuration Mastery section.
Working with Scenes
Scenes are where your game lives. This section covers the lifecycle, switching between scenes, running them in parallel, and passing data.
The Scene lifecycle
A scene moves through a defined sequence of states. The methods you override are called automatically:
class GameScene extends Phaser.Scene {
constructor() {
super('GameScene');
}
init(data) {
// Called FIRST. Receives data passed from scene.start('GameScene', data).
// Reset per-run state here (NOT in the constructor).
this.level = data.level || 1;
this.score = 0;
}
preload() {
// Called after init. Queue assets to load.
this.load.image('logo', 'assets/logo.png');
}
create(data) {
// Called after preload completes. Build the scene.
// 'data' is the same object passed to init.
this.add.image(400, 300, 'logo');
}
update(time, delta) {
// Called every frame while the scene is RUNNING.
}
}
The full state progression: PENDING → INIT → START → LOADING → CREATING → RUNNING, with PAUSED, SLEEPING, SHUTDOWN, and DESTROYED as later states.
- With a
preload():init()→preload()→ loader runs →create()→update()loop. - Without a
preload():init()→create()→update()loop (loading is skipped).
Important
Reset state in
init(), not the constructor. The constructor runs only once when the scene is first created.init()runs every time the scene starts or restarts — so it's the right place to zero out scores, flags, and arrays.
Scene-injected properties
Every scene gets a rich set of systems injected as this. properties:
| Property | What it is |
|---|---|
this.add |
Game Object Factory — creates and adds objects (this.add.sprite(...)) |
this.make |
Game Object Creator — creates objects without adding them to the display list |
this.load |
Loader — queue and load assets |
this.input |
Input plugin — keyboard, mouse, touch, gamepad |
this.physics |
Arcade Physics (if configured) |
this.matter |
Matter Physics (if configured) |
this.tweens |
Tween Manager |
this.time |
Clock — timers and delayed calls |
this.cameras |
Camera Manager |
this.anims |
Global Animation Manager |
this.sound |
Sound Manager |
this.scene |
Scene Plugin — start/stop/launch other scenes |
this.data |
This scene's local Data Manager |
this.registry |
Global Data Manager (shared across all scenes) |
this.children |
This scene's display list |
this.events |
This scene's event emitter |
Switching between scenes
The Scene Plugin (this.scene) controls all multi-scene orchestration.
// start() — shuts down the current scene and starts the target
this.scene.start('LevelTwo', { score: 100 });
// restart() — shuts down and restarts the SAME scene
this.scene.restart({ score: 0 });
// switch() — sleeps the current scene (state preserved), starts/wakes the target
this.scene.switch('PauseMenu');
// transition() — animated transition over a duration
this.scene.transition({
target: 'LevelTwo',
duration: 1000,
moveAbove: true,
data: { score: 100 }
});
Warning
this.scene.start('X')shuts down the calling scene. If you want both scenes running at the same time, uselaunch()orrun()instead.
Running scenes in parallel
// launch() — start another scene WITHOUT stopping this one
this.scene.launch('UIScene', { lives: 3 });
// run() — smart launcher: starts if not running, resumes if paused, wakes if sleeping
this.scene.run('UIScene');
// Control render order of parallel scenes
this.scene.bringToTop('UIScene');
this.scene.sendToBack('Background');
this.scene.moveAbove('GameScene', 'UIScene');
A classic pattern is a gameplay scene with a UI overlay launched alongside it:
class UIScene extends Phaser.Scene {
constructor() {
super({ key: 'UIScene', active: true }); // active: true auto-starts it
}
create() {
this.score = 0;
this.scoreText = this.add.text(10, 10, 'Score: 0');
// Listen for events from the gameplay scene
const gameScene = this.scene.get('GameScene');
gameScene.events.on('addScore', (points) => {
this.score += points;
this.scoreText.setText('Score: ' + this.score);
});
}
}
Passing data between scenes
There are several ways to share data:
// 1. Pass data on start/launch/restart (available in init AND create)
this.scene.start('LevelScene', { level: 5, score: 1200 });
// 2. The global registry — shared across ALL scenes
this.registry.set('playerHP', 100);
const hp = this.registry.get('playerHP');
// 3. This scene's local data manager
this.data.set('localValue', 42);
// 4. Direct reference to another scene
const other = this.scene.get('OtherScene');
other.somePublicProperty;
// 5. React to registry changes via events
this.registry.events.on('changedata-playerHP', (parent, value, prev) => {
// update the UI
});
Pausing, sleeping, and stopping
this.scene.pause(); // stops update loop, still RENDERS
this.scene.resume();
this.scene.sleep(); // stops update AND render, keeps state in memory
this.scene.wake();
this.scene.stop(); // full shutdown, clears display list and timers
// State checks
this.scene.isActive('OtherScene');
this.scene.isPaused('OtherScene');
this.scene.isSleeping('OtherScene');
Note
Paused scenes still render —
pause()only stops the update loop. Usesleep()to stop both updating and rendering while preserving the scene's state.
Cleaning up
Listen for the shutdown event to free resources that should be recreated on restart, and destroy for final cleanup:
create() {
this.events.once('shutdown', () => {
this.enemies = []; // clear arrays holding game object references
});
}
Caution
Stale references to destroyed game objects cause errors on scene restart. Always clear arrays and remove external event listeners on
shutdown.
Loading Assets
The Loader (this.load) fetches all external content — images, audio, JSON, tilemaps, fonts, atlases, and more. Assets are queued in preload(), downloaded in parallel, and placed into the global caches.
The preload pattern
preload() {
this.load.image('sky', 'assets/sky.png');
this.load.spritesheet('dude', 'assets/dude.png', { frameWidth: 32, frameHeight: 48 });
this.load.audio('jump', 'assets/jump.mp3');
}
create() {
// Everything above is guaranteed ready here
this.add.image(400, 300, 'sky');
this.add.sprite(100, 450, 'dude');
this.sound.play('jump');
}
Loading outside preload
If you queue files in create() or in response to a user action, the loader does not auto-start. Call this.load.start() yourself:
create() {
this.load.image('extra', 'assets/extra.png');
this.load.once('complete', () => {
this.add.image(400, 300, 'extra');
});
this.load.start();
}
File types reference
Every this.load method accepts positional arguments or a single config object, and most accept an array to batch-load.
Textures: image, spritesheet, atlas, atlasXML, multiatlas, unityAtlas, aseprite, svg, htmlTexture, texture (compressed)
Audio/Video: audio, audioSprite, video
Data: json, xml, text, binary, html, css, glsl
Fonts: bitmapFont, font
Tilemaps: tilemapTiledJSON, tilemapCSV, tilemapImpact
Other: animation, pack, script, scripts, plugin, scenePlugin, sceneFile
Common loads
preload() {
// Single image
this.load.image('star', 'assets/star.png');
// Sprite sheet (fixed-size frame grid)
this.load.spritesheet('explosion', 'assets/explosion.png', {
frameWidth: 64, frameHeight: 64, startFrame: 0, endFrame: 23
});
// Texture atlas (packed, named frames — e.g. from TexturePacker)
this.load.atlas('sprites', 'assets/sprites.png', 'assets/sprites.json');
// Audio with multiple formats for cross-browser support
this.load.audio('bgm', ['assets/music.ogg', 'assets/music.mp3']);
// JSON data
this.load.json('levelData', 'assets/level1.json');
// Tiled tilemap
this.load.tilemapTiledJSON('map', 'assets/map.json');
// Bitmap font (needs texture + XML/JSON font data)
this.load.bitmapFont('pixels', 'assets/font.png', 'assets/font.xml');
// Video
this.load.video('intro', 'assets/intro.mp4');
}
URL resolution
The final URL is baseURL + path + filename. Set them once and load relative paths:
preload() {
this.load.setBaseURL('https://cdn.example.com/');
this.load.setPath('assets/images/');
this.load.setPrefix('LEVEL1.'); // prepended to the CACHE KEY, not the URL
// Loads from https://cdn.example.com/assets/images/hero.png, cached as 'LEVEL1.hero'
this.load.image('hero', 'hero.png');
// Absolute URLs bypass baseURL/path
this.load.image('cloud', 'https://other.com/cloud.png');
}
Loading progress bar
preload() {
const bar = this.add.graphics();
this.load.on('progress', (value) => {
bar.clear();
bar.fillStyle(0xffffff, 1);
bar.fillRect(250, 280, 300 * value, 30); // value is 0..1
});
this.load.on('complete', () => bar.destroy());
// ...queue your assets...
}
Loader events
| Event | Fires when |
|---|---|
'start' |
Loader begins |
'progress' |
Overall progress updates (0–1) |
'fileprogress' |
Per-file download progress (if available) |
'load' |
A single file finishes downloading |
'filecomplete-{type}-{key}' |
A specific file is processed (e.g. filecomplete-image-hero) |
'loaderror' |
A file failed to load |
'complete' |
Everything in the queue is done |
Note
While assets are loading, your scene's
update()does not run (it's paused during preload). Thepreupdate,postupdate, andrenderevents still fire, so you can animate a loading screen.
Game Objects: Sprites and Images
Sprites and Images are the workhorses of most games. Both display a texture; the difference is that a Sprite can play frame-by-frame animations and an Image cannot.
Image vs Sprite
| Feature | Image | Sprite |
|---|---|---|
| Display a texture | Yes | Yes |
| Tint, alpha, flip, scale, rotate | Yes | Yes |
| Physics body | Yes | Yes |
| Input / hit area | Yes | Yes |
Frame-by-frame animation (play) |
No | Yes |
Tip
Use
Imagefor anything that doesn't need to animate — backgrounds, static props, UI. It skips the per-frame animation update and is slightly cheaper. Reach forSpriteonly when you need the Animation component.
Creating and positioning
// Static image
const bg = this.add.image(400, 300, 'background');
// Sprite (optionally with a starting frame)
const player = this.add.sprite(100, 200, 'player', 'idle-0');
// Position
player.setPosition(200, 300);
player.x = 250;
player.setRandomPosition(); // random within the game area
// Size
player.displayWidth = 128; // scaled width (adjusts scaleX)
player.setDisplaySize(128, 128);
Scaling and rotation
player.setScale(2); // uniform 2x
player.setScale(2, 1); // stretch horizontally
player.scaleX = 1.5;
player.setAngle(45); // degrees
player.setRotation(Math.PI); // radians
player.angle += 1; // spin a degree per frame in update()
Tint and alpha
// Tint (WebGL only) — multiply the texture color
player.setTint(0xff0000); // red tint
player.setTint(0xff0000, 0x00ff00, 0x0000ff, 0xffff00); // per-corner
player.clearTint();
// Phaser 4 tint modes: MULTIPLY (default), FILL, ADD, SCREEN, OVERLAY, HARD_LIGHT
player.setTint(0xff0000).setTintMode(Phaser.TintModes.FILL);
// Alpha (transparency), 0..1
player.setAlpha(0.5);
player.alpha = 1;
Important
In Phaser 4,
setTintFill()is removed. UsesetTint(color).setTintMode(Phaser.TintModes.FILL)instead.
Flipping
player.setFlipX(true); // face left
player.flipY = false;
player.setFlip(true, false);
player.toggleFlipX();
Flipping is rendering-only — it does not affect physics bodies or hit areas.
Origin and depth
// Origin is the anchor point, normalized 0..1 (default center 0.5, 0.5)
player.setOrigin(0, 0); // top-left
player.setOrigin(0.5, 1); // bottom-center (good for characters)
// Depth controls render order (higher = on top)
player.setDepth(10);
player.setToTop(); // move to top of the display list (no depth change)
Visibility and scroll factor
player.setVisible(false); // skips rendering, still updates
// Scroll factor: 0 = fixed to the camera (HUD), 1 = moves with the world (default)
hudIcon.setScrollFactor(0);
Chaining
Every setter returns the object, so you can chain:
this.add.sprite(100, 100, 'coin')
.setScale(0.5)
.setTint(0xffff00)
.setDepth(5)
.play('spin');
Other built-in game objects
Beyond Sprite and Image, the factory (this.add.*) creates many specialized objects:
| Factory call | What it makes |
|---|---|
this.add.text(x, y, str, style) |
Canvas-based styled text |
this.add.bitmapText(x, y, font, str, size) |
Fast pre-rendered bitmap text |
this.add.tileSprite(x, y, w, h, key) |
Repeating/scrolling texture (parallax) |
this.add.nineslice(...) |
Scalable UI panel with fixed corners |
this.add.graphics() |
Vector drawing (rectangles, circles, lines, paths) |
this.add.rectangle(x, y, w, h, color) |
A simple filled rectangle shape |
this.add.container(x, y) |
A transform-inheriting parent for children |
this.add.group() |
A logical collection / object pool |
this.add.layer() |
A render-order bucket |
this.add.particles(x, y, key, config) |
A particle emitter |
this.add.video(x, y, key) |
Video playback |
this.add.gradient(...) |
GPU color gradient (new in v4) |
The this.make.* equivalents create the same objects without adding them to the display list — useful when you want to configure an object before placing it, or build something off-screen.
Text and Bitmap Text
Phaser offers two text systems with different trade-offs.
Text vs BitmapText
| Feature | Text |
BitmapText |
|---|---|---|
| Rendering | Canvas 2D, uploaded as a texture | Pre-rendered font texture |
| Web/CSS fonts | Yes | No (uses a bitmap font file) |
| Shadows, gradients, strokes | Yes | Drop shadow only (WebGL) |
| Word wrap | Built-in | setMaxWidth |
| Performance | Slower (re-renders on change) | Fast (batched) |
Tip
Use
Textfor UI that needs flexible styling or web fonts and changes infrequently. UseBitmapTextfor scores, timers, damage numbers, and anything that updates every frame — it just updates vertex data instead of re-rendering a canvas.
Canvas Text
const title = this.add.text(400, 50, 'Hello World', {
fontFamily: 'Arial',
fontSize: '32px',
color: '#ffffff',
stroke: '#000000',
strokeThickness: 4,
shadow: { offsetX: 2, offsetY: 2, color: '#000', blur: 4, fill: true }
});
title.setOrigin(0.5); // Text origin defaults to (0,0), so center it manually
// Update content
title.setText('Score: 100');
title.setText(['Line 1', 'Line 2']); // array = multi-line
// Word wrap
const para = this.add.text(50, 50, longString, {
fontSize: '20px', color: '#fff', wordWrap: { width: 300 }
});
// Restyle after creation (all setters chain)
title.setColor('#00ffff').setFontSize(48).setAlign('center');
Caution
Phaser does not load web fonts for you. The Text object uses the browser's
fillText, so the font must already be available via CSS@font-faceor a font loader before you create the text. Font names with digits or spaces must be double-quoted inside the string:fontFamily: '"Press Start 2P"'.
Bitmap Text
// Load in preload: this.load.bitmapFont('pixelFont', 'font.png', 'font.xml');
const score = this.add.bitmapText(100, 100, 'pixelFont', 'Score: 0', 32);
score.setText('Score: 500');
score.setFontSize(48);
score.setCenterAlign();
// WebGL extras
score.setDropShadow(2, 2, 0x000000, 0.5);
score.setCharacterTint(0, 5, Phaser.TintModes.MULTIPLY, 0xff0000);
Animations
Animation in Phaser means playing a sequence of texture frames over time on a Sprite. There are two pieces: the global Animation Manager (this.anims) where you define animations, and the per-sprite Animation State (sprite.anims) that controls playback.
Defining and playing
// In preload — load a spritesheet
this.load.spritesheet('dude', 'dude.png', { frameWidth: 32, frameHeight: 48 });
// In create — define a GLOBAL animation (available to all sprites)
this.anims.create({
key: 'run',
frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 7 }),
frameRate: 10,
repeat: -1 // -1 = loop forever
});
// Play it on a sprite
const player = this.add.sprite(100, 300, 'dude');
player.play('run');
Frames from a spritesheet vs an atlas
// Spritesheet: numeric frame indices
frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 7 })
frames: this.anims.generateFrameNumbers('dude', { frames: [0, 1, 2, 1] }) // custom order
// Atlas: string frame names
frames: this.anims.generateFrameNames('gems', {
prefix: 'ruby_', start: 1, end: 6, zeroPad: 4 // ruby_0001 ... ruby_0006
})
Controlling playback
player.play('walk', true); // ignore if already playing this anim
player.playReverse('walk');
player.anims.playAfterDelay('walk', 1000);
player.anims.pause();
player.anims.resume();
player.anims.stop();
player.chain('idle'); // play 'idle' after the current anim finishes
player.chain(['fall', 'land', 'idle']);
Yoyo and repeat
this.anims.create({
key: 'pulse',
frames: this.anims.generateFrameNumbers('orb', { start: 0, end: 5 }),
frameRate: 10,
yoyo: true, // forward then backward
repeat: -1,
repeatDelay: 500
});
Animation events
player.on('animationcomplete', (anim) => {
console.log('done:', anim.key);
});
// Key-specific: only fires for 'explode'
sprite.on('animationcomplete-explode', (anim, frame, gameObject) => {
gameObject.destroy();
});
Available events: animationstart, animationupdate, animationrepeat, animationcomplete, animationcomplete-{key}, animationstop, animationrestart.
Important
An animation with
repeat: -1never firesanimationcompletebecause it never ends. To end an infinite animation, callstop()and listen foranimationstopinstead.
Note
Animations are global by default. Don't recreate the same
this.anims.create()in every scene — it logs a warning and returns the existing one. Guard withif (!this.anims.exists('walk')) { ... }if you might run a scene'screate()more than once.
Tweens
A tween smoothly changes one or more properties of an object over time — position, scale, alpha, rotation, anything numeric. Tweens are perfect for UI motion, easing, and juicy effects.
Basic tween
const logo = this.add.image(100, 300, 'logo');
this.tweens.add({
targets: logo,
x: 600,
duration: 2000,
ease: 'Power2'
});
this.tweens.add() creates the tween and starts it immediately. Tweens auto-destroy when they complete — you don't need to keep a reference unless you want to control them later.
Property values
this.tweens.add({
targets: sprite,
x: 400, // absolute value
y: '-=100', // relative (subtract 100 from current)
angle: '+=180', // relative
alpha: { value: 0, duration: 300, ease: 'Cubic.easeIn' }, // per-property config
scale: [0.5, 1.5, 1], // array: interpolate through these values
duration: 1000
});
Yoyo, repeat, and hold
this.tweens.add({
targets: coin,
y: '-=50',
duration: 600,
ease: 'Sine.easeInOut',
yoyo: true, // return to the start value
hold: 200, // pause at the end before yoyo
repeat: -1, // infinite
repeatDelay: 300
});
Multiple targets and stagger
this.tweens.add({
targets: [a, b, c],
y: 100,
duration: 1000,
delay: this.tweens.stagger(100) // 100ms between each target
// delay: this.tweens.stagger(200, { from: 'center' })
});
Tween chains (sequences)
this.tweens.chain({
targets: player,
tweens: [
{ x: 300, duration: 1000, ease: 'Power2' },
{ y: 500, duration: 800, ease: 'Bounce.easeOut' },
{ scale: 2, duration: 500 },
{ alpha: 0, duration: 400 }
],
onComplete: () => console.log('chain done')
});
Number tweens
Tween a plain number with no target object — great for counters:
this.tweens.addCounter({
from: 0,
to: 100,
duration: 2000,
onUpdate: (tween) => {
const value = Math.floor(tween.getValue());
this.scoreText.setText('Score: ' + value);
}
});
Easing functions
Use a string name in the ease property (case-insensitive). Types: Quad, Cubic, Quart, Quint, Sine, Expo, Circ, Elastic, Back, Bounce — each with .easeIn, .easeOut, .easeInOut variants (the bare name defaults to Out). Plus Linear, Stepped, and the Power0–Power4 aliases.
ease: 'Sine.easeInOut'
ease: 'Bounce.easeOut'
ease: 'Back' // = Back.easeOut
ease: (t) => t * t // custom: t is 0..1
Callbacks and control
const tween = this.tweens.add({
targets: sprite, x: 600, duration: 2000, persist: true,
onStart: () => {}, onComplete: () => {}
});
tween.pause(); tween.resume(); tween.restart(); tween.stop();
this.tweens.killTweensOf(sprite);
Note
Tweens auto-destroy after completing. If you need to replay one, set
persist: true(then calltween.destroy()yourself when done). A tween skips any property whose name starts with_.
Arcade Physics
Arcade Physics is Phaser's fast, lightweight physics system built for AABB (axis-aligned bounding box) and circular collisions. It's ideal for platformers, top-down games, and arcade-style action. (Phaser also ships Matter.js for full rigid-body physics — see the Admin Guide.)
Enabling physics
Turn it on in the game config:
const config = {
physics: {
default: 'arcade',
arcade: {
gravity: { y: 300 },
debug: false
}
}
};
Creating physics objects
Use this.physics.add.* to create objects with bodies:
const player = this.physics.add.sprite(100, 300, 'player'); // dynamic body
const coin = this.physics.add.image(300, 100, 'coin'); // dynamic body
const wall = this.physics.add.staticImage(400, 300, 'wall'); // static body
const platforms = this.physics.add.staticGroup(); // static group
// Add a body to an existing object
this.physics.add.existing(mySprite); // dynamic
this.physics.add.existing(mySprite, true); // static
Velocity, acceleration, gravity
player.setVelocity(200, -300); // x and y pixels/sec
player.setVelocityX(160);
player.setAcceleration(100, 0);
player.setMaxVelocity(300, 600);
player.setBounce(0.2); // rebound factor
player.setDrag(300); // deceleration when acceleration is 0
player.setCollideWorldBounds(true); // stay inside the world
player.body.gravity.y = 200; // extra per-body gravity
player.body.allowGravity = false; // exempt from world gravity
Collisions and overlaps
There are two checks: collide separates the bodies (pushes them apart); overlap only detects intersection without moving anything (use it for pickups and triggers).
// Persistent collider — checked automatically every frame (preferred)
this.physics.add.collider(player, platforms);
// Persistent overlap with a callback
this.physics.add.overlap(player, coins, collectCoin, null, this);
function collectCoin(player, coin) {
coin.disableBody(true, true); // disable the body and hide the coin
}
// Self-collision within a group
this.physics.add.collider(enemies, enemies);
// One-shot checks (call each frame in update)
this.physics.collide(player, platforms);
this.physics.overlap(player, coins, collectCoin);
Tip
A persistent
this.physics.add.collider()is checked automatically every frame.this.physics.collide()is a one-shot that you must call yourself every frame inupdate(). Prefer the persistent collider for things that always collide.
Physics groups
// Dynamic group — members get dynamic bodies automatically
const bullets = this.physics.add.group({
maxSize: 20,
collideWorldBounds: true,
allowGravity: false
});
// Static group — immovable platforms
const platforms = this.physics.add.staticGroup();
platforms.create(400, 568, 'ground').setScale(2).refreshBody();
Important
After changing the position, scale, or origin of a static body's game object, you must call
gameObject.refreshBody()(orbody.reset()). Static bodies do not auto-sync to transform changes.
World bounds and events
this.physics.world.setBounds(0, 0, 1600, 1200);
// Physics events require opt-in per body
player.body.onWorldBounds = true;
this.physics.world.on('worldbounds', (body, up, down, left, right) => {
console.log('hit edge');
});
Note
World events (
'collide','overlap','worldbounds') only fire if you set the relevant body flag (onCollide,onOverlap,onWorldBounds) totrue.
Useful helpers
this.physics.moveTo(sprite, targetX, targetY, 200); // move toward a point at speed
this.physics.accelerateTo(sprite, x, y, 300);
this.physics.velocityFromAngle(angle, speed, sprite.body.velocity);
const nearest = this.physics.closest(source, targets);
Caution
arcade.debug: truedraws body outlines and velocity vectors — invaluable while developing, but expensive. Always set it tofalsefor release builds.
Input: Keyboard, Mouse, Touch, Gamepad
Phaser provides a unified input system via this.input in any scene. You can handle input with event listeners or by polling state each frame.
Keyboard
create() {
// Cursor keys: { up, down, left, right, space, shift } Key objects
this.cursors = this.input.keyboard.createCursorKeys();
// Specific keys
this.keyW = this.input.keyboard.addKey('W');
this.keys = this.input.keyboard.addKeys('W,A,S,D');
// Custom-named mapping
this.wasd = this.input.keyboard.addKeys({
up: Phaser.Input.Keyboard.KeyCodes.W,
left: Phaser.Input.Keyboard.KeyCodes.A,
down: Phaser.Input.Keyboard.KeyCodes.S,
right: Phaser.Input.Keyboard.KeyCodes.D
});
// Event-driven
this.input.keyboard.on('keydown-SPACE', (event) => { /* fire */ });
}
update() {
// Polling — true every frame the key is held
if (this.cursors.left.isDown) { /* move left */ }
// JustDown — true only once per press
if (Phaser.Input.Keyboard.JustDown(this.cursors.space)) { /* jump */ }
}
Tip
To stop the browser scrolling when arrow keys or space are pressed, capture them:
this.input.keyboard.addCapture('SPACE,UP,DOWN,LEFT,RIGHT').
Mouse and pointer
// Scene-wide pointer events (anywhere on the canvas)
this.input.on('pointerdown', (pointer) => {
console.log('clicked at', pointer.x, pointer.y);
if (pointer.rightButtonDown()) { /* right click */ }
});
this.input.on('pointermove', (pointer) => {});
this.input.on('wheel', (pointer, over, dx, dy, dz) => {});
// Make a specific object clickable
const button = this.add.sprite(400, 300, 'button');
button.setInteractive();
button.on('pointerdown', (pointer, localX, localY, event) => {});
button.on('pointerover', () => button.setTint(0xcccccc));
button.on('pointerout', () => button.clearTint());
Custom hit areas and pixel-perfect hit testing:
sprite.setInteractive(new Phaser.Geom.Circle(32, 32, 32), Phaser.Geom.Circle.Contains);
sprite.setInteractive({ useHandCursor: true });
sprite.setInteractive(this.input.makePixelPerfect()); // expensive — use sparingly
Drag and drop
const item = this.add.sprite(400, 300, 'item').setInteractive();
this.input.setDraggable(item);
this.input.on('dragstart', (pointer, obj) => obj.setTint(0xff0000));
this.input.on('drag', (pointer, obj, dragX, dragY) => {
obj.x = dragX;
obj.y = dragY;
});
this.input.on('dragend', (pointer, obj) => obj.clearTint());
// Drop zones
const zone = this.add.zone(600, 300, 200, 200).setRectangleDropZone(200, 200);
this.input.on('drop', (pointer, obj, dropZone) => {
obj.x = dropZone.x;
obj.y = dropZone.y;
});
Touch
Touch works through the same pointer events. For multi-touch, reserve extra pointers:
this.input.addPointer(2); // now pointer1..pointer3 available (mouse is mousePointer)
Gamepad
Enable it in the config (input: { gamepad: true }), then:
this.input.gamepad.once('connected', (pad) => {
console.log('connected:', pad.id);
});
update() {
const pad = this.input.gamepad.pad1;
if (!pad) return;
if (pad.left) { /* d-pad left */ }
if (pad.A) { /* bottom face button */ }
if (pad.R2 > 0) { /* right trigger, 0..1 */ }
const lx = pad.leftStick.x; // -1..1
const ly = pad.leftStick.y;
}
Cameras
Every scene has a Camera Manager (this.cameras) and a default main camera (this.cameras.main). Cameras control what part of your game world is visible and where it renders on screen.
Scroll, zoom, and centering
const cam = this.cameras.main;
cam.setScroll(200, 100); // look at a different part of the world
cam.centerOn(400, 300); // center on a world coordinate
cam.setZoom(2); // 2x zoom in (< 1 zooms out)
cam.setBounds(0, 0, 2048, 2048); // constrain scrolling to the world
Following a target
// Instant follow
cam.startFollow(player);
// Smooth follow with lerp (lower = smoother), and an offset
cam.startFollow(player, true, 0.08, 0.08, 0, -50);
// Dead zone: camera only scrolls when the target leaves this rectangle
cam.setDeadzone(200, 150);
cam.stopFollow();
Camera effects
All effects return the camera for chaining:
cam.fadeIn(1000); // fade in from black
cam.fadeOut(1000, 0, 0, 0); // fade out to black (r,g,b)
cam.flash(250); // white flash
cam.shake(300, 0.02); // screen shake (duration, intensity)
cam.pan(800, 600, 2000, 'Sine.easeInOut');
cam.zoomTo(2, 1000);
Listen for completion:
cam.on(Phaser.Cameras.Scene2D.Events.FADE_OUT_COMPLETE, () => {
this.scene.start('GameOver');
});
Multiple cameras (HUD / minimap)
const main = this.cameras.main;
// A minimap in the corner
const minimap = this.cameras.add(600, 0, 200, 150).setZoom(0.2).setName('minimap');
// A non-scrolling HUD camera
const hud = this.cameras.add(0, 0, 800, 600).setScroll(0, 0);
// Each camera ignores the other's objects
main.ignore(hudGroup);
hud.ignore(worldGroup);
Screen-to-world conversion
When the camera is scrolled or zoomed, raw pointer coordinates are screen space. Convert them:
const worldPoint = cam.getWorldPoint(pointer.x, pointer.y);
// or use pointer.worldX / pointer.worldY directly
Caution
Never set camera zoom to exactly
0— it's clamped to a minimum of0.001but a zero zoom is a logic error.
Audio and Sound
Phaser's sound system (this.sound) abstracts over the Web Audio API and HTML5 Audio, automatically choosing the best backend and handling mobile autoplay restrictions.
Loading and playing
preload() {
this.load.audio('coin', ['assets/coin.ogg', 'assets/coin.mp3']);
this.load.audio('bgm', 'assets/music.mp3');
}
create() {
// Fire-and-forget (auto-destroys when done)
this.sound.play('coin');
this.sound.play('coin', { volume: 0.5, rate: 1.2 });
// Retained reference for ongoing control
this.music = this.sound.add('bgm', { loop: true, volume: 0.4 });
this.music.play();
}
Tip
Always provide multiple audio formats (OGG + MP3 at minimum). No single format works in every browser — Phaser picks the first one the browser supports. MP3 has the broadest support; OGG lacks Safari support; AAC/M4A works well on Safari/iOS.
Controlling sound
this.music.setVolume(0.5);
this.music.setRate(1.5); // 0.5 = half speed, 2 = double
this.music.setLoop(true);
this.music.setPan(-1); // -1 left, 0 center, 1 right (WebAudio)
this.music.pause();
this.music.resume();
this.music.stop();
// Global controls (affect all sounds)
this.sound.volume = 0.8;
this.sound.setMute(true);
this.sound.stopAll();
Mobile autoplay
Browsers block audio until the user interacts. Phaser handles unlocking automatically, but if you want to start music immediately on load, check the locked state:
if (this.sound.locked) {
this.sound.once('unlocked', () => this.sound.play('bgm'));
} else {
this.sound.play('bgm');
}
Note
There is one SoundManager per game, shared by all scenes. Looping sounds continue across scene changes unless you explicitly stop them. Sounds are not auto-cleaned on scene shutdown — stop or remove them yourself if needed.
Spatial audio (WebAudio only)
this.sound.setListenerPosition(player.x, player.y);
const roar = this.sound.add('roar', {
source: { x: 800, y: 300, refDistance: 50, maxDistance: 2000, follow: enemySprite }
});
roar.play();
Groups, Containers, and Layers
These three constructs organize collections of game objects, but they serve different purposes.
| Group | Container | Layer | |
|---|---|---|---|
| Purpose | Logical collection / object pool | Visual parent with shared transform | Render-order bucket |
| Has position/scale/rotation | No | Yes (children inherit) | No |
| On the display list | No (children are) | Yes (renders children) | Yes (renders children) |
| Object pooling | Yes | No | No |
| Factory | this.add.group() |
this.add.container(x, y) |
this.add.layer() |
Groups (and object pooling)
Use a Group to manage many similar objects — enemies, bullets, coins — and to pool them (reuse inactive objects instead of creating/destroying).
const bullets = this.add.group({
classType: Phaser.GameObjects.Sprite,
defaultKey: 'bullet',
maxSize: 30
});
function fireBullet(x, y) {
const bullet = bullets.get(x, y); // finds an inactive member or creates one
if (bullet) {
bullet.setActive(true).setVisible(true);
bullet.body.velocity.y = -300;
}
}
function killBullet(bullet) {
bullets.killAndHide(bullet); // active=false, visible=false, kept for reuse
}
Batch operations across the whole group:
enemies.setVisible(false);
enemies.setTint(0xff0000);
enemies.playAnimation('walk');
enemies.incX(5); // add 5 to every member's x
enemies.setXY(100, 200, 50, 0); // first at (100,200), each +50 x
Note
A Group is not on the display list — its children render individually. Moving a Group does nothing visually. If you need children to move together, use a Container.
Containers
A Container is a visual parent: its children inherit its position, rotation, scale, and alpha. Great for composite UI (health bars, inventory slots) and clusters that move as one.
const hud = this.add.container(10, 10);
hud.setScrollFactor(0); // pin to the camera
const bar = this.add.rectangle(0, 0, 200, 20, 0x00ff00);
const label = this.add.text(210, -5, '100 HP');
hud.add([bar, label]);
hud.setPosition(50, 50); // moves everything
hud.setScale(1.5); // scales children
hud.setAlpha(0.8); // fades all children
Important
A Container's origin is always
(0, 0)and cannot be changed — position children relative to that. Containers have a per-frame matrix cost per child, so prefer a Group or Layer when you don't need a shared transform. For input, you must callcontainer.setSize(w, h)beforesetInteractive().
Layers
A Layer is a lightweight render-order bucket with no transform. Use it to control which batches of objects draw on top of which.
const bgLayer = this.add.layer();
const entityLayer = this.add.layer();
const uiLayer = this.add.layer();
bgLayer.add(this.add.image(400, 300, 'sky'));
entityLayer.add(player);
uiLayer.add(scoreText);
bgLayer.setDepth(0);
entityLayer.setDepth(1);
uiLayer.setDepth(2);
Common Workflows (Recipes)
Practical patterns that combine the systems above.
A boot → preload → menu → game flow
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
scene: [BootScene, PreloadScene, MenuScene, GameScene]
};
Only the first scene in the array starts automatically. BootScene configures global settings, PreloadScene loads all assets with a progress bar, then this.scene.start('MenuScene').
A score that updates a separate UI scene
// GameScene emits an event
this.events.emit('addScore', 10);
// UIScene (launched in parallel) listens
const game = this.scene.get('GameScene');
game.events.on('addScore', (points) => {
this.score += points;
this.scoreText.setText('Score: ' + this.score);
});
Spawning enemies on a timer
create() {
this.time.addEvent({
delay: 1000,
loop: true,
callback: () => {
const x = Phaser.Math.Between(0, 800);
this.enemies.get(x, 0);
}
});
}
A pause overlay
// In GameScene
this.input.keyboard.on('keydown-P', () => {
this.scene.pause();
this.scene.launch('PauseMenu');
});
// In PauseMenu
this.input.keyboard.on('keydown-P', () => {
this.scene.stop();
this.scene.resume('GameScene');
});
Parallax scrolling background
create() {
this.bg = this.add.tileSprite(400, 300, 800, 600, 'clouds');
}
update() {
this.bg.tilePositionX += 0.5; // scroll the texture
}
Flash and respawn on hit
function onPlayerHit(player, enemy) {
this.cameras.main.shake(200, 0.01);
player.setTint(0xff0000);
this.time.delayedCall(500, () => player.clearTint());
}
Advanced Topics
Filters (post-processing effects)
Phaser 4 unifies the old FX and Mask systems into a single Filter system. Filters can be applied to any game object or camera (WebGL only). Each filter has two lists: internal (object-local, cheaper) and external (screen-space, full resolution).
sprite.enableFilters();
sprite.filters.internal.addGlow(0xff00ff, 4, 0, 1);
sprite.filters.external.addBlur(0, 2, 2, 1);
// Filters on a camera affect the whole view
this.cameras.main.filters.external.addBlur(1, 2);
// Masks are now a filter too
sprite.filters.internal.addMask(maskImage);
Built-in filters include Blur, Glow, Shadow, Pixelate, ColorMatrix, Bloom, Vignette, Wipe, Blend, ImageLight, Blocky, GradientMap, Quantize, and Key (chroma keying). See the Admin Guide for the full list.
Rendering massive numbers of objects
Phaser 4 adds two GPU-accelerated game objects for extreme counts:
SpriteGPULayerrenders a million or more sprites in a single draw call (up to 100x faster than individual sprites) by storing all member data in a static GPU buffer. Each member supports GPU-driven animations and per-member scroll factors for parallax.TilemapGPULayerrenders an entire tilemap layer as a single quad — cost is per-pixel, not per-tile — so it can display up to 4096×4096 tiles with no penalty for tile count.
const layer = this.add.spriteGPULayer('stars', 1000000);
const member = { x: 100, y: 200, frame: 'star', scaleX: 1, scaleY: 1, alpha: 1 };
layer.addMember(member);
Tilemaps
Load a Tiled JSON map and build layers:
preload() {
this.load.image('tiles', 'assets/tileset.png');
this.load.tilemapTiledJSON('map', 'assets/level.json');
}
create() {
const map = this.make.tilemap({ key: 'map' });
const tileset = map.addTilesetImage('tilesetName', 'tiles');
const ground = map.createLayer('Ground', tileset, 0, 0);
ground.setCollisionByProperty({ collides: true });
this.physics.add.collider(this.player, ground);
// GPU layer for huge maps (pass true as the last argument)
const gpuGround = map.createLayer('Ground', tileset, 0, 0, true);
}
Particles
const emitter = this.add.particles(400, 300, 'spark', {
speed: { min: -200, max: 200 },
lifespan: 1000,
scale: { start: 1, end: 0 },
blendMode: 'ADD'
});
emitter.explode(50); // burst of 50
Custom plugins
Register global or per-scene plugins via the config plugins object to extend Phaser with reusable systems (UI controls, state machines, etc.). The community maintains a large catalogue of plugins (see RexRainbow's collection).
Lighting
sprite.setLighting(true); // v4 — no pipeline juggling
sprite.setSelfShadow(true, 0.5, 1/3); // cast self-shadows from texture brightness
Lighting works across most game objects including BitmapText, Particles, TileSprite, and both tilemap layer types.
How It Works
A conceptual look at what Phaser does for you, useful when debugging.
The boot sequence
When you call new Phaser.Game(config):
- The config is parsed into a read-only
Configinstance (available later asgame.config). - Global managers are created: AnimationManager, TextureManager, CacheManager, InputManager, SceneManager, ScaleManager, SoundManager, TimeStep (the game loop), and PluginManager.
- Phaser waits for
DOMContentLoaded, then boots: creates the renderer, adds the canvas to the DOM, prints the console banner, and emitsBOOT. - Once textures are ready it emits
READYand starts the main loop.
You can hook into boot with callbacks.preBoot and callbacks.postBoot in the config, and listen for lifecycle events on game.events ('blur', 'focus', 'hidden', 'visible', 'pause', 'resume').
The per-frame step
Each animation frame, the TimeStep calls the game step, which:
- Emits
PRE_STEPandSTEP(global managers like input and sound update here). - Updates the Scene Manager — each active scene's
update(time, delta)runs (scenes update in reverse array order, so the top scene gets input priority first). - Emits
POST_STEP, then renders every visible scene in forward array order (so the top scene draws last, on top).
Why scenes update reverse but render forward
The top-most scene (last in the array) updates first so it can consume input before lower scenes, but renders last so it appears on top. This is why a UI overlay scene placed last in the config array sits above and intercepts input ahead of the gameplay scene.
The texture and cache model
Loaded images become GPU textures in the Texture Manager; everything else lands in typed sub-caches. Because these are owned by the Game (not a scene), assets persist across scene changes and are shared globally. This is why you load assets once in a preloader scene and use them everywhere.
Migrating from Phaser 3
Phaser 4 keeps most of the public API you know, but there are important breaking changes. If you only use the standard API (Sprites, Text, Tilemaps, Arcade Physics, tweens), most code works unchanged. The big-ticket items:
| Area | What changed |
|---|---|
| Renderer | The v3 Pipeline system is replaced by a node-based RenderNode architecture. Custom pipelines must be rewritten as render nodes. |
| FX & Masks | Unified into one Filter system. preFX/postFX → filters.internal/filters.external. BitmapMask → the Mask filter. |
| Tint | setTintFill() removed → use setTint(color).setTintMode(Phaser.TintModes.FILL). |
| Geometry | Geom.Point removed → use Vector2. All geometry helpers now return Vector2. |
| Math | Math.TAU now equals PI * 2 (was PI / 2). Use the new Math.PI_OVER_2 for the old value. Math.PI2 removed. |
| Data structures | Phaser.Struct.Set/Map replaced by native JS Set/Map. |
| Lighting | sprite.setPipeline('Light2D') → sprite.setLighting(true). |
| Round pixels | roundPixels now defaults to false (was true). |
| Removed objects | Mesh and Plane removed (proper 3D planned for the future). |
| DynamicTexture / RenderTexture | Must now call render() to execute buffered drawing commands. |
Important
Phaser 4 ships a detailed Migration Guide and an AI agent skill dedicated to v3→v4 migration. Point your AI coding assistant at the repository's
skills/v3-to-v4-migrationfolder and it can perform much of the conversion automatically.
There's a full migration checklist in the v4.0 Migration Guide that ships with the framework.
Troubleshooting
Error: Cannot create WebGL context, aborting.
Cause: You forced type: Phaser.WEBGL but the browser or device cannot create a WebGL context.
Solution: Switch to type: Phaser.AUTO so Phaser falls back to Canvas automatically.
Prevention: Only force Phaser.WEBGL when you specifically require WebGL features and have a fallback message for unsupported browsers.
Error: Image / texture shows as a green-and-black box
Cause: The texture key referenced in this.add.image(x, y, 'key') was never loaded, failed to load, or is misspelled. Phaser substitutes its built-in "missing texture" placeholder.
Solution: Verify the key matches exactly (keys are case-sensitive) and that the file path resolves. Check the Network tab for 404s and listen for this.load.on('loaderror', file => console.warn(file.key)).
Prevention: Run your game from a web server (not file://) and keep a single source of truth for asset keys.
Error: Assets never load and create() never runs
Cause: You queued files outside preload() (e.g. in create()) and didn't start the loader, or you're loading from file:// and CORS blocks the requests.
Solution: Call this.load.start() after queueing outside preload. Serve over HTTP: npx serve or python3 -m http.server.
Prevention: Do all initial loading in preload(), which auto-starts the loader.
Error: Cannot add Scene with duplicate key: X
Cause: Two scenes were registered with the same key, or you added a scene whose constructor sets a key that collides.
Solution: Give every scene a unique key in its super('Key') call or scene config.
Prevention: Keep a constants file listing every scene key.
Error: Sound doesn't play until I click
Cause: Browser autoplay policy blocks audio until the first user interaction. This is expected behavior, not a bug.
Solution: Trigger audio from a user action, or wait for the unlocked event: this.sound.once('unlocked', () => this.sound.play('bgm')).
Prevention: Design your menu so the first sound plays on a button press.
Error: A static platform doesn't collide where it's drawn
Cause: You scaled or moved a static-body game object without refreshing its body, so the physics body is still at the old size/position.
Solution: Call platform.refreshBody() (or body.reset()) after changing a static object's transform.
Prevention: Chain it at creation: group.create(x, y, 'ground').setScale(2).refreshBody().
Error: update() doesn't run during loading
Cause: A scene's update() is intentionally paused while its loader is running.
Solution: Animate your loading screen using the loader's progress event instead of update().
Prevention: Keep preload-time visuals event-driven.
Error: My HUD scrolls away with the world
Cause: HUD objects have the default scroll factor of 1, so they move with the camera.
Solution: Set hudObject.setScrollFactor(0), or render the HUD in a separate camera that doesn't scroll.
Prevention: Build HUDs in a dedicated UI scene or container pinned with setScrollFactor(0).
Error: Animation plays once and never repeats
Cause: The animation's repeat is 0 (the default), so it plays a single time.
Solution: Set repeat: -1 for an infinite loop in the this.anims.create() config.
Prevention: Decide loop behavior when defining the animation.
Error: Tint or filter has no effect
Cause: Tint, per-corner alpha, and filters are WebGL-only features. You're running under the Canvas renderer.
Solution: Use type: Phaser.AUTO or Phaser.WEBGL so WebGL is active when available.
Prevention: Don't force Phaser.CANVAS unless you specifically need it.
Error: this.physics is undefined
Cause: Physics wasn't enabled in the game config, so the plugin wasn't installed into the scene.
Solution: Add physics: { default: 'arcade', arcade: {} } to your config.
Prevention: Enable physics in the config before using this.physics.
Error: Restarting a scene keeps old state / throws on destroyed objects
Cause: State was initialized in the constructor (runs once) instead of init(), or stale references to destroyed objects survived the restart.
Solution: Move state resets into init() and clear object references on the shutdown event.
Prevention: Treat the constructor as one-time setup and init() as per-run setup.
Error: Text using a custom web font shows the wrong font
Cause: The font wasn't loaded by the browser before the Text object was created — Phaser doesn't load web fonts.
Solution: Load the font via CSS @font-face or a web font loader and wait for it to be ready before creating the Text. Quote font names with spaces: fontFamily: '"Press Start 2P"'.
Prevention: Preload web fonts in your boot scene.
Frequently Asked Questions
Q: Do I need a build step to use Phaser? No. You can use the CDN script tag with plain HTML and JavaScript. A bundler (via npm) is recommended for larger projects and TypeScript, but it's optional.
Q: JavaScript or TypeScript?
Both are fully supported. Phaser ships first-class TypeScript definitions. The create-phaser-game scaffolder offers both for most templates.
Q: How do I make my game full-screen / responsive?
Use the scale config with mode: Phaser.Scale.FIT (keeps aspect ratio) or RESIZE (fills the parent). For true fullscreen mode, call this.scale.startFullscreen() from a user gesture.
Q: What's the difference between this.add and this.make?
this.add.sprite(...) creates the object and adds it to the scene's display list. this.make.sprite(...) creates it without adding it — useful for off-screen construction or configuring before placement.
Q: Why is my image positioned by its center?
Game objects default to a center origin (0.5, 0.5). Call setOrigin(0, 0) for top-left positioning. (Text is the exception — it defaults to top-left.)
Q: When should I use Arcade Physics vs Matter.js? Use Arcade for platformers, top-down, and arcade action — it's fast and simple (AABB + circles). Use Matter.js when you need realistic rigid-body physics: stacking, slopes, joints, complex shapes. Matter is covered in the Admin Guide.
Q: How do I share data between scenes?
Use the global this.registry for game-wide data, pass an object to this.scene.start('X', data), or get a direct reference with this.scene.get('X'). See Passing data between scenes.
Q: How do I pool bullets/enemies for performance?
Use a Group with maxSize and get() / killAndHide(). See Groups (and object pooling).
Q: My game stutters on mobile. What can I help?
Disable arcade.debug, batch objects by texture and blend mode to reduce draw-call breaks, prefer Image over Sprite for static objects, use SpriteGPULayer/TilemapGPULayer for very high counts, and consider the pixelArt/autoMobileTextures config options.
Q: How do I pause the entire game (not just a scene)?
Call game.pause() to halt all updates and rendering, and game.resume() to continue. Check game.isPaused. To pause a single scene, use this.scene.pause().
Q: How do I destroy and recreate the game?
game.destroy(true) removes the canvas and cleans up. Pass game.destroy(true, true) only if you'll never create another Phaser instance on the page. It's asynchronous — listen for the DESTROY event.
Q: Where are the official examples and tutorials? The Phaser website hosts 2,000+ code examples, 700+ tutorials, full API docs at docs.phaser.io, and a free 500-page "Phaser by Example" book.
Q: Can AI assistants help me build Phaser games?
Yes — Phaser is well understood by major LLMs, and this repository ships a comprehensive set of AI agent skills (in the skills/ folder) covering every subsystem. Point your AI coding agent at them for idiomatic Phaser 4 code.
Q: How do I add a filter/effect to a sprite?
sprite.enableFilters() then sprite.filters.internal.addGlow(...) or sprite.filters.external.addBlur(...). Filters are WebGL-only. See Filters.
Q: My keyboard input also scrolls the page. How can I help?
Capture the keys: this.input.keyboard.addCapture('SPACE,UP,DOWN,LEFT,RIGHT'), which calls preventDefault on them.
Quick Reference
Renderer constants
| Constant | Value |
|---|---|
Phaser.AUTO |
0 |
Phaser.CANVAS |
1 |
Phaser.WEBGL |
2 |
Phaser.HEADLESS |
3 |
Scene lifecycle methods
| Method | When | Receives |
|---|---|---|
init(data) |
First, every start | data object |
preload() |
After init | — |
create(data) |
After loading | data object |
update(time, delta) |
Every frame | timestamp, ms since last frame |
Scene Plugin (this.scene) essentials
| Method | Effect |
|---|---|
start(key, data) |
Stop this scene, start target |
launch(key, data) |
Start target in parallel |
run(key, data) |
Start/resume/wake target |
restart(data) |
Restart this scene |
switch(key) |
Sleep this, start/wake target |
pause() / resume() |
Stop/start update (still renders) |
sleep() / wake() |
Stop/start update + render |
stop() |
Full shutdown |
get(key) |
Get a scene reference |
Common factory methods (this.add.*)
image, sprite, text, bitmapText, tileSprite, nineslice, graphics, rectangle, container, group, layer, particles, video, zone, gradient, noise. Physics variants: this.physics.add.sprite, image, staticImage, group, staticGroup, existing, collider, overlap.
Loader methods (this.load.*)
image, spritesheet, atlas, multiatlas, aseprite, svg, audio, audioSprite, video, json, xml, text, bitmapFont, font, tilemapTiledJSON, tilemapCSV, pack, plugin, script.
Top-level namespaces
Phaser.Game, Phaser.Scene, Phaser.GameObjects, Phaser.Physics (Arcade, Matter), Phaser.Input, Phaser.Loader, Phaser.Cameras, Phaser.Animations, Phaser.Tweens, Phaser.Sound, Phaser.Tilemaps, Phaser.Math, Phaser.Geom, Phaser.Display, Phaser.Curves, Phaser.Time, Phaser.Data, Phaser.Textures, Phaser.Scale, Phaser.Filters, Phaser.Utils.
Useful links
- API docs: https://docs.phaser.io
- Examples (2,000+): https://phaser.io/examples
- Tutorials (700+): https://phaser.io/learn
- Discord: https://discord.gg/phaser
- The architecture, build internals, security model, and full config reference live in the companion Admin Guide.