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.