Game Development 10 min read

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.

Aotu Lab
Aotu Lab
Aotu Lab
How to Build a ‘Merge Watermelon’ Game with Phaser 3 and TypeScript

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 watch

Engine 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.png11.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/

TypeScriptgame developmentHTML5PhysicsCollisionPhaserMatter.js
Aotu Lab
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.