10-Minute Practical Guide: Building a "Catch Coins" Game with Phaser 3
A step-by-step tutorial on setting up a Vite + Phaser 3 environment and building a lightweight 2D arcade game with physics and collision detection. Perfect for developers looking to quickly master HTML5 game development.

10-Minute Practical Guide: Building a "Catch Coins" Game with Phaser 3
Over the years, I've seen many backend and frontend developers want to build a small game for fun or add an interactive easter egg to an internal dashboard. But hearing about Unity or Cocos Creator usually brings a headache: multi-gigabyte installers, massive project structures, and steep learning curves. For developers who just want to run a lightweight 2D web game, that's overkill.
If you need a free, well-documented, and out-of-the-box HTML5 game solution, Phaser is undoubtedly the modern answer. It cuts through the noise and focuses on three core tasks: rendering graphics to a canvas, running the game loop frame-by-frame, and handling physics collisions and user input.
Today, I won't dive into source code or feature matrices. Instead, I'll walk you through setting up the environment from scratch and building a "Catch Coins" mini-game. Follow along, and you'll grasp the core game development loop, giving you the confidence to build independent 2D web games.
🛠 Prerequisites: What You Need to Know
- Environment: Node.js 16+ (used for the local dev server)
- Basics: Familiarity with foundational JavaScript syntax. No graphics theory or complex math required.
- Mindset: Don't fear the low-level details! Phaser abstracts vector math and rendering pipelines cleanly. You only need to focus on business logic.
💡 Why use a local server?
Many beginners double-click a.htmlfile directly and get a black screen withCORSorfile://protocol errors in the console. Phaser loads sprite assets via network requests, and modern browsers block local files from cross-origin resource loading for security reasons. A lightweight Vite dev server avoids 80% of these mysterious issues.
🚀 Step 1: Environment Setup & Core Concepts
Open your terminal and run the following commands:
(See installation code block below)
Once installed, open src/main.js (or your entry file). Phaser's architecture is highly focused. Two objects drive everything: Phaser.Game and Scene.
Gameinstance: The canvas manager. Initializes WebGL/Canvas, handles fullscreen, and manages scaling.Scene: The "stage". Games typically split into Start Screen, Gameplay, and Game Over. Each Scene follows a strict lifecycle:preload(): Asynchronously loads images, audio, and JSON configs. The screen isn't rendered yet, making it perfect for loading spinners.create(): Resources are ready. Initializes sprites, physics worlds, and input listeners. This is the "setup" phase.update(time, delta): Executes every frame.deltais the time elapsed since the last frame (ms). Use it for frame-rate independent movement to prevent sluggish gameplay on slower machines.
Let's write a shell to verify the setup:
javascript
import Phaser from 'phaser';
class MainScene extends Phaser.Scene {
constructor() { super('MainScene'); }
preload() { console.log('Loading assets...'); }
create() {
this.add.text(400, 300, 'Phaser environment ready', { fontSize: '28px' })
.setOrigin(0.5).setColor('#00ffcc');
}
update() {}
}
const config = {
type: Phaser.AUTO,
width: 800, height: 600,
backgroundColor: '#1a1a2e',
scene: MainScene,
physics: { default: 'arcade', arcade: { gravity: { y: 300 } } }
};
new Phaser.Game(config);
Run npm run dev. If you see the centered text in your browser, your game loop is running successfully.
🎮 Step 2: Hands-On — The "Catch Coins" Game
Text alone isn't fun. Let's add gameplay. The logic is straightforward: a basket moves left/right at the bottom. Coins drop randomly from the top. Catch them for points; miss them and you game over.
1. Prepare Assets
Place two images in public/assets/: basket.png and coin.png. (Solid color squares work fine for testing.)
2. Write the Core Logic
Replace the MainScene class with the full implementation below. Pay attention to the "why" comments:
javascript
import Phaser from 'phaser';
class MainScene extends Phaser.Scene {
constructor() {
super('MainScene');
this.score = 0;
this.basket = null;
this.coins = null;
}
preload() {
this.load.image('basket', '/assets/basket.png');
this.load.image('coin', '/assets/coin.png');
}
create() {
// 1. Create the basket, enable physics, and keep it static (immune to gravity)
this.basket = this.physics.add.staticSprite(400, 560, 'basket').setScale(0.5);
// 2. Create a coins group (object pool concept)
this.coins = this.physics.add.group({
key: 'coin',
repeat: 8, // Pre-generate 9 coins
setXY: { x: 50, y: 0, stepX: 90 }
});
this.coins.children.iterate(child => {
child.setScale(0.4);
child.setVelocityY(Phaser.Math.Between(80, 150));
child.setBounce(1, Phaser.Math.Float());
});
// 3. Register collision callback
this.physics.add.overlap(this.basket, this.coins, this.catchCoin, null, this);
// 4. Keyboard controls
this.cursors = this.input.keyboard.createCursorKeys();
// 5. UI text
this.scoreText = this.add.text(16, 16, 'Score: 0', { fontSize: '24px', fill: '#fff' });
}
update() {
// Frame-rate independent horizontal movement
if (this.cursors.left.isDown) {
this.basket.body.setVelocityX(-300);
} else if (this.cursors.right.isDown) {
this.basket.body.setVelocityX(300);
} else {
this.basket.body.setVelocityX(0);
}
// Reset missed coins to the top
this.coins.children.iterate(child => {
if (child.active && child.y > 620) {
this.resetCoin(child);
}
});
}
catchCoin(basket, coin) {
this.score += 10;
this.scoreText.setText(`Score: ${this.score}`);
this.resetCoin(coin);
}
resetCoin(coin) {
coin.y = Phaser.Math.Between(0, -200);
coin.x = Phaser.Math.Between(20, 780);
coin.setVelocityY(Phaser.Math.Between(80, 150));
}
}
const config = {
type: Phaser.AUTO,
width: 800, height: 600,
backgroundColor: '#1a1a2e',
scene: MainScene,
physics: { default: 'arcade', arcade: { gravity: { y: 0 } } } // No gravity needed as we manually set velocity
};
new Phaser.Game(config);
💣 Common Pitfalls & Pro Tips
When mentoring developers through their first Phaser projects, these issues come up frequently. Here's how to avoid them early:
- Sprite Origin & Collision Offset: Phaser defaults to
setOrigin(0.5), meaning the collision box is centered on the image. Manually changing the origin often misaligns the debug physics box. For collision-heavy games, stick to the default. - Frequent
newinupdate(): JS garbage collection isn't instant. Callingthis.add.image()every frame for spawning coins will cause memory leaks and frame drops. Use an Object Pool: pre-create disabled sprites increate(), activate them when needed, and recycle them when they leave the screen. - Choosing a Physics Engine: Phaser bundles Arcade (lightweight, rectangle-based), Matter.js (complex polygons/rigid bodies), and Impact. For casual 2D games, always start with Arcade. It's highly optimized. Only switch to Matter if you need complex slopes or ragdoll physics.
- Mobile Responsiveness:
800x600overflows on phones. Addscale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH }to your config. The canvas will automatically scale and center to fit any viewport.
📝 Summary & Next Steps
This guide skipped deep rendering pipeline theory because that's for engine contributors. As a product or web developer, mastering Scene Lifecycles + Physics Collisions + Input Events covers 90% of 2D game needs.
You've successfully run your first Phaser project. Next, try:
- Adding pause/speed-up logic with
this.input.keyboard.on('keydown-SPACE', ...) - Applying elastic easing to coin drops using
this.tweens.add() - Integrating tilemap loading from the official Phaser Examples repo
Game development's biggest thrill is seeing instant visual feedback from a single line of code. Drop the perfectionism, get your hands dirty, and start building. Stuck on something? Drop a comment and I'll help you debug!
Installation Commands:
bash
npm create vite@latest phaser-game -- --template vanilla
cd phaser-game
npm install phaser
npm run dev