Do you like my tutorials?

Then consider supporting me on Ko-fi

Talking about CLOCKS - The Game game, Game development, HTML5, Javascript and Phaser.

About 4 years ago I started the “Clocks – The Game” tutorial series to port the mobile hit which at the moment does not seem to be available on the app store.

Anyway, it’s an one button game where you have to destroy all clocks on the stage by hitting them with a ball which is fired from clocks’ hand.

The complete theory behind the prototype can be found in the first post of the series, and this is the Phaser 3 prototype with the first 10 levels of the game for you to play:

Click or tap to fire the ball from the highlighted clock, try to highlight all clocks to advance levels.

And here is the source code, updated to Phaser 3, optimized and completely commented:

let game;

let gameOptions = {

    // grid size, in pixels
    gridSize: 40,

    // level width, in tiles
    levelWidth: 8,

    // level height, in tiles
    levelHeight: 8,

    // ball speed, in pixels per second
    ballSpeed: 600,

    // starting level
    startingLevel: 0
}

window.onload = function() {
    let gameConfig = {
        type: Phaser.AUTO,
        backgroundColor: 0x2babca,
        scale: {
            mode: Phaser.Scale.FIT,
            autoCenter: Phaser.Scale.CENTER_BOTH,
            parent: "thegame",
            width: 640,
            height: 960
        },
        physics: {
            default: "arcade"
        },
        scene: playGame
    }
    game = new Phaser.Game(gameConfig);
    window.focus();
}

class playGame extends Phaser.Scene {
    constructor() {
        super("PlayGame");
    }
    preload() {
        this.load.image("smallclockface", "assets/sprites/smallclockface.png");
        this.load.image("bigclockface", "assets/sprites/bigclockface.png");
        this.load.image("ball", "assets/sprites/ball.png");
        this.load.spritesheet("smallclock", "assets/sprites/smallclock.png", {
            frameWidth: 70,
            frameHeight: 70
        });
        this.load.spritesheet("smallhand", "assets/sprites/smallhand.png", {
            frameWidth: 70,
            frameHeight: 70
        });
        this.load.spritesheet("bigclock", "assets/sprites/bigclock.png", {
            frameWidth: 140,
            frameHeight: 140
        });
        this.load.spritesheet("bighand", "assets/sprites/bighand.png", {
            frameWidth: 140,
            frameHeight: 140
        });
    }
    create() {

        // player can fire now
        this.canFire = true;

        // clocks reached so far, just one, the one we start from
        this.clocksReached = 1;

        // total clocks in the level, about to be loaded
        this.totalClocks = 0;

        // array containing all clocks
        this.clocksArray = [];

        // physics group which contains all clock hands
        this.handGroup = this.physics.add.group();

        // physics group which contains all clocks
        this.clockGroup = this.physics.add.group();

        // loop through all current level items
        for(let i = 0; i < levels[gameOptions.startingLevel].tiledOutput.length; i ++) {

            // switching among possible values
            switch(levels[gameOptions.startingLevel].tiledOutput[i]) {

                // small clock
                case 1:
                    this.clocksArray.push(this.placeClock(new Phaser.Math.Vector2(i % gameOptions.levelWidth * 2 + 1, Math.floor(i / gameOptions.levelHeight) * 2 + 1), "small"));
                    break;

                // big clock
                case 2:
                    this.clocksArray.push(this.placeClock(new Phaser.Math.Vector2(i % gameOptions.levelWidth * 2 + 2, Math.floor(i / gameOptions.levelHeight) * 2), "big"));
                    break;
            }
        }

        // pick a random clock and make it the active clock
        this.activeClock = Phaser.Utils.Array.GetRandom(this.clocksArray);

        // change active clock appearance
        this.activeClock.setFrame(1);
        this.activeClock.tint = 0x2babca;
        this.activeClock.face.visible = true;
        this.activeClock.hand.setFrame(1);
        this.activeClock.hand.tint = 0xffffff;

        // add the ball
        this.ball = this.physics.add.sprite(game.config.width / 2, game.config.height / 2, "ball");

        // the ball is not visible at the beginning of the game
        this.ball.visible = false;

        // set ball to collide with world bounds
        this.ball.body.collideWorldBounds = true;

        // set ball to listen to world bounds collision
        this.ball.body.onWorldBounds = true;

        // when something (the ball) collide with world bounds...
        this.physics.world.on("worldbounds", function() {

            // restart the game
            this.scene.start("PlayGame");
        }, this);

        // wait for player input then call "throwBall" method
        this.input.on("pointerdown", this.throwBall, this);

        // handle overlap between the ball and "clockGroup" group, then call "handleOverlap" method
        this.physics.add.overlap(this.ball, this.clockGroup, this.handleOverlap, null, this);
    }

    // method to place a clock on the stage, given the coordinates and a "small" or "big" prefix
    placeClock(clockCoordinates, prefix) {

        // clock creation
        let clockSprite = this.clockGroup.create(clockCoordinates.x * gameOptions.gridSize, clockCoordinates.y * gameOptions.gridSize, prefix + "clock");

        // faceSprite is the clock face
        let faceSprite = this.add.sprite(clockSprite.x, clockSprite.y, prefix + "clockface");

        // clock face is not visible by default
        faceSprite.visible = false;

        // clock face is stored as a custom clock property
        clockSprite.face = faceSprite;

        // hand sprite is the clock hand
        let handSprite = this.handGroup.create(clockSprite.x, clockSprite.y, prefix + "hand");

        // set clock hand tint
        handSprite.tint = 0x2babca;

        // set a random clock hand rotation
        handSprite.rotation = Phaser.Math.Angle.Random();

        // set a random angular velocity
        handSprite.body.angularVelocity = Phaser.Math.RND.between(levels[gameOptions.startingLevel].clockSpeed[0], levels[gameOptions.startingLevel].clockSpeed[1]) * Phaser.Math.RND.sign();

        // clock hand is stored as a custom clock property
        clockSprite.hand = handSprite;

        // increase totalCloks
        this.totalClocks ++;

        // return the clock itself
        return clockSprite;
    }

    // method to fire the ball. It's called throwBall rather than fireBall because the game is not a RPG :)
    throwBall() {

        // if we can fire...
        if(this.canFire) {

            // set canFire to false, we are already firing
            this.canFire = false;

            // get active clock hand rotation
            let handAngle = this.activeClock.hand.rotation;

            // place the ball at the same active clock position
            this.ball.x = this.activeClock.x;
            this.ball.y = this.activeClock.y;

            // set the ball visible
            this.ball.visible = true;

            // calculate velocity according to clock angle rotation
            let ballVelocity = this.physics.velocityFromRotation(handAngle, gameOptions.ballSpeed);

            // set ball velocity
            this.ball.body.setVelocity(ballVelocity.x, ballVelocity.y);

            // destroy active clock, clock face and clock hand
            this.activeClock.hand.destroy();
            this.activeClock.face.destroy();
            this.activeClock.destroy();
        }
    }

    // method to handle overlap between ball and clock
    handleOverlap(ball, clock) {

        // can't we fire? (this means: are we firing at the moment)
        if(!this.canFire) {

            // change clock frame and tint color to make it active
            clock.setFrame(1);
            clock.tint = 0x2babca;

            // show clock face
            clock.face.visible = true;

            // change clock hand frame tint and color to make it active
            clock.hand.setFrame(1);
            clock.hand.tint = 0xffffff;

            // now this clock is the active clock
            this.activeClock = clock;

            // hide the ball
            this.ball.visible = false;

            // stop the ball
            this.ball.setVelocity(0, 0);

            // we reached another clock
            this.clocksReached ++;

            // are there more clocks to reach?
            if(this.clocksReached < this.totalClocks) {

                // we can fire again
                this.canFire = true;
            }
            else {

                // advance by one level
                gameOptions.startingLevel = (gameOptions.startingLevel + 1) % levels.length;

                // wait one second and a half, then restart the game
                this.time.addEvent({
                    delay: 1500,
                    callbackScope: this,
                    callback: function() {
                        this.scene.start("PlayGame");
                    }
                });
            }
        }
    }
}

// the levels. 0: empty tile / 1: small clock / 2: big clock

let levels = [
// level 1
{
     clockSpeed: [200, 450],
     tiledOutput: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
},
// level 2
{
     clockSpeed: [200, 450],
     tiledOutput: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
},
// level 3
{
     clockSpeed: [200, 450],
     tiledOutput: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
},
// level 4
{
     clockSpeed: [200, 450],
     tiledOutput: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
},
// level 5
{
     clockSpeed: [200, 450],
     tiledOutput: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
},
// level 6
{
     clockSpeed: [200, 450],
     tiledOutput: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
},
// level 7
{
     clockSpeed: [200, 450],
     tiledOutput: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
},
// level 8
{
     clockSpeed: [200, 450],
     tiledOutput: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
},
// level 9
{
     clockSpeed: [200, 450],
     tiledOutput: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0]
},
// level 10
{
     clockSpeed: [200, 450],
     tiledOutput: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, 0]
}
]

Since the game has disappeared from the store, it would be nice to retrieve the original levels and publish them as a HTML5 games, maybe I’ll manage to get the levels from the author, stay tuned and download the source code of the prototype.

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