| name | phaser-game-development |
| description | Builds 2D HTML5 games with Phaser 3 + TypeScript + Vite (or a pinned CDN): scenes, Arcade/Matter physics, tilemaps, tweens, cameras, and a static dist/ bundle. Use when the user asks for Phaser, a 2D browser game with sprites/physics/scenes, or a web game engine. Not for raw Canvas2D (html5-canvas-game) or 3D scene graphs (three-js-web-game). Never inline binary game assets; keep Vite base './' so dist/ is portable. |
| version | 1.0.1 |
Phaser 3 Game Development
Phaser 3 is the dominant 2D web game framework: WebGL/Canvas renderer, scene system, asset loader,
Arcade + Matter physics, tweens, cameras, input. It runs anywhere Chrome runs and builds to a plain
static dist/ — ideal for the WEB delivery target. This skill is the structured 2D-web path; use
three-js-web-game for 3D and html5-canvas-game when you deliberately want no engine.
When to Use
- The user asks for a 2D browser game with sprites, physics, collisions, tilemaps, scenes, tweens, or a game loop.
- Trigger keywords: "Phaser", "make a browser game", "2D game", "sprite", "physics", "collision", "game scene", "web game with a game engine".
- You want a structured renderer, physics system, and scene management (vs. raw canvas in
html5-canvas-game).
- The game is 2D. For 3D, use
three-js-web-game instead.
- Art assets should be produced through
game-art-pipeline (AI art / SVG → textures & atlases) and loaded into Phaser.
Prerequisites
- Runtime: Chrome / Chromium with WebGL support.
- Node 18+ for the Vite + TypeScript toolchain.
- Windows host (primary): PowerShell is the primary shell. Commands below use
npm run scripts which are OS-agnostic. When using MSYS bash, use forward slashes in paths.
- Phaser 3 installed via
npm install phaser (Vite path) or pinned CDN URL (no-build path).
- Assets placed in
public/assets/ (Vite copies public/ verbatim; reference as assets/… in code).
Procedure
1. Scaffold — Vite + TypeScript (recommended)
npm create vite@latest my-game -- --template vanilla-ts
cd my-game
npm install phaser
npm pkg set scripts.dev="vite" scripts.build="vite build" scripts.preview="vite preview"
Create vite.config.ts — make the build path-relative so dist/ works from any subfolder or file://:
import { defineConfig } from 'vite';
export default defineConfig({
base: './',
server: { port: 5173, open: false },
build: { target: 'es2020', assetsInlineLimit: 0 },
});
Create index.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>My Game</title>
<style>
html, body { margin: 0; height: 100%; background: #0e0e1a; overflow: hidden; }
#app { width: 100vw; height: 100vh; display: grid; place-items: center; }
canvas { display: block; }
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></>
Put game assets in public/assets/ (Vite copies public/ verbatim; reference as assets/…).
CDN no-build alternative (prototype / single file)
<script src="https://cdn.jsdelivr.net/npm/phaser@3.80.1/dist/phaser.min.js"></script>
Pin the exact version (@3.80.1), never @3 or @latest — see pitfall #6.
2. Project structure
my-game/
├─ index.html
├─ vite.config.ts
├─ public/assets/ # png, svg, atlas json, audio (served as /assets/…)
└─ src/
├─ main.ts # Phaser.Game config + scene list
└─ scenes/
├─ BootScene.ts # preload the loader UI / global assets
├─ GameScene.ts # gameplay
└─ PauseScene.ts # overlay
3. Game config + scene lifecycle
Every scene extends Phaser.Scene and uses three lifecycle hooks:
preload() — queue asset loads. Nothing is available for use yet.
create() — assets are loaded; build the world, sprites, physics, input, tweens.
update(time, delta) — per-frame logic. delta is ms since last frame; scale movement by it.
src/main.ts:
import Phaser from 'phaser';
import { BootScene } from './scenes/BootScene';
import { GameScene } from './scenes/GameScene';
import { PauseScene } from './scenes/PauseScene';
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
parent: 'app',
backgroundColor: '#0e0e1a',
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
width: 960,
height: 540,
},
render: { antialias: true, roundPixels: true },
physics: { default: 'arcade', : { : { : }, : } },
: [, , ],
};
.(config);
Design at a fixed base resolution (960×540 here) and let Scale.FIT upscale. See pitfall #4 for
crisp text/HiDPI.
4. Concrete minimal scene (copy-paste, runs as-is)
src/scenes/GameScene.ts — player + Arcade physics + input + overlap + tween feedback + camera follow:
import Phaser from 'phaser';
export class GameScene extends Phaser.Scene {
private player!: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
private cursors!: Phaser.Types.Input.Keyboard.CursorKeys;
private coins!: Phaser.Physics.Arcade.Group;
private score = 0;
private scoreText!: Phaser.GameObjects.Text;
constructor() {
super('game');
}
preload() {
this.load.image('player', 'assets/player.png');
this.load.svg('coin', 'assets/coin.svg', { width: , : });
..(, );
}
() {
..(, , ).();
. = ...(, , );
..();
...(, ).(, );
. = ...();
( i = ; i < ; i++) {
..( + i * , + (i % ) * , );
}
...(
.,
.,
.(coin ...),
,
,
);
. = ..!.();
..!.(, .());
...(, , , );
...(, , , );
...(., , , );
. = ..(, , , { : , : })
.()
.(.);
..(..., ., );
.(, .);
}
() {
speed = ;
body = ..;
body.();
(...) body.(-speed);
(...) body.(speed);
(...) body.(-speed);
(...) body.(speed);
body..().(speed);
}
() {
coin.(, );
. += ;
..();
..({
: coin,
: { : , : },
: { : , : },
: ,
: ,
: coin.(),
});
}
() {
..();
..();
}
onVisibility = {
(. && !..()) .();
};
() {
..();
..();
..?.();
.(, .);
}
}
src/scenes/PauseScene.ts:
import Phaser from 'phaser';
export class PauseScene extends Phaser.Scene {
constructor() { super('pause'); }
create() {
this.add.rectangle(480, 270, 960, 540, 0x000000, 0.55);
this.add.text(480, 250, 'Paused', { fontSize: '48px', color: '#fff' }).setOrigin(0.5);
this.add.text(480, 310, 'Press P or Esc to resume', { fontSize: '20px', color: '#ccc' })
.setOrigin(0.5);
const resume = () => { this.scene.stop(); this..(); };
..!.(, resume);
..!.(, resume);
}
}
src/scenes/BootScene.ts (minimal — jump straight to game, or add a loader bar here):
import Phaser from 'phaser';
export class BootScene extends Phaser.Scene {
constructor() { super('boot'); }
create() { this.scene.start('game'); }
}
5. Art from AI/SVG (game-art-pipeline) + texture atlases
Produce art through game-art-pipeline, drop it in public/assets/, then load:
this.load.image('hero', 'assets/hero.png');
this.load.svg('shield', 'assets/shield.svg', { width: 128, height: 128 });
this.load.spritesheet('run', 'assets/run.png', { frameWidth: 48, frameHeight: 48 });
this.load.atlas('ui', 'assets/ui.png', 'assets/ui.json');
Use atlas frames and build animations from them:
this.add.image(100, 100, 'ui', 'button_play');
this.anims.create({
key: 'run',
frames: this.anims.generateFrameNumbers('run', { start: 0, end: 7 }),
frameRate: 12,
repeat: -1,
});
this.player.play('run');
Prefer one atlas over many loose PNGs — fewer HTTP requests and fewer WebGL texture binds (each
texture swap is a draw-call break). Pack related sprites together via game-art-pipeline.
6. Arcade physics essentials
- Body vs sprite: an Arcade body is an axis-aligned rectangle sized to the texture frame. It does
not rotate and does not follow non-uniform scale. Fit it explicitly:
sprite.body.setSize(w, h).setOffset(x, y);
sprite.body.setCircle(r, offX, offY);
collider vs overlap: this.physics.add.collider(a, b) resolves & blocks; overlap(a, b, cb)
only fires a callback (pickups, triggers, damage zones).
- Static geometry:
this.physics.add.staticGroup() for walls/platforms that never move.
- Debug: flip
arcade.debug: true in config to draw body outlines & velocity vectors — the fastest
way to diagnose a body/sprite mismatch. Turn it off for the shipped build.
- Need rotation, joints, stacking, or slopes? Switch
default: 'matter' and use this.matter.*.
7. Tweens — juicy feedback
Tweens are the cheapest way to make a game feel alive: hit-pops, screen-independent easing, pulses.
this.tweens.add({ targets: sprite, scale: 1.15, yoyo: true, duration: 120, ease: 'Sine.easeInOut' });
this.tweens.chain({ targets: door, tweens: [
{ x: '+=200', duration: 400, ease: 'Cubic.easeInOut' },
{ alpha: 0, duration: 200 },
]});
Always killAll()/kill the relevant tweens on scene shutdown so callbacks don't fire on destroyed
objects.
8. Camera
this.cameras.main.setBounds(0, 0, worldW, worldH);
this.cameras.main.startFollow(target, true, 0.08, 0.08);
this.cameras.main.setZoom(1.5);
this.cameras.main.fadeIn(400);
this.cameras.main.flash(150, 255, 255, 255);
Trauma-informed pacing (this is a trauma-support game — keep the player safe and in control):
9. Build → static web
npm run build
npm run preview
dist/ is a self-contained static bundle (thanks to base: './'). Drop it on any static host or open
via the preview server. Do a production build before shipping — dev-only issues (missing public/
asset, absolute path) surface only in build.
Pitfalls
- Using an asset before it's loaded. Anything referenced in
create()/update() must be queued
in preload() (or a prior scene's loader). Symptom: missing/green textures, Texture not found.
Fix: load in preload, or gate on this.load.once('complete', …).
- Physics body ≠ sprite. The default AABB body matches the texture frame, ignores rotation, and
doesn't track non-uniform scale — so collisions feel "off." Fix with
body.setSize/setOffset/setCircle
and confirm visually via arcade.debug: true.
- Memory leaks on scene restart.
scene.restart() / scene.start() recreates scene objects but
not listeners you attached to global targets (document, window, this.game.events) or
long-lived tweens/timers. Each restart stacks another listener → runaway callbacks, growing memory,
ghost input. Fix: register everything you add on Phaser.Scenes.Events.SHUTDOWN cleanup — remove DOM
listeners, tweens.killAll(), time.removeAllEvents(), keyboard.removeAllKeys().
- HiDPI blur. Phaser 3 has no working
resolution game-config prop; on Retina/HiDPI the FIT
upscale can soften text and thin art. Fix: design at a fixed base resolution and upscale with FIT;
for text call .setResolution(window.devicePixelRatio); for pixel-art games set
render: { pixelArt: true, antialias: false, roundPixels: true }.
- No pause / no blur handling. By default the loop keeps running when the tab loses focus — bad
for a calm, trauma-safe experience and it burns cycles. Add a Pause scene, a
P/Esc toggle, and a
visibilitychange auto-pause (shown above).
- CDN version drift.
phaser@3 or @latest in a <script> tag silently upgrades and can break
the game between sessions. Pin the exact version (phaser@3.80.1), or better, use the Vite path
with Phaser in package.json so the version is locked and bundled.
Verification
npm run dev and open http://localhost:5173 in Chrome.
- Open DevTools → Console must be clean. Common red flags:
Texture "x" not found / green-and-black boxes → asset not loaded in preload (pitfall #1).
Failed to load resource 404 → wrong path; assets live in public/assets/ → referenced as
assets/….
- WebGL context lost / warnings → too many textures or a leak on restart (pitfall #3).
- Screenshot the canvas and confirm: sprites render, player moves, collisions/overlaps fire,
tweens play, HUD is crisp, pause works, tab-away pauses.
- Run through
npm run preview too — verify the built bundle, not just dev.
Verify checklist: console clean · assets render · movement + collision/overlap fire · tweens play ·
HUD crisp on HiDPI · pause works · tab-blur auto-pauses · production preview matches dev.
Related skills
game-art-pipeline — produce AI art / SVG → textures & atlases for loading into Phaser.
html5-canvas-game — raw canvas games with no engine, when you want zero dependencies.
three-js-web-game — 3D web games with Three.js.