Phaser Getting Started — Complete Beginner's Guide (Build Your First 2D Game)
Phaser Getting Started — Complete Beginner’s Guide (Build Your First 2D Game)
Learn Phaser.js from scratch: set up a game canvas, understand scenes, load sprites and audio, handle keyboard input, and build your first 2D browser game.
What You’ll Learn
- What a Phaser game config object does and how to write one
- How scenes work — think of them as acts in a play
- Loading images, spritesheets, and audio into your game
- Moving a character with keyboard arrow keys
- Building a complete collect-the-coins mini game from scratch
Phaser is the most popular 2D game framework for the browser — but you don’t need to be a game developer to use it. By the end, you’ll have a working game.
Why Getting Started with Phaser Matters
Every game begins with a foundation: a canvas to draw on, a loop that runs 60 times per second, and a way to respond to player input. Phaser handles all of this so you can focus on gameplay.
At DodaTech, we build interactive security training simulators that teach penetration testing and defensive strategies through gamified scenarios. These simulators use Phaser game mechanics — collision detection for zone alerts, sprite management for character movements, and scene transitions for module navigation. The same skills you learn here power real-world training tools used by security teams.
Learning Path
flowchart LR
A[Getting Started] --> B[Sprites & Physics]
B --> C[Scenes & State]
C --> D[Input & Audio]
D --> E[Tilemaps & Level Design]
A -->|You are here| A
Phaser vs Raw Canvas: When to Use What
| Approach | Best For | Trade-off |
|---|---|---|
| Phaser | 2D games, interactive sims | Heavier (900KB) but full-featured |
| Raw Canvas API | Simple drawing, data viz | Manual physics, input, audio |
| WebGL directly | 3D, complex shaders | Steep learning curve |
Phaser wraps Canvas and WebGL rendering into one consistent API. You write your game logic once; Phaser picks the best renderer.
Setting Up Phaser — The Container Analogy
Think of a Phaser game like setting up a stage for a play. You need:
- A stage (the HTML page)
- A curtain area (a
<div>where the game renders) - A playbill (the config object that tells Phaser what to do)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Phaser Game</title>
<script src="https://cdn.jsdelivr.net/npm/phaser@3.80.1/dist/phaser.min.js"></script>
</head>
<body>
<div id="game"></div>
<script>
// game code here
</script>
</body>
</html>The <script> tag loads Phaser from a CDN — no installation needed. For local development, you can also install via npm: npm install phaser and import it with a bundler like Webpack.
The Game Config — Your Blueprint
Every Phaser project starts with a config object. It’s like a blueprint that tells Phaser: “Here’s how big the canvas is, what physics to use, and what scenes to load.”
const config = {
type: Phaser.AUTO, // Try WebGL first, fall back to Canvas
width: 800, // Canvas width in pixels
height: 600, // Canvas height in pixels
backgroundColor: '#1a1a2e',
physics: {
default: 'arcade',
arcade: { gravity: { y: 0 }, debug: false }
},
scene: [MainScene] // At least one scene required
};
const game = new Phaser.Game(config);Why Phaser.AUTO? It tries WebGL rendering first because it’s faster (uses your GPU). If WebGL isn’t available (older browsers), it falls back to Canvas mode. You don’t have to worry about which one — Phaser decides.
Why new? The new keyword creates an actual game instance. Without it, Phaser.Game(config) returns undefined and nothing renders. This is one of the most common beginner mistakes.
| Property | Values | What It Does |
|---|---|---|
type | Phaser.AUTO, Phaser.WEBGL, Phaser.CANVAS | Chooses renderer |
width / height | pixels | Game canvas dimensions |
backgroundColor | hex string | Background color before anything renders |
physics | object | Configures physics (arcade, matter, or none) |
scene | array of classes | Scenes the game manages |
Scene Lifecycle — Like Acts in a Play
A Scene in Phaser is like an act in a play. Each act has a beginning, middle, and end. Similarly, each scene has three core methods that run in order:
class MainScene extends Phaser.Scene {
constructor() {
super({ key: 'MainScene' }); // Give the scene a name
}
preload() {
// 1. Load assets (images, sounds)
// Runs first — like gathering props before a play
}
create() {
// 2. Set up game objects, text, input
// Runs after preload — like actors taking their positions
}
update(time, delta) {
// 3. Game loop — runs every frame (about 60 times/second)
// The play is happening now — this is where movement and logic go
}
}preload() — Gathering Your Props
Before the play starts, you need your props (assets). That’s what preload() is for:
preload() {
this.load.image('background', 'assets/bg.png');
this.load.image('player', 'assets/player.png');
this.load.image('coin', 'assets/coin.png');
this.load.audio('collect', 'assets/collect.wav');
}Each this.load.image() call has three parts:
- Key (
'background') — a nickname you choose. You’ll use this increate()to reference the asset. - Path (
'assets/bg.png') — where the file lives relative to your HTML page. - Optional — additional settings like cross-origin or file type overrides.
Important: The key must be unique. If you use the same key twice, the second one overwrites the first. And the key in preload() must match exactly in create() — 'player' and 'Player' are different!
create() — Setting the Stage
After all assets finish loading, create() runs. Now you can place your sprites, add physics, set up input, and display text:
create() {
this.add.image(400, 300, 'background'); // Center background
this.player = this.add.sprite(100, 500, 'player');
this.coins = this.physics.add.staticGroup();
}Why can’t we do this in preload()? Because preload() only loads files. The rendering engine, physics system, and input handlers aren’t ready yet. If you try this.add.text() in preload(), Phaser throws a runtime error.
update() — The Game Loop
Once everything is set up, update() runs roughly 60 times per second. This is where you check for input, move objects, and handle game logic:
update(time, delta) {
// delta = milliseconds since the LAST frame
// At 60fps, delta is about 16.67ms
if (this.cursors.left.isDown) {
this.player.x -= 200 * (delta / 1000); // Frame-independent movement
}
}Why multiply by delta? Without it, movement speed depends on frame rate. A player on a 144Hz monitor moves 2.4x faster than someone on 60Hz. By multiplying by delta / 1000, you convert pixels-per-frame to pixels-per-second — consistent on any screen.
Loading Assets — Images, Spritesheets, Audio
Images
this.load.image('key', 'path/to/image.png');
// Usage: this.add.image(x, y, 'key');
The key is your nickname for the image. Use it in create() to place the image on screen.
Spritesheets — One File, Many Frames
A spritesheet is a single image that contains multiple frames of animation arranged in a grid. Think of it like a flipbook:
this.load.spritesheet('player',
'assets/player-run.png',
{ frameWidth: 32, frameHeight: 48 } // Each frame is 32x48 pixels
);
// Create animation in create()
this.anims.create({
key: 'run',
frames: this.anims.generateFrameNumbers('player', { start: 0, end: 5 }),
frameRate: 10,
repeat: -1 // -1 = loop forever
});Audio — Always Provide Fallbacks
Different browsers support different audio formats. MP3 works everywhere except some Firefox versions prefer OGG. Provide both:
this.load.audio('music', [
'assets/music.ogg',
'assets/music.mp3'
]);Phaser will try each format in order and use the first one the browser supports. This is called format fallback.
Keyboard Input
Think of keyboard input like a remote control for your game. Phaser gives you two ways to use it:
- Cursor keys — a pre-built object for arrow keys, space, and shift
- Custom keys — bind any keyboard key to an action
create() {
// Method 1: Cursor keys (arrow keys + space + shift)
this.cursors = this.input.keyboard.createCursorKeys();
// Method 2: WASD keys (custom)
this.wasd = {
up: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W),
down: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S),
left: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A),
right: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D)
};
// One-shot action (press once to jump)
this.input.keyboard.on('keydown-SPACE', () => {
this.player.setVelocityY(-300);
});
}
update() {
const speed = 200;
if (this.cursors.left.isDown) {
this.player.x -= speed * (this.game.loop.delta / 1000);
} else if (this.cursors.right.isDown) {
this.player.x += speed * (this.game.loop.delta / 1000);
}
}Why use isDown vs events? Use isDown for continuous actions like movement (hold to keep moving). Use keydown-SPACE events for single actions like jumping or shooting (press once, act once).
You Might Be Wondering…
“Why do I need a web server? Can’t I just open the HTML file?” Modern browsers block loading local files (especially audio and JSON) for security reasons. This is called the same-origin policy. Use VS Code Live Server, npx serve ., or python -m http.server 8000 — all free and easy.
“What’s the difference between this.add.image and this.add.sprite?” An Image is a static picture — no physics, no animation. A Sprite can have physics, play animations, and respond to input. Use images for backgrounds and decorations; use sprites for characters and interactive objects.
Common Mistakes
Forgetting
newwithPhaser.Game—const game = Phaser.Game(config)silently fails because it returnsundefined. Always usenew:const game = new Phaser.Game(config).Accessing scene properties in
preload()— Methods likethis.add,this.physics, andthis.inputaren’t available during preload. Only usethis.loadinpreload().Asset key mismatches — The key in
load.image('player', ...)must exactly match the key inthis.add.image(100, 100, 'player'). Case matters:'player'≠'Player'.Frame-dependent movement without delta — Movement like
this.player.x += 5runs faster on high-FPS displays. Always usedelta:this.player.x += 200 * (delta / 1000).Not setting world collision bounds — Sprites can move off-screen entirely without
this.player.setCollideWorldBounds(true).Missing fallback audio formats — Always provide both MP3 and OGG:
this.load.audio('sfx', ['sfx.mp3', 'sfx.ogg']).
Practice Questions
What is the purpose of
Phaser.AUTOin the game config? It tries WebGL rendering first and falls back to Canvas if WebGL is unavailable, ensuring compatibility across browsers.Why does
update()receive adeltaparameter, and how is it used?deltais the time in milliseconds since the last frame. It’s used to make movement frame-independent:x += speed * (delta / 1000)ensures consistent speed at any frame rate.What happens if you call
this.add.image()insidepreload()? Phaser throws a runtime error because the rendering system isn’t ready yet. Onlythis.loadmethods are available inpreload().What’s the difference between
this.add.image()andthis.physics.add.sprite()?image()creates a static display object with no physics body or animation.sprite()creates a physics-enabled object that can collide, animate, and respond to forces.Challenge: Modify the mini project below to add a moving enemy that patrols horizontally. Use a sprite,
setVelocityX(), andsetBounce(1)to make it reverse direction when hitting world bounds. How would you make the player lose a life on contact?
FAQ
Try It Yourself
Here’s a complete, runnable HTML file that creates a game with a player character, coins, arrow-key movement, and a score counter. Copy this into a file, serve it with a local server, and play:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello Phaser</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #0f0f23;
}
</style>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/phaser@3.80.1/dist/phaser.min.js"></script>
<script>
class GameScene extends Phaser.Scene {
constructor() {
super({ key: 'GameScene' });
}
preload() {
this.createPlaceholderAssets();
}
createPlaceholderAssets() {
// Background — gradient sky
const bgCanvas = this.textures.createCanvas('background', 800, 600);
const bgCtx = bgCanvas.context;
const gradient = bgCtx.createLinearGradient(0, 0, 0, 600);
gradient.addColorStop(0, '#1a1a4e');
gradient.addColorStop(0.5, '#4a90d9');
gradient.addColorStop(1, '#2d5a27');
bgCtx.fillStyle = gradient;
bgCtx.fillRect(0, 0, 800, 600);
bgCtx.fillStyle = '#3a7d32';
bgCtx.fillRect(0, 520, 800, 80);
bgCanvas.refresh();
// Player — a small character
const playerCanvas = this.textures.createCanvas('player', 32, 48);
const pCtx = playerCanvas.context;
pCtx.fillStyle = '#ff6b35';
pCtx.fillRect(4, 0, 24, 48);
pCtx.fillStyle = '#ffd166';
pCtx.fillRect(8, 4, 16, 16);
playerCanvas.refresh();
// Coin
const coinCanvas = this.textures.createCanvas('coin', 24, 24);
const cCtx = coinCanvas.context;
cCtx.fillStyle = '#ffd700';
cCtx.beginPath();
cCtx.arc(12, 12, 10, 0, Math.PI * 2);
cCtx.fill();
cCtx.fillStyle = '#ffaa00';
cCtx.beginPath();
cCtx.arc(12, 12, 6, 0, Math.PI * 2);
cCtx.fill();
coinCanvas.refresh();
}
create() {
this.add.image(400, 300, 'background');
this.player = this.physics.add.sprite(100, 460, 'player');
this.player.setCollideWorldBounds(true);
this.coins = this.physics.add.staticGroup();
const coinPositions = [
[200, 400], [350, 350], [500, 400],
[650, 350], [280, 300], [550, 300]
];
coinPositions.forEach(([x, y]) => {
this.coins.create(x, y, 'coin');
});
this.physics.add.overlap(
this.player, this.coins,
this.collectCoin, null, this
);
this.cursors = this.input.keyboard.createCursorKeys();
this.score = 0;
this.scoreText = this.add.text(16, 16, 'Coins: 0 / 6', {
fontSize: '24px', fill: '#ffffff',
fontFamily: 'monospace',
stroke: '#000000', strokeThickness: 3
});
this.add.text(400, 560, 'Arrow keys to move — Collect the coins!', {
fontSize: '14px', fill: '#ffffff', fontFamily: 'monospace'
}).setOrigin(0.5);
}
update() {
const speed = 250;
const delta = this.game.loop.delta / 1000;
if (this.cursors.left.isDown) {
this.player.x -= speed * delta;
} else if (this.cursors.right.isDown) {
this.player.x += speed * delta;
}
}
collectCoin(player, coin) {
coin.destroy();
this.score++;
this.scoreText.setText(`Coins: ${this.score} / 6`);
if (this.score >= 6) {
this.scoreText.setText('You Win! All coins collected!');
}
}
}
const config = {
type: Phaser.AUTO,
width: 800, height: 600,
backgroundColor: '#1a1a2e',
physics: {
default: 'arcade',
arcade: { gravity: { y: 0 }, debug: false }
},
scene: [GameScene]
};
new Phaser.Game(config);
</script>
</body>
</html>Open this in a browser (via a local server) to see a player, 6 coins, arrow-key movement, and a live score. This covers: Phaser.Game config, scene lifecycle, procedural asset generation, sprite creation, static physics groups, overlap detection, keyboard input, and real-time UI updates.
What’s Next
| Topic | Description |
|---|---|
| Phaser Sprites & Physics | Deep dive into Arcade physics, collisions, and velocity |
| Phaser Scenes & State | Multi-scene games, transitions, and data persistence |
| Phaser Input & Audio | Gamepad support, touch input, audio sprites, and spatial sound |
| Phaser Tilemaps | Level design with Tiled, tile layers, and procedural generation |
| JavaScript Basics | Review JavaScript fundamentals if any syntax felt unfamiliar |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. These Phaser game mechanics power interactive training simulators used in security awareness programs worldwide.
What’s Next
Congratulations on completing this Phaser Getting Started tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro