Jesse A. Green / notebook

Phaser User Guide

Generated June 2026 from phaserjs/phaser at 9e67ded

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

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

  1. Quick Start
  2. Installation
  3. Your First Game, Step by Step
  4. Core Concepts
  5. Configuring Your Game
  6. Working with Scenes
  7. Loading Assets
  8. Game Objects: Sprites and Images
  9. Text and Bitmap Text
  10. Animations
  11. Tweens
  12. Arcade Physics
  13. Input: Keyboard, Mouse, Touch, Gamepad
  14. Cameras
  15. Audio and Sound
  16. Groups, Containers, and Layers
  17. Common Workflows (Recipes)
  18. Advanced Topics
  19. How It Works
  20. Migrating from Phaser 3
  21. Troubleshooting
  22. Frequently Asked Questions
  23. 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 is npx serve or python3 -m http.server in your project folder, then open http://localhost:8000.

Tip

Phaser.AUTO tells 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.js saves 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 example this.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:

  1. Updates global managers (input, sound).
  2. Calls update(time, delta) on each active scene.
  3. 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; x increases right, y increases down.
  • Most game objects have their origin (anchor point) at their center (0.5, 0.5) by default — so this.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; angle is in degrees, rotation in 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.WEBGL directly will not fall back to Canvas if WebGL is unavailable, and the game can fail silently. Prefer Phaser.AUTO unless 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 forces antialias: true and pixelArt: 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.target is informational and does not enforce a frame rate. Use fps.limit to actually cap it. The browser's display refresh rate is always the upper bound — setting limit: 120 on 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, use launch() or run() 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. Use sleep() 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). The preupdate, postupdate, and render events 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 Image for anything that doesn't need to animate — backgrounds, static props, UI. It skips the per-frame animation update and is slightly cheaper. Reach for Sprite only 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. Use setTint(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 Text for UI that needs flexible styling or web fonts and changes infrequently. Use BitmapText for 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-face or 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: -1 never fires animationcomplete because it never ends. To end an infinite animation, call stop() and listen for animationstop instead.

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 with if (!this.anims.exists('walk')) { ... } if you might run a scene's create() 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 call tween.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 in update(). 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() (or body.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) to true.

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: true draws body outlines and velocity vectors — invaluable while developing, but expensive. Always set it to false for 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 of 0.001 but 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 call container.setSize(w, h) before setInteractive().

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:

  • SpriteGPULayer renders 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.
  • TilemapGPULayer renders 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):

  1. The config is parsed into a read-only Config instance (available later as game.config).
  2. Global managers are created: AnimationManager, TextureManager, CacheManager, InputManager, SceneManager, ScaleManager, SoundManager, TimeStep (the game loop), and PluginManager.
  3. Phaser waits for DOMContentLoaded, then boots: creates the renderer, adds the canvas to the DOM, prints the console banner, and emits BOOT.
  4. Once textures are ready it emits READY and 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:

  1. Emits PRE_STEP and STEP (global managers like input and sound update here).
  2. 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).
  3. 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-migration folder 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.

  • 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.