Get the full commented source code of

HTML5 Suika Watermelon Game

Talking about Perfect Square! game, Game development, HTML5, Javascript and Phaser.

A couple of days ago, Phaser Studio team released an official Phaser Parcel template to help developers building their HTML5 games.

It seems to be the first template of a long series covering the most important package managers.

So, why not trying them with an actual HTML5 game to show developers how to use them and test speed and final file size?

Each test will be done with my Perfect Square! prototype, which you can play in a more complete version on my itch.io page. The game is called Drop the Square.

To install the Phaser Parcel template, you must have Node.js and npm installed on your computer.

If you don’t know what I am talking about, or are afraid by strange stuff like Node.js or web server, I wrote a free minibook to get you started into this world.

The template comes with a basic game structure, which I changed a bit, making the folder structure look this way:

In public > assets folder I removed the default assets and added the ones used in my game.

In src > scenes I deleted some scenes I did not need for this kind of hyper casual game. No need for title screen and game over screen, so I kept only Boot.js, Preloader.js and Game.js.

To store extra classes and scripts I created a folder called scripts in src folder.

Into src > scripts you can find gameOptions.js, gameTexts.js, gameWall.js, playerSquare.js and squareText.js.

Finally I changed the background color of index.html.

Here is the result:

Tap and hold to make the square grow, release to drop it. Don’t make it fall down the hole or hit the ground. There are also in-game instructions.

Let’s see the files I changed:

index.html

I just changed the background color of body element at line 13.

HTML
<!doctype html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/png" href="/favicon.png" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Phaser - Template</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            background-color: #ffffff;
        }

        #app {
            width: 100%;
            height: 100vh;
            overflow: hidden;
            display: flex;
            justify-content: center;
            align-items: center;
        }
    </style>
</head>

<body>
    <div id="app">
        <div id="game-container"></div>
    </div>
    <script type="module" src="src/main.js"></script>
</body>

</html>

src > main.js

I adjusted it a bit to use my game size and scale object, and to import the scenes I really needed in the game.

JavaScript
import { Boot } from './scenes/Boot';
import { Game } from './scenes/Game';
import { Preloader } from './scenes/Preloader';

const ScaleObject = {
    mode : Phaser.Scale.FIT,
    autoCenter : Phaser.Scale.CENTER_BOTH,
    parent : 'game-container',
    width : 640,
    height : 960    
}

const config = {
    type : Phaser.AUTO,
    scale : ScaleObject,
    scene : [
        Boot,
        Preloader,
        Game
    ]
};

export default new Phaser.Game(config);

src > scenes > Boot.js

I stripped almost everything because I don’t want to preload anything at this stage.

JavaScript
import { Scene } from 'phaser';

export class Boot extends Scene {
    
    constructor() {
        super('Boot');
    }

    create() {
        this.scene.start('Preloader');
    }
}

src > scenes > Preloader.js

I preloaded my own assets obviously, and made some changes to the loading bar because I did not like the way it was scripted.

This way it will always be centered and also I added to canvas the bar frame after the bar itself.

JavaScript
import { Scene } from 'phaser';

export class Preloader extends Scene {
    
    constructor() {
        super('Preloader');
    }

    init() {    
        this.cameras.main.setBackgroundColor(0xffffff);

        // loading bar
        const barWidth = 300;
        const barHeight = 20;
        const barX = (this.game.config.width - barWidth) / 2;
        const barY = (this.game.config.height - barHeight) / 2;        
        const bar = this.add.rectangle(barX, barY, 1, barHeight, 0x888888);
        bar.setOrigin(0);        
        this.add.rectangle(barX, barY, barWidth, barHeight).setStrokeStyle(4, 0x444444).setOrigin(0);        
        this.load.on('progress', (progress) => {
            bar.width = barWidth * progress;
        });
    }

    preload() { 
        this.load.setPath('assets');
        this.load.image('base', 'base.png');
        this.load.image('square', 'square.png');
        this.load.image('top', 'top.png');
        this.load.bitmapFont('font', 'font.png', 'font.fnt');
    }

    create () {
        this.scene.start('Game');
    }
}

src > scenes > Game.js

This is the game itself and obviously contains my code. You can check all Perfect Square! series posts to see how I wrote it.

JavaScript
import { Scene } from 'phaser';
import { GameWall } from '../scripts/gameWall';
import { PlayerSquare } from '../scripts/playerSquare';
import { SquareText } from '../scripts/squareText';
import { GameOptions } from '../scripts/gameOptions';
import { GameTexts } from '../scripts/gameTexts';

const GameModes = {
    IDLE : 0,
    WAITING : 1,
    GROWING : 2
}

export class Game extends Scene {
    
    constructor() {
        super('Game');
    }

    create() {
        const tintColor = Phaser.Utils.Array.GetRandom(GameOptions.bgColors);
        this.cameras.main.setBackgroundColor(tintColor);
        this.gameWidth = this.game.config.width;
        this.gameHeight = this.game.config.height;
        this.saveData = localStorage.getItem(GameOptions.localStorageName) == null ? { level: 1} : JSON.parse(localStorage.getItem(GameOptions.localStorageName));
        this.placeWalls();
        this.square = new PlayerSquare(this, this.gameWidth / 2, -400, 'square');
        this.squareText = new SquareText(this, this.square.x, this.square.y, 'font', this.saveData.level, 120, tintColor);
        this.squareTweenTargets = [this.square, this.squareText];
        this.levelText = this.add.bitmapText(this.gameWidth / 2, 0, 'font', 'level ' + this.saveData.level, 60);
        this.levelText.setOrigin(0.5, 0);
        this.updateLevel();
        this.input.on('pointerdown', this.startGrowing, this);
        this.input.on('pointerup', this.stopGrowing, this);
    }

    placeWalls() {
        this.leftSquare = new GameWall(this, 0, this.gameHeight, 'base', new Phaser.Math.Vector2(1, 1));
        this.rightSquare = new GameWall(this, this.gameWidth, this.gameHeight, 'base', new Phaser.Math.Vector2(0, 1));
        this.leftWall = new GameWall(this, 0, this.gameHeight - this.leftSquare.height, 'top', new Phaser.Math.Vector2(1, 1));
        this.rightWall = new GameWall(this, this.gameWidth, this.gameHeight - this.leftSquare.height, 'top', new Phaser.Math.Vector2(0, 1));
    }

    updateLevel() {
        let holeWidth = Phaser.Math.Between(GameOptions.holeWidthRange[0], GameOptions.holeWidthRange[1]);
        let wallWidth = Phaser.Math.Between(GameOptions.wallRange[0], GameOptions.wallRange[1]);
        this.leftSquare.tweenTo((this.gameWidth - holeWidth) / 2);
        this.rightSquare.tweenTo((this.gameWidth + holeWidth) / 2);
        this.leftWall.tweenTo((this.gameWidth - holeWidth) / 2 - wallWidth);
        this.rightWall.tweenTo((this.gameWidth + holeWidth) / 2 + wallWidth);
        this.tweens.add({
            targets : this.squareTweenTargets,
            y : 150,
            scaleX : 0.2,
            scaleY : 0.2,
            angle : 50,
            duration : 500,
            ease : 'Cubic.easeOut',
            onComplete : () => {
                this.rotateTween = this.tweens.add({
                    targets : this.squareTweenTargets,
                    angle : 40,
                    duration : 300,
                    yoyo : true,
                    repeat : -1
                });
                if (this.square.successful == 0) {
                    this.addInfo(holeWidth, wallWidth);
                }
                this.currentGameMode = GameModes.WAITING;
            }
        })
    }

    addInfo(holeWidth, wallWidth) {
        this.infoGroup = this.add.group();
        let targetSquare = this.add.sprite(this.gameWidth / 2, this.gameHeight - this.leftSquare.displayHeight, 'square');
        targetSquare.displayWidth = holeWidth + wallWidth;
        targetSquare.displayHeight = holeWidth + wallWidth;
        targetSquare.alpha = 0.3;
        targetSquare.setOrigin(0.5, 1);
        this.infoGroup.add(targetSquare);
        let targetText = this.add.bitmapText(this.gameWidth / 2, targetSquare.y - targetSquare.displayHeight - 20, 'font', GameTexts.landHere, 48);
        targetText.setOrigin(0.5, 1);
        this.infoGroup.add(targetText);
        let holdText = this.add.bitmapText(this.gameWidth / 2, 250, 'font', GameTexts.infoLines[0], 40);
        holdText.setOrigin(0.5, 0);
        this.infoGroup.add(holdText);
        let releaseText = this.add.bitmapText(this.gameWidth / 2, 300, 'font', GameTexts.infoLines[1], 40);
        releaseText.setOrigin(0.5, 0);
        this.infoGroup.add(releaseText);
    }

    startGrowing() {
        if (this.currentGameMode == GameModes.WAITING) {
            this.currentGameMode = GameModes.GROWING;
            if (this.square.successful == 0) {
                this.infoGroup.toggleVisible();
            }
            this.growTween = this.tweens.add({
                targets : this.squareTweenTargets,
                scaleX : 1,
                scaleY : 1,
                duration : GameOptions.growTime
            });
        }
    }

    stopGrowing() {
        if (this.currentGameMode == GameModes.GROWING) {
            this.currentGameMode = GameModes.IDLE;
            this.growTween.stop();
            this.rotateTween.stop();
            this.rotateTween = this.tweens.add({
                targets : this.squareTweenTargets,
                angle : 0,
                duration :300,
                ease : 'Cubic.easeOut',
                onComplete : () => {
                    if (this.square.displayWidth <= this.rightSquare.x - this.leftSquare.x) {
                        this.tweens.add({
                            targets : this.squareTweenTargets,
                            y : this.gameHeight + this.square.displayWidth,
                            duration : 600,
                            ease : 'Cubic.easeIn',
                            onComplete : () => {
                                this.levelText.text = GameTexts.failure;
                                this.gameOver();
                            }
                        })
                    }
                    else{
                        if (this.square.displayWidth <= this.rightWall.x - this.leftWall.x) {
                            this.fallAndBounce(true);
                        }
                        else{
                            this.fallAndBounce(false);
                        }
                    }
                }
            })
        }
    }

    fallAndBounce(success) {
        let destY = this.gameHeight - this.leftSquare.displayHeight - this.square.displayHeight / 2;
        let message = success ? GameTexts.success : GameTexts.failure;
        if (success) {
            this.square.successful ++;
        }
        else {
            destY = this.gameHeight - this.leftSquare.displayHeight - this.leftWall.displayHeight - this.square.displayHeight / 2;
        }
        this.tweens.add({
            targets : this.squareTweenTargets,
            y : destY,
            duration : 600,
            ease : 'Bounce.easeOut',
            onComplete: () => {
                this.levelText.text = message;
                if (!success) {
                    this.gameOver();
                }
                else{
                    this.time.addEvent({
                        delay : 1000,
                        callback : () => {
                            if (this.square.successful == this.saveData.level) {
                                this.saveData.level ++;
                                localStorage.setItem(GameOptions.localStorageName, JSON.stringify({
                                    level : this.saveData.level
                                }));
                                this.scene.start('Game');
                            }
                            else {
                                this.squareText.updateText(this.saveData.level - this.square.successful);
                                this.levelText.text = 'level ' + this.saveData.level;
                                this.updateLevel();
                            }
                        }
                    });
                }
            }
        })
    }

    gameOver() {
        this.time.addEvent({
            delay : 1000,
            callback : () => {
                this.scene.start('Game');
            }
        });
    }
}

src > scripts > gameOptions.js

The first of my custom scripts, this one groups all configurable game options.

JavaScript
export const GameOptions = {
    bgColors : [0x62bd18, 0xff5300, 0xd21034, 0xff475c, 0x8f16b2, 0x588c7e, 0x8c4646],
    holeWidthRange : [80, 260],
    wallRange : [10, 50],
    growTime : 1500,
    localStorageName : 'squaregamephaserparcel'
}

src > scripts > gameTexts.js

Here I placed the few texts used in the game.

JavaScript
export const GameTexts = {
    level : 'level',
    infoLines : ['tap and hold to grow', 'release to drop'],
    landHere : 'land here',
    success : 'Yeah!!!',
    failure : 'On no!!'
}

src > scripts > gameWall.js

Custom class to handle game walls, extending Sprite class.

JavaScript
export class GameWall extends Phaser.GameObjects.Sprite {
 
    constructor(scene, x, y, key, origin) {
        super(scene, x, y, key);
        this.setOrigin(origin.x, origin.y);
        scene.add.existing(this);
    }
 
    tweenTo(x) {
        this.scene.tweens.add({
            targets : this,
            x : x,
            duration : 500,
            ease : 'Cubic.easeOut'
        });
    }
}

src > scripts > playerSquare.js

Custom class to handle player square, extending Sprite class.

JavaScript
export class PlayerSquare extends Phaser.GameObjects.Sprite {
 
    constructor(scene, x, y, key) {
        super(scene, x, y, key);
        this.successful = 0;
        this.setScale(0.2);
        scene.add.existing(this)
    }
}

src > scripts > squareText.js

Custom class to handle square text, extending BitmapText class.

JavaScript
export class SquareText extends Phaser.GameObjects.BitmapText {
 
    constructor(scene, x, y, font, text, size, tintColor) {
        super(scene, x, y, font, text, size);
        this.setOrigin(0.5);
        this.setScale(0.4);
        this.setTint(tintColor); 
        scene.add.existing(this);
    }
     
    updateText(text) {
        this.setText(text);
    }
}

And that’s it, using the Phaser Parcel template to build my game was quite easy, I only had to change a couple of paths in the distributable index.html file, because I did not want it to call JavaScript files starting with a root path.

I just stripped the / from src=”/index.xxxxxxxx.js” in both entries turning them to src=”index.xxxxxxxx.js”.

Will you use the Phaser Parcel template? Download the files to change in the official template and play with the template itself.

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