| name | phaser |
| description | Build 2D browser games with Phaser 4: scene lifecycle, sprites, Arcade/Matter physics, tilemaps, WebGL rendering, filters, lighting, shaders, DynamicTexture/RenderTexture, SpriteGPULayer, TilemapGPULayer, and Phaser 3 to 4 migration. Trigger: phaser, phaser 4, phaser v4, create phaser game, add phaser scene, phaser sprite, phaser physics, phaser tilemap, phaser arcade, phaser webgl renderer, phaser filters, phaser SpriteGPULayer, phaser TilemapGPULayer, migrate phaser 3 to 4. |
Phaser 4 Game Development
This skill covers Phaser 4 only. If a project lists phaser@^3, treat it as a migration target — read references/migration-hotspots.md first, then bump to phaser@^4. Do not start new work on v3.
Build 2D browser games using Phaser 4's WebGL-first renderer, scene model, and updated rendering APIs.
Core principles
Phaser 4 is not Phaser 3 with renamed methods — the renderer, filter model, shader assumptions, texture orientation, and batching all changed.
- WebGL-first, not Canvas-first: treat Canvas as legacy compatibility.
- Measure assets before loader config: most sprite/tile bugs are incorrect frame metadata, not rendering bugs.
- Prefer the simplest rendering path: standard game objects until scale or effects justify filters, GPU layers, or shaders.
- Migration is selective redesign: basic scene code ports cleanly; masks, FX, custom pipelines, shaders, and texture workflows usually need real updates.
STOP: Before Loading Any Spritesheet or Atlas
Read references/spritesheets-and-textures.md first. A few pixels off in frame size, spacing, or margin creates silent corruption that surfaces later as animation/rendering bugs. Never guess frame dimensions; don't assume texture orientation is irrelevant when compressed textures or custom shaders are involved.
STOP: Before Porting Phaser 3 Code
Read references/migration-hotspots.md first. Search for the APIs that changed meaning or disappeared — where most migration time goes:
setTintFill
BitmapMask
preFX / postFX
Phaser.Geom.Point
Math.TAU / Math.PI2
setPipeline('Light2D')
DynamicTexture / RenderTexture
- custom pipelines
- custom shader code
TileSprite cropping
Reference Files
Read these before working on the relevant feature:
| When working on... | Read first |
|---|
| Migrating Phaser 3 code | references/migration-hotspots.md |
| Loading spritesheets, atlases, compressed textures, or TileSprite | references/spritesheets-and-textures.md |
| Performance issues, GPU layers, filters, lighting, or batching | references/rendering-and-performance.md |
| Arcade physics, bodies, groups, pooling | references/arcade-physics.md |
| Tilemaps, object layers, collision setup | references/tilemaps.md |
Quick Start: Scaffold a Phaser 4 + Vite Project
For a fresh project (Vite + TypeScript + plain Phaser, no React unless the UI demands it):
npm create vite@latest my-game -- --template vanilla-ts
cd my-game
npm install phaser@^4
Minimal src/main.ts:
import Phaser from "phaser";
class GameScene extends Phaser.Scene {
constructor() {
super("Game");
}
create() {
this.add.text(20, 20, "Hello Phaser 4", { color: "#fff" });
}
}
new Phaser.Game({
type: Phaser.WEBGL,
parent: "game",
width: 800,
height: 600,
backgroundColor: "#1a1a2e",
scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH },
scene: [GameScene],
});
index.html needs <div id="game"></div> and <script type="module" src="/src/main.ts"></script>.
Engine version pin. Always pin phaser@^4 explicitly — npm's latest may flip back to phaser@3.x if 4.x sees a regression. Don't rely on phaser alone resolving to v4.
No image assets yet? Build textures procedurally in BootScene.preload() with this.add.graphics() + generateTexture("key", w, h) so the loop runs before any art exists. Replace later with real sprites (e.g. via vg generate).
ESM import gotcha — "Phaser is not defined". Phaser 4 ESM sets no global Phaser. The official vibedgames/phaserjs template's scene files import only import { Scene } from "phaser", so the moment you add code that uses the Phaser.* namespace as a runtime value — Phaser.Math.Clamp, Phaser.BlendModes.ADD, Phaser.TintModes.FILL, Phaser.Scale.RESIZE, Phaser.Scenes.Events, Phaser.Math.Angle — it throws ReferenceError: Phaser is not defined (and it fires at module-eval time if used at top level, so the whole scene fails to load). Fix: import the namespace as a value in any file that uses it:
import * as Phaser from "phaser";
export class Game extends Phaser.Scene { … }
Phaser.Types.* in type positions is erased at compile time and is fine either way — this only bites for runtime values. Also note setTintFill(c) is gone in Phaser 4 → use sprite.setTint(c).setTintMode(Phaser.TintModes.FILL) for a solid white hit-flash.
Architecture Decisions (Make Early)
Rendering Path Choice
| Path | Use when |
|---|
| Standard game objects | Most gameplay, UI, and ordinary animation |
SpriteGPULayer | Very large numbers of mostly simple quads or particle-like members |
TilemapGPULayer | Very large orthographic tile layers using one tileset |
RenderTexture / DynamicTexture | You need capture, compositing, stamping, or texture reuse |
| Filters / Shader | The effect is genuinely image-space or shader-driven |
Physics System Choice
| System | Use when |
|---|
| Arcade | Platformers, shooters, most 2D action games |
| Matter | Physics puzzles, compound bodies, more realistic collisions |
| None | Menu scenes, card games, visual novels, strategy UIs |
Scene Structure
scenes/
├── BootScene.ts # Asset loading, progress bar, shader/texture setup
├── MenuScene.ts # Title screen and options
├── GameScene.ts # Main gameplay
├── UIScene.ts # HUD overlay (launched in parallel)
└── GameOverScene.ts # End screen and restart flow
Scene Transitions
this.scene.start("GameScene", { level: 1 });
this.scene.launch("UIScene");
this.scene.pause("GameScene");
this.scene.stop("UIScene");
Core Patterns
Game Configuration
Prefer explicit WebGL unless there is a concrete reason not to.
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.WEBGL,
width: 800,
height: 600,
roundPixels: false,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
physics: {
default: "arcade",
arcade: { gravity: { y: 300 }, debug: false },
},
scene: [BootScene, MenuScene, GameScene],
};
Scene Lifecycle
class GameScene extends Phaser.Scene {
init(data: unknown) {}
preload() {}
create() {}
update(time: number, delta: number) {}
}
Frame-Rate Independent Movement
this.player.x += this.speed * (delta / 1000);
this.player.x += this.speed;
Phaser 4 Migration Replacements
sprite.setTintFill(0xff0000);
sprite.setTint(0xff0000).setTintMode(Phaser.TintModes.FILL);
sprite.setPipeline("Light2D");
sprite.setLighting(true);
const mask = new Phaser.Display.Masks.BitmapMask(scene, maskObject);
sprite.setMask(mask);
sprite.filters.internal.addMask(maskObject);
colorMatrix.sepia();
colorMatrix.colorMatrix.sepia();
Math.TAU;
Math.PI2;
Math.PI_OVER_2;
Math.TAU;
RenderTexture and DynamicTexture
Phaser 4 buffers drawing commands. Execute them deliberately.
const rt = this.add.renderTexture(0, 0, 256, 256);
rt.draw(sprite, 0, 0);
rt.render();
Use preserve() or render modes only when they solve a concrete problem. Extra indirection complicates debugging quickly.
TilemapGPULayer
const layer = map.createLayer("Ground", tileset, 0, 0, { gpu: true });
layer.generateLayerDataTexture();
Constraints: orthographic only, one tileset, max 4096×4096 tiles.
SpriteGPULayer
const layer = this.add.spriteGPULayer(texture, frameCount);
const memberConfig = {};
for (let i = 0; i < 100000; i++) {
memberConfig.x = Math.random() * 800;
memberConfig.y = Math.random() * 600;
memberConfig.scaleX = memberConfig.scaleY = 1;
memberConfig.alpha = 1;
layer.addMember(memberConfig);
}
Use it for starfields, particle-like swarms, animated backgrounds — not for interactive gameplay entities that mutate frequently.
Pixel Rounding
Do not assume old roundPixels behavior. Phaser 4 defaults it to false.
sprite.vertexRoundMode = "safe";
Use rounding intentionally for pixel art. Leave it off for rotated, scaled, or camera-heavy scenes.
Top-Down Depth & Fake Height
In a top-down 2D game, screen draw order is the only depth cue. Sort every sprite by its ground Y each frame so lower-on-screen draws in front — this is what lets the player walk behind a tree, then in front of it:
this.entities.forEach((e) => e.setDepth(e.y));
Fake the third dimension with a height value z: lift the sprite by z, but keep a separate shadow sprite (never bake the shadow into the art) on the true ground point, and sort by the ground Y — not the lifted position:
e.z = Math.max(0, e.z + e.vz * dt);
e.vz -= GRAVITY * dt;
e.sprite.setPosition(e.x, e.y - e.z).setDepth(e.y);
e.shadow.setPosition(e.x, e.y).setScale(1 - e.z * 0.002);
e.body.enable = e.z === 0;
The shadow sells the arc — without it a lifted sprite just looks like it slid up the screen.
Anti-Patterns to Avoid
| Anti-pattern | Why it hurts | Better |
|---|
| Treating Phaser 4 as a drop-in Phaser 3 upgrade | You miss renderer, filter, shader, and texture changes | Audit migration hotspots first, then port intentionally |
| Starting new work on Canvas-first assumptions | Many Phaser 4 features are WebGL-centric or unavailable in Canvas | Design for WebGL; treat Canvas as fallback only if required |
| Guessing spritesheet or atlas metadata | Visual corruption appears far from the actual mistake | Measure frames, spacing, margin, and bounds before loading |
| Using filters or shaders for every visual effect | More complexity, more batch breaks, harder debugging | Use plain sprites, textures, and tint where possible |
| Applying lighting or filters everywhere | Shader changes break batches and can tank performance | Reserve them for objects that benefit visually |
Forgetting render() on DynamicTexture or RenderTexture | Queued work never lands on the texture | Make render execution explicit in the workflow |
Using SpriteGPULayer for frequently mutated gameplay entities | Its strength is scale, not arbitrary object behavior | Keep complex interactive entities on normal game objects |
Assuming TilemapGPULayer is a universal tilemap replacement | It is orthographic-only and more constrained | Use it when the layer size and rendering profile justify it |
Making raw gl calls outside supported integration points | You can desync Phaser's renderer state | Use Extern or higher-level Phaser APIs |
| "The port compiles, so the migration is done" | Rendering, shader, and texture bugs survive the first compile | Re-test visuals explicitly after every render-touching change |
Variation Guidance
Don't converge on a single setup — choose per context. What varies is the architecture, not the rigor (always measure assets and check batching costs):
- Rendering path: standard objects vs GPU layers vs textures vs shader/filter pipelines
- Physics: Arcade vs Matter vs none
- Content: tilemaps vs pure sprites vs hybrid
- Pixel art:
roundPixels off, safe per-object rounding, or deliberate full rounding
- Assets: spritesheets vs atlases vs single textures
- Scene layout: separate
UIScene vs in-scene HUD
- Tilemap:
TilemapLayer vs TilemapGPULayer (only when constraints fit)