Guide

Phaser fundamentals explained

You add a <div id="game"> to a page, import Phaser from npm, and pass a config object with width, height, and a list of scenes. Press play and a canvas appears: a player sprite falls with gravity, platforms collide via Arcade physics, and a score label ticks up when you overlap a coin. That is Phaser 3 in practice — a mature JavaScript framework for 2D (and limited 3D) games that run in every modern browser without plugins. Phaser handles the render loop, asset loading, input, cameras, tweens, tilemaps, and two physics backends so you focus on game feel instead of WebGL boilerplate. It powers everything from jam entries and advergames to sustained live-service arcade titles embedded in portals. This guide covers the scene lifecycle, preload pipelines, Arcade and Matter physics, sprites and animations, tilemaps, input and cameras, scaling for mobile, a Harbor Arcade token-runner worked example, a framework decision table, common pitfalls, and a production checklist. Pair it with our Godot fundamentals guide when comparing engines, our game loop primer for timestep concepts, and TypeScript fundamentals for typed Phaser projects.

What Phaser is (and how it differs from PixiJS or a full engine)

Phaser is a client-side game framework, not a visual editor like Unity or Godot. You write JavaScript or TypeScript; Phaser owns the canvas (WebGL with Canvas fallback), the main update loop, and high-level game objects. Version 3 (current stable line) replaced the monolithic Phaser 2 API with a modular scene system, a cleaner physics plugin architecture, and better TypeScript definitions.

Compared to PixiJS (a rendering library), Phaser ships batteries included: scene management, loaders, physics, tilemaps, particles, and input abstractions. PixiJS gives finer control over the render graph when you are building a custom engine; Phaser gets a playable prototype faster when you want arcade collisions and sprite sheets on day one. Compared to Godot or Unity web exports, Phaser downloads are tiny (hundreds of kilobytes gzipped versus multi-megabyte WASM bundles) and integrate naturally into existing React, Vue, or static sites — at the cost of no built-in 3D editor and a code-first workflow.

Core subsystems at a glance

  • Scenes — self-contained states (Boot, Preload, Menu, Game, GameOver) with lifecycle hooks.
  • Game Objects — Sprites, Images, Text, Tilemaps, Containers, and Groups in a display list.
  • Loader — queues images, atlases, audio, tilemap JSON, and spine data before gameplay starts.
  • Arcade Physics — AABB collisions, velocity, gravity, overlap callbacks; fast and predictable for platformers.
  • Matter.js plugin — convex polygons, constraints, and realistic stacking when Arcade is too simple.
  • Input — keyboard, pointer, touch, and gamepad with per-scene listeners.
  • Cameras — follow targets, zoom, shake, fade, and split-screen for larger worlds.
  • Tweens and Time — declarative motion, timers, and delayed calls without manual easing math.

Bootstrapping a Phaser 3 project

A minimal Phaser game starts with a config object passed to new Phaser.Game(config):

import Phaser from 'phaser';
import { PreloadScene } from './scenes/PreloadScene';
import { GameScene } from './scenes/GameScene';

const config = {
  type: Phaser.AUTO,
  parent: 'game',
  width: 640,
  height: 360,
  backgroundColor: '#1a1a2e',
  physics: {
    default: 'arcade',
    arcade: { gravity: { y: 800 }, debug: false }
  },
  scale: {
    mode: Phaser.Scale.FIT,
    autoCenter: Phaser.Scale.CENTER_BOTH
  },
  scene: [PreloadScene, GameScene]
};

export const game = new Phaser.Game(config);

Phaser.AUTO picks WebGL when available. The scale block is not optional polish — mobile browsers need FIT or RESIZE modes so your fixed design resolution letterboxes cleanly on tall phones. Register scenes in start order; Phaser runs the first scene automatically unless you call scene.start() manually from Boot.

Build tooling

Most teams bundle with Vite or Webpack, importing Phaser as an npm dependency (phaser@3). Official templates exist for Vite + TypeScript. For quick jams, a CDN script tag still works, but production games benefit from tree-shaking unused plugins and pinning exact versions in lockfiles. Host assets under /public or import small files as URLs; large atlases belong on a CDN with cache headers.

Scene lifecycle and state management

Each scene is a class extending Phaser.Scene (or a plain object with hook functions). Phaser calls hooks in order:

  • init(data) — receives arguments from scene.start('Game', { level: 2 }).
  • preload() — queue assets with this.load.image(), this.load.spritesheet(), this.load.tilemapTiledJSON().
  • create() — build game objects after loads finish; safe to read textures and spawn sprites.
  • update(time, delta) — per-frame logic; delta is milliseconds since last frame (use for frame-rate independent movement).

Scenes can run in parallel (scene.launch('HUD') while Game runs) or replace each other (scene.start stops the current scene by default). For global state — score, unlocked skins, audio mute — use a plain JavaScript module or Phaser’s Registry (this.registry.set('score', 0)) rather than static globals on window. Registry events (registry.events.on('changedata-score')) let HUD scenes react without tight coupling.

Sprites, animations, and the display list

A Sprite is a textured quad that can play frame animations, flip, tint, and participate in physics. Create one in create():

const player = this.physics.add.sprite(100, 200, 'duck');
player.setCollideWorldBounds(true);
player.setBounce(0.1);
this.anims.create({
  key: 'walk',
  frames: this.anims.generateFrameNumbers('duck', { start: 0, end: 3 }),
  frameRate: 10,
  repeat: -1
});

The display list determines draw order (later objects appear on top). Use Containers to group UI elements; use Groups for pools of enemies or coins with group.create(x, y, key) and group.children.iterate for batch updates. For nine-slice UI panels and bitmap fonts, Phaser includes built-in Game Objects; DOM Element overlays are available when you need HTML form inputs above the canvas.

Texture atlases

Pack many frames into one PNG plus JSON (Texture Packer, Aseprite export, or free tools). Load with this.load.atlas('duck', 'duck.png', 'duck.json') and reference frame names in animations. Atlases reduce HTTP requests and improve batching — critical on mobile latency.

Arcade physics and collisions

Arcade Physics uses axis-aligned bounding boxes — fast, deterministic, and ideal for platformers, bullet hells, and top-down shooters. Enable on a sprite with physics.add.sprite or call this.physics.add.existing(gameObject) on custom objects.

Collision setup follows a consistent pattern:

const platforms = this.physics.add.staticGroup();
platforms.create(320, 320, 'ground').setScale(2).refreshBody();
this.physics.add.collider(player, platforms);
this.physics.add.overlap(player, tokens, collectToken, undefined, this);

collider separates bodies (walls, floors). overlap fires callbacks without physical response (pickups, hitboxes). Use processCallback to gate overlaps (only when player is attacking). Tune body.setSize(w, h) and body.setOffset(x, y) so feet align with visuals — default boxes often include transparent padding from the art.

When to reach for Matter.js

Enable the Matter plugin for rotating platforms, ropes, or debris piles. Matter is slower and less predictable than Arcade; hybrid projects use Arcade for the player and Matter for specific puzzle props. See our game physics guide for integration concepts that apply across engines.

Tilemaps and level design

Phaser reads maps exported from Tiled (JSON or CSV). Load the tileset image and map file in Preload, then:

const map = this.make.tilemap({ key: 'level1' });
const tileset = map.addTilesetImage('tiles', 'tiles');
const groundLayer = map.createLayer('Ground', tileset, 0, 0);
groundLayer.setCollisionByProperty({ collides: true });
this.physics.add.collider(player, groundLayer);

Object layers in Tiled spawn enemies, triggers, and spawn points — iterate map.getObjectLayer('Spawns').objects in create(). For large worlds, combine tilemaps with camera bounds and culling; Phaser 3 does not automatically cull off-screen tiles, so consider chunking or smaller maps for low-end phones.

Input, cameras, and game feel

Read movement in update() with this.cursors = this.input.keyboard.createCursorKeys() or pointer velocity for touch drags. For one-shot actions (jump, shoot), check Phaser.Input.Keyboard.JustDown(key) to avoid held-key repeat bugs.

Cameras follow the player with this.cameras.main.startFollow(player, true, 0.1, 0.1) — the lerp values smooth motion. Use camera.shake, camera.flash, and this.tweens.add for juice without bloating update(). Audio (this.sound.add) should respect a user-gesture unlock: browsers block autoplay until the first click; play a silent buffer on first interaction.

Frame-rate independence: multiply velocity by delta / 1000 when integrating custom motion outside Arcade helpers. Arcade velocity is already per-second, but tweens and timers should use explicit durations tested at both 60 Hz and 120 Hz displays.

Worked example: Harbor Arcade token runner

Harbor Arcade needs a embeddable browser mini-game: a duck runs on platforms, collects ten tokens, and reaches an exit door. Scope fits one week in Phaser 3 with Vite bundling — mirroring the Godot duck collector but native to the site’s JavaScript stack.

  1. Project scaffold — Vite + TypeScript, phaser dependency, index.html with a single #game div inside the arcade iframe shell.
  2. PreloadScene — load duck spritesheet, token image, tiles atlas, and level1.json from Tiled; display a progress bar with load.on('progress').
  3. GameScene create — tilemap ground layer with collision; spawn player at Tiled object PlayerSpawn; static group for exit door sensor.
  4. Token group — physics group of ten overlaps; collectToken disables body, plays tween fade, increments Registry score.
  5. Exit logic — overlap door checks registry.get('score') >= 10 before scene.start('Victory'); otherwise show floating hint text.
  6. HUD scene — parallel scene with score text bound to registry change events; no physics overhead.
  7. Mobile — on-screen jump button via pointer events; Scale.FIT with max width 800 CSS pixels.
  8. Deployvite build outputs to /games/token-runner/; nginx serves gzip brotli assets with long cache on hashed filenames.

Design choices align with our platformer design guide: first tokens sit on safe ground; a later token requires a double-jump gap introduced one screen earlier with a shallow pit. Playtest on throttled 3G to verify total load stays under two megabytes.

Framework decision table

NeedPrefer Phaser 3Consider alternative
Browser-first 2D arcade, embed in existing web appPhaser — small bundle, npm workflow, scene patternsPixiJS if you need custom render pipelines only
Desktop + mobile native binaries from one editorPhaser wraps in Capacitor or TauriGodot or Unity — mature export and tooling
3D or complex lightingPhaser 3D plugin is nicheThree.js, Babylon.js, or Godot
Physics-heavy puzzles (ropes, dominoes)Matter.js plugin inside PhaserRaw Matter.js or Unity 2D physics
Multiplayer authoritative serverPhaser client + separate Node/WebSocket serverSame pattern in any engine; Phaser does not ship netcode
Team knows only C# / GDScriptPhaser requires JavaScript fluencyUnity WebGL or Godot HTML5 export

Common pitfalls

  • Creating objects in preload() — textures are not ready until create(); sprites added too early render blank.
  • Forgetting refreshBody() on scaled static groups — platforms look correct but collision boxes stay tiny.
  • Physics debug left on in production — set debug: false and strip dev-only toggles from release builds.
  • Uncapped delta spikes — tab backgrounding produces huge delta; clamp to ~50 ms in custom integrators to avoid teleporting.
  • Loading uncompressed PNG forests — atlases and WebP (where supported) cut load time more than micro-optimizing update loops.
  • Single scene god-object — menus, gameplay, and pause in one 2,000-line scene; split scenes and use Registry for shared state.
  • No destroy cleanup — listeners and timers leak when restarting scenes; call scene.shutdown() hooks to remove events.
  • Autoplay audio before gesture — silent failure on mobile; gate music behind first tap.

Production checklist

  • Pin Phaser minor version in package.json; read migration notes before bumping.
  • Preload scene with progress UI; fail gracefully on missing assets with error scene.
  • Collision boxes visually verified with debug draw in dev builds only.
  • Scale mode tested on iPhone Safari, Android Chrome, and desktop 4K.
  • Total initial download budget documented (aim under 3 MB for casual arcade).
  • Registry or module state schema versioned for save-game migrations.
  • CI runs tsc --noEmit and a headless smoke test (Playwright canvas screenshot).
  • Analytics hooks on session start, level complete, and death without blocking the game thread.
  • Privacy-friendly error logging; no PII in client crash dumps.

Key takeaways

  • Phaser 3 is a code-first browser framework: scenes, loaders, and physics let you ship 2D games inside normal web stacks.
  • Arcade Physics covers most platformers and shooters; Matter adds complexity only where needed.
  • Tilemaps + texture atlases are the production path for levels and animation — not loose per-frame PNG loads.
  • Scale, input, and audio unlock matter as much as gameplay code for mobile browser players.
  • Phaser excels at embeddable arcade experiences; pair it with design guides so mechanics feel as good as the tech stack.

Related reading