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 fromscene.start('Game', { level: 2 }).preload()— queue assets withthis.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;deltais 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.
- Project scaffold — Vite + TypeScript,
phaserdependency,index.htmlwith a single#gamediv inside the arcade iframe shell. - PreloadScene — load
duckspritesheet,tokenimage,tilesatlas, andlevel1.jsonfrom Tiled; display a progress bar withload.on('progress'). - GameScene create — tilemap ground layer with collision; spawn player at Tiled object
PlayerSpawn; static group for exit door sensor. - Token group — physics group of ten overlaps;
collectTokendisables body, plays tween fade, increments Registry score. - Exit logic — overlap door checks
registry.get('score') >= 10beforescene.start('Victory'); otherwise show floating hint text. - HUD scene — parallel scene with score text bound to registry change events; no physics overhead.
- Mobile — on-screen jump button via pointer events;
Scale.FITwith max width 800 CSS pixels. - Deploy —
vite buildoutputs 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
| Need | Prefer Phaser 3 | Consider alternative |
|---|---|---|
| Browser-first 2D arcade, embed in existing web app | Phaser — small bundle, npm workflow, scene patterns | PixiJS if you need custom render pipelines only |
| Desktop + mobile native binaries from one editor | Phaser wraps in Capacitor or Tauri | Godot or Unity — mature export and tooling |
| 3D or complex lighting | Phaser 3D plugin is niche | Three.js, Babylon.js, or Godot |
| Physics-heavy puzzles (ropes, dominoes) | Matter.js plugin inside Phaser | Raw Matter.js or Unity 2D physics |
| Multiplayer authoritative server | Phaser client + separate Node/WebSocket server | Same pattern in any engine; Phaser does not ship netcode |
| Team knows only C# / GDScript | Phaser requires JavaScript fluency | Unity WebGL or Godot HTML5 export |
Common pitfalls
- Creating objects in
preload()— textures are not ready untilcreate(); 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: falseand 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 --noEmitand 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
- Godot fundamentals explained — scene trees and exports when you need native binaries too
- Platformer game design explained — movement feel, coyote time, and level teaching
- Game loop and frame timing explained — fixed vs variable timestep concepts
- Vite fundamentals explained — bundling Phaser with fast dev server and HMR