Get the full commented source code of

HTML5 Suika Watermelon Game

Talking about Zuma game, Game development, HTML5, Javascript, Phaser and TypeScript.

Do you remember Zuma? It’s a tile-matching puzzle video game developed by PopCap Games. Released in 2003, it gained popularity for its simple yet addictive gameplay.

The goal is to eliminate all the balls in a chain before they reach the end of a path. If a ball reaches the end, it’s game over.

I was asked by a reader to make a prototype of Zuma, so why not?

Making it with Phaser is super easy, as it has excellent path management.

All we have to do, at first, is to draw a path and have the gems run on it.

Phaser’s path management provides that given a given instant t, ranging from 0 to 1, where 0 represents the beginning of the path and 1 the end, we are able to know where we are on the path.

Look at the gems running through the path:

No interaction at the moment, I am going to add it later.

This is the completely commented source code, which consists in one HTML file, one CSS file and four TypeScript files:

index.html

The web page which hosts the game, to be run inside thegame element.

HTML
<!DOCTYPE html>

<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
        <link rel="stylesheet" href="style.css">
        <script src="main.js"></script> 
    </head>
    <body>   
        <div id = "thegame"></div>
    </body>
</html>

style.css

The cascading style sheets of the main web page.

CSS
/* remove margin and padding from all elements */
* {
    padding : 0;
    margin : 0;
}

/* set body background color */
body {
    background-color : #000000;    
}

/* Disable browser handling of all panning and zooming gestures. */
canvas {
    touch-action : none;
}

gameOptions.ts

Configurable game options.

It’s a good practice to place all configurable game options, if possible, in a single and separate file, for a quick tuning of the game. I also grouped the variables to keep them more organized.

TypeScript
// CONFIGURABLE GAME OPTIONS
// changing these values will affect gameplay

export const GameOptions : any = {

    gameSize : {
        width               : 1920,     // width of the game, in pixels
        height              : 1080      // height of the game, in pixels
    },

    gameBackgroundColor     : 0x222222, // game background color

    path                    : '{"type":"Path","x":0,"y":0,"autoClose":false,"curves":[{"type":"LineCurve","points":[1460,-50,1460,540]},{"type":"EllipseCurve","x":960,"y":540,"xRadius":500,"yRadius":500,"startAngle":0,"endAngle":180,"clockwise":false,"rotation":0},{"type":"EllipseCurve","x":910,"y":540,"xRadius":450,"yRadius":450,"startAngle":180,"endAngle":0,"clockwise":false,"rotation":0},{"type":"EllipseCurve","x":960,"y":540,"xRadius":400,"yRadius":400,"startAngle":0,"endAngle":180,"clockwise":false,"rotation":0},{"type":"EllipseCurve","x":910,"y":540,"xRadius":350,"yRadius":350,"startAngle":180,"endAngle":0,"clockwise":false,"rotation":0},{"type":"EllipseCurve","x":960,"y":540,"xRadius":300,"yRadius":300,"startAngle":0,"endAngle":180,"clockwise":false,"rotation":0},{"type":"EllipseCurve","x":910,"y":540,"xRadius":250,"yRadius":250,"startAngle":180,"endAngle":0,"clockwise":false,"rotation":0},{"type":"EllipseCurve","x":960,"y":540,"xRadius":200,"yRadius":200,"startAngle":0,"endAngle":180,"clockwise":false,"rotation":0},{"type":"EllipseCurve","x":910,"y":540,"xRadius":150,"yRadius":150,"startAngle":180,"endAngle":0,"clockwise":false,"rotation":0},{"type":"EllipseCurve","x":960,"y":540,"xRadius":100,"yRadius":100,"startAngle":0,"endAngle":180,"clockwise":false,"rotation":0}]}',
    gemSpeed                : 400,      // gem speed, in pixels per second
    gemRadius               : 24        // gem radius, in pixels

}

main.ts

This is where the game is created, with all Phaser related options.

TypeScript
// MAIN GAME FILE

// modules to import
import Phaser from 'phaser';                            // Phaser
import { PreloadAssets } from './scenes/preloadAssets'; // preloadAssets scene
import { PlayGame } from './scenes/playGame';           // playGame scene
import { GameOptions } from './gameOptions';            // game options

// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
    mode        : Phaser.Scale.FIT,                     // adjust size to automatically fit in the window
    autoCenter  : Phaser.Scale.CENTER_BOTH,             // center the game horizontally and vertically
    parent      : 'thegame',                            // DOM id where to render the game
    width       : GameOptions.gameSize.width,           // game width, in pixels
    height      : GameOptions.gameSize.height           // game height, in pixels
}

// game configuration object
const configObject : Phaser.Types.Core.GameConfig = { 
    type            : Phaser.AUTO,                      // game renderer
    backgroundColor : GameOptions.gameBackgroundColor,  // game background color
    scale           : scaleObject,                      // scale settings
    scene           : [                                 // array with game scenes
        PreloadAssets,                                  // PreloadAssets scene
        PlayGame                                        // PlayGame scene
    ]
}

// the game itself
new Phaser.Game(configObject);

scenes > preloadAssets.ts

Here we preload all assets to be used in the game.

TypeScript
// CLASS TO PRELOAD ASSETS

// PreloadAssets class extends Phaser.Scene class
export class PreloadAssets extends Phaser.Scene {
  
    // constructor    
    constructor() {
        super({
            key : 'PreloadAssets'
        });
    }
  
    // method to be called during class preloading
    preload() : void {
 
        // load image
        this.load.image('gem', 'assets/sprites/gem.png');
        
    }
  
    // method to be executed when the scene is created
    create() : void {

        // start PlayGame scene
        this.scene.start('PlayGame');
    }
}

scenes >playGame.ts

Main game file, all game logic is stored here.

TypeScript
// THE GAME ITSELF

// modules to import
import { GameOptions } from '../gameOptions';

// PlayGame class extends Phaser.Scene class
export class PlayGame extends Phaser.Scene {

    constructor() {
        super({
            key : 'PlayGame'
        });
    }

    graphics    : Phaser.GameObjects.Graphics;  // graphics object where to render the path
    path        : Phaser.Curves.Path;           // the path
    gems        : Phaser.GameObjects.Sprite[];  // array with all gems

    // method to be called once the instance has been created
    create() : void {

        // initialize gems array
        this.gems = [];

        // create the path and load curves from a JSON string
        this.path = new Phaser.Curves.Path(0, 0);
        this.path.fromJSON(JSON.parse(GameOptions.path)); 
        
        // get path length, in pixels
        this.data.set('pathLength', this.path.getLength());

        // add the graphic object and draw the path on it
        this.graphics = this.add.graphics();
        this.graphics.lineStyle(2, 0xffffff, 1);
        this.path.draw(this.graphics);

        // add a gem
        this.addGem(0);
    }

    // method to add a gem
    // t : time relative to path, from 0 to 1, where 0 = at the beginning of the path, and 1 = at the end of the path
    addGem(t : number) : void {

        // get gem start point
        const startPoint : Phaser.Math.Vector2 = this.path.getPoint(t);
        
        // create a sprite at gem start point 
        const gemSprite : Phaser.GameObjects.Sprite = this.add.sprite(startPoint.x, startPoint.y, 'gem');
        
        // set a custom "t" property
        gemSprite.setData('t', t);

        // add gem sprite to gemSprite array
        this.gems.push(gemSprite);
    }

    // metod to be called at each frame
    // time : time passed since the beginning, in milliseconds
    // deltaTime : time passed since last frame, in milliseconds
    update(time : number, deltaTime : number) {

        // determine delta t movement
        const deltaT : number = deltaTime / 1000 * GameOptions.gemSpeed / this.data.get('pathLength');
      
        // loop through all gems
        this.gems.forEach((gem : Phaser.GameObjects.Sprite, index : number) => {
            
            // update gem's t data
            gem.setData('t', gem.getData('t') + deltaT);

            // if the gem reached the end of the path
            if (gem.getData('t') > 1) {

                // restart the game
                this.scene.start('PlayGame');
            }
            // if gem did not reach the end of the path
            else {

                // get new gem path point
                const pathPoint : Phaser.Math.Vector2 = this.path.getPoint(gem.getData('t'));
                
                // move the gem to new path point
                gem.setPosition(pathPoint.x, pathPoint.y);

                // get travelled distance , in pixels
                const travelledDistance : number = this.data.get('pathLength') * gem.getData('t');

                // if this is the last gem and there's enough space for another gem
                if (index == this.gems.length - 1 && travelledDistance > GameOptions.gemRadius * 2) {

                    // add a gem right behind it
                    this.addGem((travelledDistance - GameOptions.gemRadius * 2) / this.data.get('pathLength'));
                }
            }
        })
    }
}

Making sprites follow a path was easy, so in next step I am adding some new features. Meanwhile, get the source code of the entire project.

Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.