Talking about Down The Mountain game, Game development, HTML5, Javascript, Phaser and TypeScript.
With the release of Phaser 3.60, it is time to upgrade various prototypes to this latest version, using TypeScript and dividing the code into classes where possible.
Today I am focusing on Down the Mountain, a mobile smashing hit back in 2015, which in my opinion still has something to say.
This post covers the conversion of the script published in this blog post, when I introduced a fake 3D movement using Bezier curves.
Look at the result:
Tap or click on the left side of the canvas to make the player move down to the left, or the right side of the canvas to make the player move down to the right.
All tiles are recycled once they reach the top of the canvas.
Now, look at the source code, still uncommented because I plan to add some features, then comment it.
We have one HTML file, one CSS file and seven TypeScript files.
index.html
The web page which hosts the game, to be run inside thegame element.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="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.
* {
padding : 0;
margin : 0;
}
body {
background-color: #011025;
}
canvas {
touch-action : none;
-ms-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.
// CONFIGURABLE GAME OPTIONS
// changing these values will affect gameplay
export const GameOptions = {
hexagon : {
width : 70,
height : 80
},
gridSize : {
x : 5,
y : 14
},
scrollY : 60
}
main.ts
This is where the game is created, with all Phaser related options.
// MAIN GAME FILE
// modules to import
import Phaser from 'phaser';
import { PreloadAssets } from './preloadAssets';
import { PlayGame } from './playGame';
// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
mode : Phaser.Scale.FIT,
autoCenter : Phaser.Scale.CENTER_BOTH,
parent : 'thegame',
width : 540,
height : 540
}
// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
type : Phaser.AUTO,
backgroundColor : 0x5df4f0,
scale : scaleObject,
scene : [PreloadAssets, PlayGame]
}
// the game itself
new Phaser.Game(configObject);
preloadAssets.ts
Here we preload all assets to be used in the game.
// CLASS TO PRELOAD ASSETS
// this class extends Scene class
export class PreloadAssets extends Phaser.Scene {
// constructor
constructor() {
super({
key : 'PreloadAssets'
});
}
// method to be execute during class preloading
preload() : void {
this.load.image('ground', 'assets/sprites/ground.png');
this.load.spritesheet('player', 'assets/sprites/player.png', {
frameWidth : 56,
frameHeight : 64
});
}
// method to be called once the instance has been created
create() : void {
// call PlayGame class
this.scene.start('PlayGame');
}
}
playGame.ts
Main game file, all game logic is stored here.
// THE GAME ITSELF
import { Player } from './player';
import { Terrain } from './terrain';
// this class extends Scene class
export class PlayGame extends Phaser.Scene {
constructor() {
super({
key : 'PlayGame'
});
}
player : Player;
terrain : Terrain;
// method to be executed when the scene has been created
create() : void {
this.terrain = new Terrain(this);
this.player = new Player(this);
this.input.on('pointerdown', this.handleInput, this);
this.player.on('toolow', this.scrollTerrain, this);
}
handleInput(pointer : Phaser.Input.Pointer) : void {
this.player.move(pointer.x < (this.game.config.width as number / 2));
}
scrollTerrain(delta : number) : void {
Phaser.Actions.IncY(this.terrain.getChildren(), delta);
}
update() : void {
this.player.updatePosition();
this.terrain.updatePosition();
}
}
player.ts
The player, a custom class which extends Sprite object.
// PLAYER CLASS EXTENDS PHASER.GAMEOBJECTS.SPRITE
import { GameOptions } from './gameOptions';
export class Player extends Phaser.GameObjects.Sprite {
canMove : boolean;
row : number;
column : number;
constructor(scene : Phaser.Scene) {
super(scene, scene.game.config.width as number / 2, GameOptions.scrollY + 6, 'player');
scene.add.existing(this);
this.canMove = true;
this.row = 0;
this.column = 2;
}
move(goingLeft : boolean) : void {
if (this.canMove) {
if (goingLeft && (this.column > 0 || (this.row % 2 == 1))) {
this.column -= (1 - this.row % 2);
this.row ++;
this.setFrame(0);
this.lower(-1);
}
if (!goingLeft && this.column < GameOptions.gridSize.x - 1) {
this.column += (this.row % 2);
this.row ++;
this.setFrame(1);
this.lower(1);
}
}
}
lower(delta : number) : void {
let stepX : number = GameOptions.hexagon.width / 2 * delta;
let stepY : number = GameOptions.hexagon.height / 4 * 3;
this.canMove = false;
let startPoint : Phaser.Math.Vector2 = new Phaser.Math.Vector2(this.x, this.y);
let endPoint : Phaser.Math.Vector2 = new Phaser.Math.Vector2(this.x + stepX, this.y + stepY);
let controlPoint1 : Phaser.Math.Vector2 = new Phaser.Math.Vector2(this.x + stepX, this.y + stepY / 2);
let controlPoint2 : Phaser.Math.Vector2 = new Phaser.Math.Vector2(this.x + stepX, this.y + stepY / 2);
let bezierCurve : Phaser.Curves.CubicBezier = new Phaser.Curves.CubicBezier(startPoint, controlPoint1, controlPoint2, endPoint);
let tweenValue : any = {
value : 0,
previousValue : 0
}
this.scene.tweens.add({
targets : tweenValue,
value : 1,
duration : 100,
callbackScope : this,
onComplete : () => {
this.canMove = true;
},
onUpdate : (tween : Phaser.Tweens.Tween, target : any) => {
let position : Phaser.Math.Vector2 = bezierCurve.getPoint(target.value);
let prevPosition : Phaser.Math.Vector2 = bezierCurve.getPoint(target.previousValue);
this.x += position.x - prevPosition.x;
this.y += position.y - prevPosition.y;
target.previousValue = target.value;
}
})
}
updatePosition() : void {
if (this.y > GameOptions.scrollY) {
let distance : number = (this.y - 6) / -25;
this.y += distance;
this.emit('toolow', distance);
}
}
}
tile.ts
A ground tile, a custom class which extends Sprite object.
// TILE CLASS EXTENDS PHASER.GAMEOBJECTS.SPRITE
import { GameOptions } from './gameOptions';
export class Tile extends Phaser.GameObjects.Sprite {
constructor(scene : Phaser.Scene, posX : number, posY : number) {
super(scene, posX, posY, 'ground');
this.setOrigin(0, 0);
scene.add.existing(this);
}
updatePosition() : void {
if (this.y < - GameOptions.hexagon.height) {
this.y += GameOptions.hexagon.height * (GameOptions.gridSize.y * 3 / 4);
}
}
}
terrain.ts
The terrain, a custom class which extends Group object. The terrain is basically a group of Tile objects.
// TERRAIN CLASS EXTENDS PHASER.GAMEOBJECTS.GROUP
import { GameOptions } from './gameOptions';
import { Tile } from './tile';
export class Terrain extends Phaser.GameObjects.Group {
constructor(scene : Phaser.Scene) {
super(scene);
scene.add.existing(this);
for (let i : number = 0; i < GameOptions.gridSize.y; i ++) {
this.addRow(i);
}
}
addRow(i : number) : void {
let offset : number = (this.scene.game.config.width as number - GameOptions.gridSize.x * GameOptions.hexagon.width) / 2;
for (let j : number = 0; j < GameOptions.gridSize.x - i % 2; j ++) {
let hexagonX : number = GameOptions.hexagon.width * j + (GameOptions.hexagon.width / 2) * (i % 2) + offset;
let hexagonY : number = GameOptions.scrollY + GameOptions.hexagon.height * i / 4 * 3;
let tile : Tile = new Tile(this.scene, hexagonX, hexagonY)
this.add(tile);
}
}
updatePosition() : void {
this.getChildren().forEach((child : Phaser.GameObjects.GameObject) => {
let tile : Tile = child as Tile;
tile.updatePosition();
})
}
}
Old glories can live a new life, like this one. Next time I will be adding some obstacles, meanwhile download 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.