Phaser has just crossed a milestone that had been years in the making: version 4 shipped on 10 April 2026, followed by 4.1 at the end of the month and by 4.2 "Giedi" on 19 June. The renderer has been rewritten from the ground up, yet the API that thousands of developers know by heart has not moved an inch. In other words: this is a very good moment to build your first 2D game with Phaser, without fearing that everything will be obsolete in six months.
This guide starts from nothing and ends with a small game you can actually play in the browser: a character who runs, jumps, lands on platforms and collects stars for points. Set aside an hour, a code editor and a bit of curiosity.
Why Phaser is still a safe bet in 2026
Phaser has been serving HTML5 game developers for more than a decade, and its strength has never been the promise of spectacular visuals. Quite the opposite: it is a sober, well-documented framework that runs anywhere a browser runs, with no native build, no licence and no subscription. A finished Phaser game is a handful of static files you drop onto ordinary hosting.
Version 4 brings a complete overhaul of the WebGL renderer, now organised around independent render nodes, and unifies the old v3 effects and masks under a single filters system. These are deep changes, but they stay under the hood: sprites, scenes, groups, arcade physics, text and tilemaps are all written exactly as before. A Phaser 3 project usually migrates in an afternoon, and existing tutorials still hold.
If you would rather see the bare minimum first, our article on programming a video game with Phaser: the Hello World covers the basics in a few lines. Here we go further.
What you need before you start
Three things are enough: Node.js 20 or later, a code editor, and a working knowledge of modern JavaScript — const, arrow functions, object literals. No WebGL knowledge is required; Phaser handles that for you.
One technical point deserves an immediate mention, because it costs nearly everyone half an hour: a Phaser game must be served by a web server, even locally. If you open your index.html by double-clicking it, the browser will refuse to load the images for security reasons, and you will get a black screen with no useful error message.
Installing Phaser 4 and setting up the project
The simplest route today is a Vite project, which gives you the local server and the production build with no configuration:
npm create vite@latest my-game -- --template vanilla
cd my-game
npm install phaser@4.2.0
npm run dev
Then create a public/assets/ folder for your images. If you have no artwork yet, Phaser's official example asset pack provides a sky, a platform, a star and an animated character, which is more than enough for this tutorial.
Prefer to skip Node altogether? A single HTML file and the CDN will do, as long as you serve it from a local server:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My first game</title>
<script src="https://cdn.jsdelivr.net/npm/phaser@4.2.0/dist/phaser.min.js"></script>
</head>
<body>
<div id="game"></div>
<script type="module" src="main.js"></script>
</body>
</html>
The game configuration and the first scene
Everything begins with a configuration object. It describes the canvas size, the renderer, the gravity of the world and the scenes to load. Open main.js:
import Phaser from 'phaser';
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
parent: 'game',
backgroundColor: '#1d1d2b',
physics: {
default: 'arcade',
arcade: {
gravity: { y: 300 },
debug: false
}
},
scene: { preload, create, update }
};
const game = new Phaser.Game(config);
Phaser.AUTO tells the engine to use WebGL when the browser allows it and to fall back to the 2D canvas otherwise. The gravity of 300 pixels per second squared applies to every dynamic body: it is what pulls our character back down after a jump.
A Phaser scene rests on three functions whose names say exactly what they do. preload loads assets, create builds the world once those assets are available, and update runs on every frame, sixty times per second. That separation is the key to everything else: nothing should be loaded inside create, and nothing should be created inside update.
Loading images and building the scenery
The this.load method queues up assets. Each one gets a key, a string you will reuse everywhere else in the code:
function preload () {
this.load.image('sky', 'assets/sky.png');
this.load.image('platform', 'assets/platform.png');
this.load.image('star', 'assets/star.png');
this.load.spritesheet('hero', 'assets/dude.png', {
frameWidth: 32,
frameHeight: 48
});
}
That last line deserves a word. A spritesheet is a single image holding every pose of the character side by side. By declaring the width and height of one cell, you let Phaser slice it automatically into numbered frames, which you then assemble into animations. Heavier projects usually move on to texture atlases — a topic we cover in our complete guide to asset management in Phaser.
Now for the scenery. Platforms never move and are not affected by gravity: they are static bodies, gathered into a staticGroup.
let platforms;
function create () {
this.add.image(400, 300, 'sky');
platforms = this.physics.add.staticGroup();
platforms.create(400, 568, 'platform').setScale(2).refreshBody();
platforms.create(600, 400, 'platform');
platforms.create(50, 250, 'platform');
platforms.create(750, 220, 'platform');
}
Watch out for refreshBody(): when you rescale a static body, its collision box does not follow on its own. Without that call, the ground would keep its original size and your hero would happily fall through half the screen. It is one of the most common traps in the engine.
The hero, arcade physics and collisions
The character, on the other hand, is a dynamic sprite: it falls, it bounces, it reacts to collisions.
let player;
// inside create()
player = this.physics.add.sprite(100, 450, 'hero');
player.setBounce(0.2);
player.setCollideWorldBounds(true);
this.physics.add.collider(player, platforms);
this.anims.create({
key: 'left',
frames: this.anims.generateFrameNumbers('hero', { start: 0, end: 3 }),
frameRate: 10,
repeat: -1
});
this.anims.create({
key: 'idle',
frames: [{ key: 'hero', frame: 4 }],
frameRate: 20
});
this.anims.create({
key: 'right',
frames: this.anims.generateFrameNumbers('hero', { start: 5, end: 8 }),
frameRate: 10,
repeat: -1
});
The decisive line is this.physics.add.collider(player, platforms). It does nothing visible, but it sets up a contract between two objects: on every frame, Phaser checks whether they overlap and pushes them apart cleanly. Without it, the hero would drift through the scenery like a ghost. The repeat: -1 in the animations simply means "loop forever".
Handing control to the player
Phaser exposes the four arrow keys and the space bar in a single line:
let cursors;
// inside create()
cursors = this.input.keyboard.createCursorKeys();
function update () {
if (cursors.left.isDown) {
player.setVelocityX(-160);
player.anims.play('left', true);
} else if (cursors.right.isDown) {
player.setVelocityX(160);
player.anims.play('right', true);
} else {
player.setVelocityX(0);
player.anims.play('idle');
}
if (cursors.up.isDown && player.body.touching.down) {
player.setVelocityY(-330);
}
}
You never move a sprite by editing its position directly: you give it a velocity, and the physics engine takes care of the rest, frame after frame. The player.body.touching.down condition checks that the character actually has something under his feet before jumping. Remove it and your hero can climb endlessly into the sky by tapping the up arrow — sometimes that is exactly the effect you want, but it should be a decision, not an accident.
A goal: collect the stars and count the points
A game without an objective is a tech demo. Let us add twelve stars falling from the sky, bouncing on the platforms and worth points.
let stars;
let score = 0;
let scoreText;
// inside create()
stars = this.physics.add.group({
key: 'star',
repeat: 11,
setXY: { x: 12, y: 0, stepX: 70 }
});
stars.children.iterate((star) => {
star.setBounceY(Phaser.Math.FloatBetween(0.4, 0.8));
});
this.physics.add.collider(stars, platforms);
this.physics.add.overlap(player, stars, collectStar, null, this);
scoreText = this.add.text(16, 16, 'Score: 0', {
fontSize: '32px',
color: '#ffffff'
});
function collectStar (player, star) {
star.disableBody(true, true);
score += 10;
scoreText.setText('Score: ' + score);
}
Note the difference between collider and overlap. The first bounces objects off each other; the second merely reports that they are touching, without separating them. A star you pick up should not shove the player away: that is an overlap, not a collision. The repeat: 11 creates twelve stars in total — the original plus eleven repeats — spaced 70 pixels apart thanks to stepX.
Reload the page: the game is playable. A hero, some scenery, jumps, a score. The skeleton is complete, and everything you add from here will lean on these same building blocks.
Testing, building and going live
During development, npm run dev reloads the game on every save. Set debug: true in the arcade configuration to draw the collision boxes in green: that is the first reflex to have when a character floats three pixels above the ground or walks through a wall.
When you are happy with the result, npm run build produces a fully static dist/ folder. It drops straight onto GitHub Pages, Netlify, or a subfolder of your own site. No application server, no database — which is precisely what makes HTML5 games so convenient to share.
Where to go next
The natural next steps are an enemy, an end screen, several levels — and above all some thought about what makes a session worth playing, which has less to do with code than with game design. On the technical side, the official examples browser remains the best documentation there is: several hundred annotated demos, all readable straight in the browser.
Your first 2D game with Phaser fits in about a hundred lines. The second one will be more ambitious, and that is rather the point.







0 Comments