How to Build a ‘Merge Watermelon’ Game with Phaser 3 and TypeScript
This step‑by‑step guide shows how to create a simple “merge watermelon” HTML5 game using Phaser 3, TypeScript, and Matter.js physics, covering project setup, asset loading, fruit creation, input handling, collision logic, scoring, and game‑over detection.
Quick Start
Clone the Phaser 3 TypeScript project template, install dependencies, and start the development server.
git clone [email protected]:photonstorm/phaser3-typescript-project-template.git hexigua
cd hexigua
npm install
npm run watchEngine Configuration
Enable Matter.js physics and use proportional scaling.
const config = {
type: Phaser.AUTO,
backgroundColor: '#ffe8a3',
mode: Phaser.Scale.FIT,
physics: {
default: 'matter',
matter: {
gravity: { y: 2 },
debug: true
}
},
width: window.innerWidth,
height: window.innerHeight,
scene: Demo
};Asset Loading
Load the eleven fruit images (named 1.png ‑ 11.png) and a ground sprite.
preload () {
for (let i = 1; i <= 11; i++) {
this.load.image(`${i}`, `assets/${i}.png`);
}
this.load.image('ground', 'assets/ground.png');
}Fruit Creation Helper
Creates a Matter body for a fruit, assigns a circular shape, stores the fruit type in label, and adds a scaling tween.
/**
* Add a fruit.
* @param {number} x X coordinate.
* @param {number} y Y coordinate.
* @param {boolean} [isStatic=true] Whether the body starts static.
* @param {string} [key] Fruit type key; if omitted a random type 1‑5 is chosen.
*/
createFruit (x, y, isStatic = true, key) {
if (!key) {
key = `${Phaser.Math.Between(1, 5)}`; // random among first 5 types
}
const fruit = this.matter.add.image(x, y, key);
fruit.setBody({ type: 'circle', radius: fruit.width / 2 }, { isStatic, label: key });
this.tweens.add({
targets: fruit,
scale: { from: 0, to: 1 },
ease: 'Back',
easeParams: [3.5],
duration: 200
});
return fruit;
}Scene Initialization
Set world bounds, add a static ground tile, and spawn the first fruit.
create () {
this.matter.world.setBounds();
const groundSprite = this.add.tileSprite(WINDOW_WIDTH/2, WINDOW_HEIGHT-127/2, WINDOW_WIDTH, 127, 'ground');
this.matter.add.gameObject(groundSprite, { isStatic: true });
const x = WINDOW_WIDTH / 2;
const y = WINDOW_HEIGHT / 10;
let fruit = this.createFruit(x, y);
}Input Handling
On pointer down, move the current fruit horizontally to the tap position, release it to fall, and spawn a new fruit after one second.
this.input.on('pointerdown', (point) => {
if (this.enableAdd) {
this.enableAdd = false;
this.tweens.add({
targets: fruit,
x: point.x,
duration: 100,
ease: 'Power1',
onComplete: () => {
fruit.setStatic(false);
setTimeout(() => {
fruit = this.createFruit(x, y);
this.enableAdd = true;
}, 1000);
}
});
}
});Collision Handling
When two non‑static fruits with the same label collide (and are not the final watermelon), make them static, animate one onto the other, destroy both, and create a new fruit with the next label.
this.matter.world.on('collisionstart', (event, bodyA, bodyB) => {
const notWatermelon = bodyA.label !== '11';
const same = bodyA.label === bodyB.label;
const live = !bodyA.isStatic && !bodyB.isStatic;
if (notWatermelon && same && live) {
bodyA.isStatic = true;
bodyB.isStatic = true;
const { x, y } = bodyA.position;
const nextLabel = parseInt(bodyA.label) + 1;
this.tweens.add({
targets: bodyB.position,
props: {
x: { value: x, ease: 'Power3' },
y: { value: y, ease: 'Power3' }
},
duration: 150,
onComplete: () => {
bodyA.gameObject.alpha = 0;
bodyB.gameObject.alpha = 0;
bodyA.destroy();
bodyB.destroy();
this.createFruit(x, y, false, `${nextLabel}`);
}
});
}
});Game‑Over Detection
Create an invisible sensor line 200 px below the spawn height. If a falling fruit collides with this line while new fruit addition is enabled, the game ends.
const endLineSprite = this.add.tileSprite(WINDOW_WIDTH/2, y + 200, WINDOW_WIDTH, 8, 'endLine');
endLineSprite.setVisible(false);
this.matter.add.gameObject(endLineSprite, {
isStatic: true,
isSensor: true,
onCollideCallback: () => {
if (this.enableAdd) {
console.log('end'); // replace with actual game‑over handling
}
}
});Scoring
When two fruits merge, add the numeric label to the total score. Merging the final watermelon (label 10) grants an extra 100 points.
let added = parseInt(bodyA.label);
this.score += added;
if (added === 10) {
this.score += 100;
}
this.scoreText.setText(this.score);Score display initialization:
this.scoreText = this.add.text(30, 20, `${this.score}`, {
font: '90px Arial Black',
color: '#ffe325'
}).setStroke('#974c1e', 16);Repository
Full source code: https://github.com/eijil/hexigua
References
Phaser framework: https://phaser.io/
Aotu Lab
Aotu Lab, founded in October 2015, is a front-end engineering team serving multi-platform products. The articles in this public account are intended to share and discuss technology, reflecting only the personal views of Aotu Lab members and not the official stance of JD.com Technology.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
