VOOZH about

URL: https://tech-insider.org/phaser-4-tutorial-browser-game-12-steps-2026/

⇱ Phaser 4 Tutorial: Build a Browser Game in 12 Steps [2026]


Skip to content
June 12, 2026
21 min read

Phaser is the most widely used open-source framework for building 2D games that run in a browser tab, and in 2026 it reached a turning point: Phaser 4.1.0 β€œSalusa” shipped on April 30, 2026, described by the core team as the biggest release in the framework’s history – faster, more reliable, and far easier to extend than the long-running Phaser 3 line. If you know basic JavaScript, you can have a playable game running in the browser within an hour, with no app store, no native build chain, and no licensing fees.

This tutorial walks through building a complete, working arcade game – Star Dodger, a top-down space shooter where the player pilots a ship, fires bullets, and destroys incoming asteroids while a score climbs. You will finish with a real project you can host anywhere static files live. The entire game uses procedurally generated textures, so you do not need to download a single image or sprite sheet to follow along. Every line of code is here.

We cover 12 numbered steps: environment setup, project scaffolding, scene architecture, the player ship, keyboard input, bullet pooling, asteroid spawning, physics collisions, scoring and UI, a game-over scene with restart, and finally a production build you can deploy. Along the way you will find common pitfalls, output examples, an eight-item troubleshooting section, and advanced tips for shipping a polished game.

Why Phaser in 2026: A Free, MIT-Licensed Browser Game Framework

Phaser is a complete HTML5 game framework: it wraps WebGL and Canvas rendering, a physics engine, asset loading, input handling, audio, animation, and a scene system into a single library you import with one line. It is fully open source under the MIT license, meaning you can build and sell commercial games with zero royalties or per-seat fees – a sharp contrast to the runtime-fee debates that have surrounded engines like Unity. There is no editor lock-in either: you write plain JavaScript or TypeScript in any editor you like.

The community is substantial. As of the Phaser 3.80 milestone, the project reported more than 36,000 GitHub stars, over 550 contributors, and more than 32,000 developers building with it. That maturity matters when you hit a problem at 2am: nearly every error message you can produce has already been asked and answered somewhere. Phaser has been used in production for over a decade across web games, playables, advergames, and browser-based casinos.

How does it compare to the alternatives for 2D web games? The short version: Phaser is a full game framework, while several popular tools are narrower libraries. The table below summarizes the landscape so you understand why we are choosing Phaser for a complete arcade game rather than a rendering-only library.

ToolTypeBuilt-in physicsBest forLicense
Phaser 4Full 2D game frameworkYes (Arcade + Matter)Complete browser gamesMIT
PixiJS2D rendering engineNo (add your own)Fast rendering, custom enginesMIT
Three.js3D rendering libraryNo3D scenes and WebGLMIT
Kaplay (Kaboom)Minimal game libraryBasicGame jams, tiny prototypesMIT
melonJS2D game engineYesTilemap-driven 2D gamesMIT
Phaser bundles rendering, physics, input, and audio; PixiJS and Three.js are rendering-only and require you to wire the rest yourself.

If you are weighing a heavier desktop engine instead, our Godot vs Unity 2026 comparison covers that tradeoff. For pure web delivery with instant load and no install, Phaser remains the pragmatic 2026 choice – and games ship as static files that even run well over cloud-gaming and low-end devices, a theme we explore in our GeForce Now vs Xbox Cloud guide.

Prerequisites: Node.js 20+, npm, and a Modern Browser

Before writing any game code, get your toolchain in place. Phaser itself is just a JavaScript library, but we will use a modern bundler (Vite) for instant hot-reload during development and an optimized build for production. Here are the exact versions this tutorial was written and tested against in June 2026.

RequirementVersion used hereWhy it matters
Node.js20.x LTS or newer (22.x fine)Runs the dev server and build tooling
npm10.x (ships with Node 20+)Installs Phaser and Vite
Phaser4.1.0 β€œSalusa”The game framework
ViteLatest 5.x via official templateDev server + production bundler
BrowserChrome/Edge/Firefox/Safari (current)WebGL2 rendering target
EditorVS Code (any editor works)Writing the code
Tested configuration. Phaser 3 projects use a near-identical API, but this guide targets Phaser 4.

Verify Node is installed and recent. Open a terminal and run the two commands below. If Node is missing or older than version 20, install the current LTS from the official Node.js site first.

node --version
# Expected output (or higher):
# v20.11.0

npm --version
# Expected output (or higher):
# 10.2.4

No prior game-development experience is required. You should be comfortable with JavaScript fundamentals – variables, functions, objects, arrow functions, and ES modules (import / export). If you have built a web app with any framework, you already know enough. Phaser’s concepts (scenes, sprites, physics bodies) are new, but we introduce each one as it appears.

Step 1: Scaffold the Project with the Official Phaser Template

The Phaser team ships official starter templates for every major bundler – Vite, Webpack, Parcel, and framework integrations for React, Vue, and Svelte. We use the Vite + JavaScript template because it gives the fastest dev loop with the least configuration. Run the create command, then pick the Vite (Basic) template when prompted.

npm create @phaserjs/game@latest star-dodger

# When prompted, choose:
# Framework/Bundler: Vite
# Language: JavaScript
# (decline analytics if asked)

cd star-dodger
npm install
npm run dev

Vite prints a local URL. Open it and you should see the template’s placeholder scene with the Phaser logo. That confirms Node, the bundler, and Phaser 4 are all working together. The expected terminal output looks like this:

 VITE v5.4.2 ready in 412 ms

 ➜ Local: http://localhost:8080/
 ➜ Network: use --host to expose
 ➜ press h + enter to show help

If you prefer to add Phaser to an existing project rather than scaffold a new one, you can simply run npm install phaser@latest and import it. The template route is recommended for beginners because it pre-wires the bundler, an index.html with a mount point, and a sensible folder layout. For background on the bundler we rely on, see the official Vite documentation.

Step 2: Understand the Project Structure and Entry Point

Open the project in your editor. The template produces a small, predictable tree. We will replace the contents of src/ with our own scenes, but the entry point and HTML host stay the same.

star-dodger/
β”œβ”€β”€ index.html # Mounts the game into a div
β”œβ”€β”€ package.json
β”œβ”€β”€ vite.config.js
└── src/
 β”œβ”€β”€ main.js # Phaser.Game config + scene list
 └── scenes/
 β”œβ”€β”€ Boot.js # We will create these
 β”œβ”€β”€ Game.js
 └── GameOver.js

The main.js file is the heart of every Phaser project: it builds a single Phaser.Game instance from a configuration object. That object declares the renderer type, canvas size, background color, physics engine, and – crucially – the ordered list of scenes. Replace src/main.js with the following. We will create the three scene files in the next steps.

import Phaser from 'phaser';
import Boot from './scenes/Boot.js';
import Game from './scenes/Game.js';
import GameOver from './scenes/GameOver.js';

const config = {
 type: Phaser.AUTO, // WebGL if available, Canvas fallback
 width: 800,
 height: 600,
 backgroundColor: '#0b0f2a',
 parent: 'game-container', // matches the div in index.html
 physics: {
 default: 'arcade',
 arcade: {
 gravity: { x: 0, y: 0 }, // top-down: no gravity
 debug: false
 }
 },
 scale: {
 mode: Phaser.Scale.FIT,
 autoCenter: Phaser.Scale.CENTER_BOTH
 },
 scene: [Boot, Game, GameOver]
};

export default new Phaser.Game(config);

Two settings deserve attention. type: Phaser.AUTO tells Phaser to use WebGL for hardware-accelerated rendering and silently fall back to Canvas2D on devices that lack it. The scale block makes the 800Γ—600 canvas resize to fit any screen while preserving aspect ratio and centering – the single most important line for mobile playability.

Step 3: Create the Boot Scene and Generate Textures in Code

A Scene is a self-contained slice of your game – a menu, a level, a game-over screen. Each scene has lifecycle methods Phaser calls automatically: preload() (load assets), create() (build the world once), and update() (run every frame). Our Boot scene does something slightly unusual: instead of loading image files, it draws the player ship, bullet, and asteroid into off-screen textures using the Graphics API, then registers them as reusable textures. This keeps the entire tutorial asset-free.

Create src/scenes/Boot.js with the code below.

import Phaser from 'phaser';

export default class Boot extends Phaser.Scene {
 constructor() {
 super('Boot');
 }

 create() {
 const g = this.add.graphics();

 // Player ship: a cyan triangle, 40x40
 g.fillStyle(0x4dd2ff, 1);
 g.fillTriangle(20, 0, 0, 40, 40, 40);
 g.generateTexture('ship', 40, 40);
 g.clear();

 // Bullet: a small yellow rectangle, 6x16
 g.fillStyle(0xffe066, 1);
 g.fillRect(0, 0, 6, 16);
 g.generateTexture('bullet', 6, 16);
 g.clear();

 // Asteroid: a grey circle, 44x44
 g.fillStyle(0x9aa3b2, 1);
 g.fillCircle(22, 22, 22);
 g.generateTexture('asteroid', 44, 44);
 g.destroy();

 this.scene.start('Game');
 }
}

The key method is generateTexture(key, width, height), which rasterizes whatever you have drawn into a named texture stored in Phaser’s texture manager. Once registered, any sprite can reference it by key – 'ship', 'bullet', 'asteroid' – exactly as if it had been loaded from a PNG. After creating all three, the scene immediately hands control to the Game scene with this.scene.start('Game').

Step 4: Build the Game Scene and Add the Player Ship

Now create the main gameplay scene. Start src/scenes/Game.js with a player sprite that has a physics body. A physics sprite (created via this.physics.add.sprite) is a normal sprite plus an Arcade Physics body that can move, collide, and respond to velocity. We constrain the ship to the screen with setCollideWorldBounds(true).

import Phaser from 'phaser';

export default class Game extends Phaser.Scene {
 constructor() {
 super('Game');
 }

 create() {
 // Player ship near the bottom-center
 this.player = this.physics.add.sprite(400, 540, 'ship');
 this.player.setCollideWorldBounds(true);
 this.player.body.setSize(32, 32); // tighter hitbox than the art

 // Tunable constants
 this.PLAYER_SPEED = 380;
 this.score = 0;
 this.isGameOver = false;

 // Input, bullets, asteroids, UI added in later steps
 }

 update() {
 // Per-frame logic added in later steps
 }
}

Save and the dev server hot-reloads. You should now see a cyan triangular ship sitting near the bottom of a dark blue canvas. It does not move yet – that is the next step. Notice we set a slightly smaller physics body (setSize(32, 32)) than the 40Γ—40 art; a forgiving hitbox makes arcade games feel fairer and is a trick used in nearly every shipped shooter.

Step 5: Capture Keyboard Input and Move the Ship

Phaser’s input system exposes the keyboard, pointer (mouse/touch), and gamepad. For movement we register the arrow keys plus WASD so both control schemes work. Input objects are created once in create(), then read every frame in update(). Add the input setup to the end of create():

// --- inside create(), after the player setup ---
this.cursors = this.input.keyboard.createCursorKeys();
this.keys = this.input.keyboard.addKeys('W,A,S,D,SPACE');

Now drive the ship’s velocity in update(). Setting velocity (rather than directly changing x/y) lets the physics engine handle smooth, frame-rate-independent movement and world-bounds collision for you.

update() {
 if (this.isGameOver) return;

 const body = this.player.body;
 body.setVelocity(0);

 const left = this.cursors.left.isDown || this.keys.A.isDown;
 const right = this.cursors.right.isDown || this.keys.D.isDown;
 const up = this.cursors.up.isDown || this.keys.W.isDown;
 const down = this.cursors.down.isDown || this.keys.S.isDown;

 if (left) body.setVelocityX(-this.PLAYER_SPEED);
 if (right) body.setVelocityX(this.PLAYER_SPEED);
 if (up) body.setVelocityY(-this.PLAYER_SPEED);
 if (down) body.setVelocityY(this.PLAYER_SPEED);

 // Normalize diagonal speed so it is not faster
 body.velocity.normalize().scale(this.PLAYER_SPEED);
}

Reload and you can fly the ship with the arrow keys or WASD. The normalize().scale() call fixes a classic bug: without it, holding two direction keys produces ~1.41Γ— the intended speed because the X and Y velocities add as a vector. This is one of the most common pitfalls in beginner movement code, and now it is handled.

Step 6: Fire Bullets with an Object Pool (Groups)

Shooters create and destroy hundreds of bullets per minute. Allocating and garbage-collecting a fresh sprite for each shot causes frame stutter. The professional solution is object pooling: pre-allocate a fixed set of bullet sprites in a Group, then recycle inactive ones. Phaser’s physics groups make this almost free. Add a bullet group and a fire-rate timer in create():

// --- inside create() ---
this.bullets = this.physics.add.group({
 defaultKey: 'bullet',
 maxSize: 32
});
this.lastFired = 0;
this.FIRE_DELAY = 180; // milliseconds between shots

Then handle firing in update(). We use group.get(), which returns a recycled inactive sprite (or creates one up to maxSize). The time argument Phaser passes to update(time, delta) lets us throttle the fire rate without a separate timer.

update(time, delta) {
 if (this.isGameOver) return;

 // ... movement code from Step 5 ...

 const firePressed = this.keys.SPACE.isDown;
 if (firePressed && time > this.lastFired) {
 const bullet = this.bullets.get(this.player.x, this.player.y - 24);
 if (bullet) {
 bullet.setActive(true).setVisible(true);
 bullet.body.enable = true;
 bullet.setVelocityY(-520);
 this.lastFired = time + this.FIRE_DELAY;
 }
 }

 // Recycle bullets that fly off the top
 this.bullets.children.each((b) => {
 if (b.active && b.y < -20) this.killBullet(b);
 });
}

killBullet(b) {
 b.setActive(false).setVisible(false);
 b.body.enable = false;
}

Hold Space and a stream of yellow bullets rises from the ship. Each bullet that leaves the top of the screen is deactivated and returned to the pool rather than destroyed, so memory use stays flat no matter how long you play. This pattern – get, activate, recycle – is the single most important performance habit in Phaser game development.

Step 7: Spawn Asteroids on a Timed Event

Enemies need to appear continuously. Phaser's time system provides this.time.addEvent, a repeating timer that calls a function on an interval – perfect for spawning. Asteroids drop from random X positions above the top edge and drift downward. Add the asteroid group and spawner to create():

// --- inside create() ---
this.asteroids = this.physics.add.group();

this.spawnTimer = this.time.addEvent({
 delay: 700, // every 0.7s
 loop: true,
 callback: this.spawnAsteroid,
 callbackScope: this
});

Now define the spawn method on the class. We randomize the horizontal position and the fall speed so the difficulty feels organic, and we use a Phaser helper, Phaser.Math.Between, for clean integer ranges.

spawnAsteroid() {
 if (this.isGameOver) return;

 const x = Phaser.Math.Between(30, 770);
 const rock = this.asteroids.create(x, -30, 'asteroid');
 rock.setVelocityY(Phaser.Math.Between(120, 260));
 rock.body.setCircle(22); // round hitbox
 rock.setAngularVelocity(Phaser.Math.Between(-120, 120));
}

Reload and grey asteroids now rain down at varying speeds and spins. They pass straight through the ship and bullets for now because we have not defined collisions – that is Step 8. Note the setCircle(22) call: a circular physics body matches the round art far better than the default rectangle, which would clip corners and feel wrong.

Step 8: Wire Up Collisions and Overlap Detection

Arcade Physics offers two interaction methods. this.physics.add.collider makes bodies physically bounce off each other; this.physics.add.overlap detects when two bodies intersect without a physical reaction, which is exactly what shooters want – a bullet should trigger a hit, not bounce. Add both relationships at the end of create():

// --- inside create() ---
// Bullet hits asteroid: destroy both, add score
this.physics.add.overlap(
 this.bullets, this.asteroids,
 this.hitAsteroid, null, this
);

// Asteroid hits player: game over
this.physics.add.overlap(
 this.player, this.asteroids,
 this.hitPlayer, null, this
);

Now implement the two callbacks. Phaser passes the two overlapping objects as arguments, in the same order as the groups you registered. We recycle the bullet, destroy the asteroid, bump the score, and – on a player hit – flip the game-over flag and transition scenes.

hitAsteroid(bullet, asteroid) {
 this.killBullet(bullet);
 asteroid.destroy();
 this.score += 10;
 this.scoreText.setText('SCORE: ' + this.score);
}

hitPlayer(player, asteroid) {
 if (this.isGameOver) return;
 this.isGameOver = true;
 this.physics.pause();
 player.setTint(0xff4d4d);
 this.spawnTimer.remove();

 this.time.delayedCall(900, () => {
 this.scene.start('GameOver', { score: this.score });
 });
}

Shoot an asteroid and it vanishes; fly into one and the ship turns red, physics freeze, and after a short pause the game-over scene loads with your score. We pass data between scenes via the second argument to scene.start – a plain object the next scene receives in its init(). The scoreText reference does not exist yet; we create it in Step 9.

Step 9: Add the Score and On-Screen UI Text

Text in Phaser is a game object like any other, created with this.add.text(x, y, string, style). UI elements should render above the action, so we set a high depth. Add this to create():

// --- inside create() ---
this.scoreText = this.add.text(16, 16, 'SCORE: 0', {
 fontFamily: 'monospace',
 fontSize: '24px',
 color: '#ffffff'
}).setDepth(10);

// A subtle hint that fades after a few seconds
const hint = this.add.text(400, 300,
 'Arrows/WASD to move β€’ SPACE to shoot', {
 fontFamily: 'monospace', fontSize: '18px', color: '#7f8cb0'
 }).setOrigin(0.5).setDepth(10);

this.tweens.add({
 targets: hint, alpha: 0, delay: 2500, duration: 800,
 onComplete: () => hint.destroy()
});

The hint introduces tweens – Phaser's animation system for smoothly interpolating any numeric property over time. Here we fade the hint's alpha from 1 to 0 after a delay, then destroy it. Tweens are how you add juice: screen shakes, scale pops, color flashes, and smooth menu transitions all build on this same API. Reload and your live score now updates with every asteroid destroyed.

Step 10: Build the Game Over Scene with Restart

The final scene displays the score and lets the player restart. It demonstrates the init(data) lifecycle method, which receives the object passed from scene.start('GameOver', { score }). We also persist a high score to localStorage so it survives page reloads. Create src/scenes/GameOver.js:

import Phaser from 'phaser';

export default class GameOver extends Phaser.Scene {
 constructor() {
 super('GameOver');
 }

 init(data) {
 this.finalScore = data.score || 0;
 }

 create() {
 const best = Math.max(
 this.finalScore,
 Number(localStorage.getItem('starDodgerBest') || 0)
 );
 localStorage.setItem('starDodgerBest', best);

 this.add.text(400, 220, 'GAME OVER', {
 fontFamily: 'monospace', fontSize: '56px', color: '#ff4d4d'
 }).setOrigin(0.5);

 this.add.text(400, 300, 'Score: ' + this.finalScore, {
 fontFamily: 'monospace', fontSize: '28px', color: '#ffffff'
 }).setOrigin(0.5);

 this.add.text(400, 340, 'Best: ' + best, {
 fontFamily: 'monospace', fontSize: '20px', color: '#ffe066'
 }).setOrigin(0.5);

 const btn = this.add.text(400, 420, 'β–Ά PLAY AGAIN', {
 fontFamily: 'monospace', fontSize: '26px', color: '#4dd2ff'
 }).setOrigin(0.5).setInteractive({ useHandCursor: true });

 btn.on('pointerdown', () => this.scene.start('Game'));
 this.input.keyboard.once('keydown-SPACE',
 () => this.scene.start('Game'));
 }
}

This introduces interactive objects: calling setInteractive() on the button text makes it respond to pointer events, and pointerdown fires on both click and touch – so the restart button works on mobile with no extra code. We also bind the Space key as a keyboard shortcut. Restarting calls scene.start('Game'), which re-runs the Game scene's create() from scratch, resetting score and state cleanly.

Step 11: Ramp Difficulty and Polish the Feel

A game that never gets harder gets boring. Add a simple difficulty curve by shortening the spawn delay as the score climbs. Because we stored the timer in this.spawnTimer, we can adjust it live. Add this block to the top of update() (after the game-over guard):

// --- inside update(), difficulty scaling ---
const targetDelay = Math.max(220, 700 - this.score * 2);
if (this.spawnTimer.delay !== targetDelay) {
 this.spawnTimer.reset({
 delay: targetDelay,
 loop: true,
 callback: this.spawnAsteroid,
 callbackScope: this
 });
}

Now asteroids spawn faster the better you play, capped at one every 220ms so it stays survivable. For extra polish, add a tween that flashes the screen edge red on a near miss, or use this.cameras.main.shake(120, 0.004) inside hitPlayer for a satisfying impact. Small touches like camera shake, a particle burst on asteroid destruction, and a rising audio pitch on combo streaks are what separate a prototype from something players want to share.

Step 12: Build for Production and Deploy as Static Files

Your game is complete. To ship it, run the Vite build, which bundles and minifies everything into a dist/ folder of plain static assets – HTML, JavaScript, and nothing that needs a server runtime. This is the great advantage of browser games: deployment is just file hosting.

npm run build

# Output:
# vite v5.4.2 building for production...
# βœ“ 41 modules transformed.
# dist/index.html 0.46 kB
# dist/assets/index-a1b2c3d4.js 1,180.22 kB β”‚ gzip: 310.51 kB
# βœ“ built in 3.84s

# Preview the production build locally:
npm run preview

Upload the contents of dist/ to any static host – Netlify, Vercel, Cloudflare Pages, GitHub Pages, or even an S3 bucket. There is no backend, no database, and no server cost for a single-player game like this. The Phaser library is the bulk of the bundle (~310 kB gzipped); for production you can tree-shake unused modules with a custom build, but the default is perfectly acceptable for launch. Congratulations – you have shipped a complete browser game.

Common Pitfalls When Building Your First Phaser Game

Beginners hit the same handful of issues repeatedly. Knowing them in advance saves hours of debugging.

  • Diagonal speed boost: Setting both X and Y velocity without normalizing makes diagonal movement ~41% faster. Always call body.velocity.normalize().scale(speed), as in Step 5.
  • Forgetting this.physics.add: Using this.add.sprite creates a sprite with no physics body, so velocity and collisions silently do nothing. For anything that moves or collides, use this.physics.add.sprite.
  • Destroying pooled bullets: Calling .destroy() on group members defeats pooling and causes GC stutter. Deactivate with setActive(false).setVisible(false) and disable the body instead.
  • Reading input in create(): create() runs once. Key states must be polled in update(), which runs every frame. A common bug is checking cursors.left.isDown in create() and wondering why nothing moves.
  • Mismatched hitboxes: The default physics body is a rectangle the size of the texture. Round sprites need setCircle(); otherwise corners trigger phantom collisions that feel unfair.
  • Mutating scene state across restarts: Putting game variables on the module scope instead of the scene instance leaves stale values after restart. Initialize all state inside create().

Troubleshooting: Eight Errors and Their Fixes

When something breaks, the browser console (F12) is your first stop. Here are the eight problems readers report most often, with the exact fix for each.

SymptomLikely causeFix
Blank/black canvas, no errorsScene never started or wrong parent divConfirm parent in config matches the div id, and Boot calls scene.start('Game')
Cannot read properties of undefined (reading 'body')Sprite created without physicsUse this.physics.add.sprite, not this.add.sprite
Ship does not moveInput read in create()Move the velocity checks into update()
Bullets fire only oncePool exhausted; bullets never recycledDeactivate off-screen bullets and disable their body
Texture 'ship' not foundBoot scene ran after Game, or typo in keyList Boot first in the scene array; match key strings exactly
Collisions never fireBodies disabled or wrong group orderEnsure bodies are enabled; check argument order in the callback
Game runs faster on high-refresh monitorsMoving by pixels instead of velocityUse physics velocity, or multiply movement by delta
Build works in dev, blank on hostWrong base path on subfolder hostingSet base: './' in vite.config.js
The eight most common Phaser 4 beginner issues and their one-line fixes.

A general debugging tip: set arcade: { debug: true } in your physics config. Phaser will draw every body's outline and velocity vector in real time, instantly revealing mismatched hitboxes, disabled bodies, and objects that have drifted off-screen but never recycled.

Advanced Tips: Audio, Particles, Mobile, and TypeScript

Once the core loop works, these upgrades take the game from prototype to polished. Each builds on a system Phaser already includes.

  • Audio: Load sounds in a preload step and play them with this.sound.play('laser'). Phaser uses the Web Audio API with an HTML5 Audio fallback and handles the browser autoplay-unlock-on-first-input requirement for you.
  • Particles: Replace the plain asteroid destruction with this.add.particles for an explosion burst. A 12-particle emitter that lives 300ms transforms the game's feel for a few lines of code.
  • Touch controls: Because we used pointer events and a FIT scale mode, the game is already playable on phones. For movement, read this.input.activePointer and steer the ship toward the touch position.
  • TypeScript: Phaser ships complete type definitions. Choosing the TypeScript template at scaffold time gives you autocomplete on every method and catches key-name typos at compile time – well worth it on larger projects. See the TypeScript docs to get started.
  • Tilemaps and levels: For platformers and RPGs, design levels in the free Tiled editor and load them with Phaser's tilemap API – far more maintainable than hand-placing objects in code.
  • Save state and leaderboards: We used localStorage for a local high score. For global leaderboards, post scores to a small API; our Bun REST API tutorial shows how to stand one up quickly.

For deeper reference while you build, the framework's GitHub releases page documents every API change, the Phaser overview on Wikipedia gives historical context, and Mozilla's MDN game development hub explains the underlying browser APIs Phaser sits on top of.

The Complete Game Scene Reference

For convenience, here is the full Game.js with every step assembled. Drop this in alongside the Boot and GameOver scenes from Steps 3 and 10, and you have the entire working project.

import Phaser from 'phaser';

export default class Game extends Phaser.Scene {
 constructor() { super('Game'); }

 create() {
 this.player = this.physics.add.sprite(400, 540, 'ship');
 this.player.setCollideWorldBounds(true);
 this.player.body.setSize(32, 32);

 this.PLAYER_SPEED = 380;
 this.FIRE_DELAY = 180;
 this.lastFired = 0;
 this.score = 0;
 this.isGameOver = false;

 this.cursors = this.input.keyboard.createCursorKeys();
 this.keys = this.input.keyboard.addKeys('W,A,S,D,SPACE');

 this.bullets = this.physics.add.group({ defaultKey: 'bullet', maxSize: 32 });
 this.asteroids = this.physics.add.group();

 this.spawnTimer = this.time.addEvent({
 delay: 700, loop: true,
 callback: this.spawnAsteroid, callbackScope: this
 });

 this.scoreText = this.add.text(16, 16, 'SCORE: 0', {
 fontFamily: 'monospace', fontSize: '24px', color: '#ffffff'
 }).setDepth(10);

 this.physics.add.overlap(this.bullets, this.asteroids, this.hitAsteroid, null, this);
 this.physics.add.overlap(this.player, this.asteroids, this.hitPlayer, null, this);
 }

 update(time) {
 if (this.isGameOver) return;

 const targetDelay = Math.max(220, 700 - this.score * 2);
 if (this.spawnTimer.delay !== targetDelay) {
 this.spawnTimer.reset({ delay: targetDelay, loop: true,
 callback: this.spawnAsteroid, callbackScope: this });
 }

 const body = this.player.body;
 body.setVelocity(0);
 if (this.cursors.left.isDown || this.keys.A.isDown) body.setVelocityX(-this.PLAYER_SPEED);
 if (this.cursors.right.isDown || this.keys.D.isDown) body.setVelocityX(this.PLAYER_SPEED);
 if (this.cursors.up.isDown || this.keys.W.isDown) body.setVelocityY(-this.PLAYER_SPEED);
 if (this.cursors.down.isDown || this.keys.S.isDown) body.setVelocityY(this.PLAYER_SPEED);
 body.velocity.normalize().scale(this.PLAYER_SPEED);

 if (this.keys.SPACE.isDown && time > this.lastFired) {
 const bullet = this.bullets.get(this.player.x, this.player.y - 24);
 if (bullet) {
 bullet.setActive(true).setVisible(true);
 bullet.body.enable = true;
 bullet.setVelocityY(-520);
 this.lastFired = time + this.FIRE_DELAY;
 }
 }
 this.bullets.children.each((b) => { if (b.active && b.y < -20) this.killBullet(b); });
 }

 spawnAsteroid() {
 if (this.isGameOver) return;
 const rock = this.asteroids.create(Phaser.Math.Between(30, 770), -30, 'asteroid');
 rock.setVelocityY(Phaser.Math.Between(120, 260));
 rock.body.setCircle(22);
 rock.setAngularVelocity(Phaser.Math.Between(-120, 120));
 }

 killBullet(b) { b.setActive(false).setVisible(false); b.body.enable = false; }

 hitAsteroid(bullet, asteroid) {
 this.killBullet(bullet);
 asteroid.destroy();
 this.score += 10;
 this.scoreText.setText('SCORE: ' + this.score);
 }

 hitPlayer(player, asteroid) {
 if (this.isGameOver) return;
 this.isGameOver = true;
 this.physics.pause();
 player.setTint(0xff4d4d);
 this.cameras.main.shake(150, 0.005);
 this.spawnTimer.remove();
 this.time.delayedCall(900, () => this.scene.start('GameOver', { score: this.score }));
 }
}

Frequently Asked Questions

Is Phaser free to use for commercial games?

Yes. Phaser is released under the permissive MIT license, so you can build, sell, and monetize commercial games with no royalties, runtime fees, or per-seat costs. This is one of its biggest advantages over engines that charge based on revenue or installs.

Should I use Phaser 3 or Phaser 4 in 2026?

For new projects, use Phaser 4 – version 4.1.0 "Salusa" shipped on April 30, 2024, and Phaser 4 is the actively developed line, offering better performance and an easier-to-extend architecture. Phaser 3 (last in the 3.80 series) remains stable and has more years of community tutorials, but new development is focused on Phaser 4. The core gameplay API used in this tutorial is nearly identical across both.

Do I need to know TypeScript?

No. This entire tutorial is written in plain JavaScript and runs as-is. Phaser does ship full TypeScript definitions, and for larger projects the autocomplete and compile-time checks are valuable, but JavaScript is perfectly suitable for shipping real games.

Can Phaser games run on mobile?

Yes. Because Phaser renders to an HTML5 canvas and supports pointer (touch) events natively, games run in any mobile browser. With the FIT scale mode used here, your game automatically resizes to any screen. You can also wrap a Phaser game in Capacitor or a WebView to publish to app stores.

How large is a Phaser game bundle?

The Phaser library is the bulk of the bundle, roughly 300 kB gzipped in a default build. That loads quickly on modern connections, and because the output is static files, it caches aggressively. For size-sensitive projects you can produce a custom build that strips unused modules.

What can I build next after this arcade game?

The same systems – scenes, physics, groups, input, and tweens – scale to platformers, top-down RPGs, puzzle games, and endless runners. Add the tilemap API for level design, the particle system for effects, and a small backend for global leaderboards. Each genre reuses the foundation you just built.

Why generate textures in code instead of loading images?

For this tutorial it keeps the project fully self-contained – no asset downloads, no broken file paths. In a real game you would load PNG sprites or sprite atlases in a preload() step. The generateTexture technique is also genuinely useful for placeholder art, simple shapes, and procedurally generated visuals.

Does Phaser support multiplayer?

Phaser handles the client side; for real-time multiplayer you pair it with a server over WebSockets (for example using Colyseus or a custom Node server). The client renders state and sends inputs while the server is authoritative. That is a larger topic, but Phaser imposes no limits on it.

Related Coverage

You now have a complete, working browser game built with Phaser 4 – from an empty folder to a deployable production bundle in twelve steps. The patterns here (scene architecture, physics groups, object pooling, tweened polish, and scene-to-scene data) are the same ones used in commercially shipped Phaser titles. Clone the structure, swap in your own art and mechanics, and ship something.

πŸ‘ Nadia Dubois

Nadia Dubois

AI & Innovation Editor

Nadia Dubois is the AI & Innovation Editor at Tech Insider, where she tracks the rapid evolution of artificial intelligence, from foundation models to real-world enterprise deployment. She previously covered AI and startups for La Tribune and contributed to MIT Technology Review's European coverage. Nadia specializes in generative AI, AI regulation, and the intersection of technology and European industrial policy. She holds a dual degree in Computational Linguistics and Journalism from Sciences Po Paris.

View all articles
πŸ‘ Tech Insider
Tech
Insider

Tech Insider delivers in-depth coverage of the technologies shaping the future: AI, cybersecurity, cloud computing, hardware, and the trends that matter.

Company

Explore

Categories

Β© 2026 Tech Insider Media AB. All rights reserved.