HTML5 prototype of iOS hit “Hero Slide” made with Phaser – controlling the game with swipes
Talking about Hero Slide game, Game development, HTML5, Javascript and Phaser.
Learn cross platform HTML5 game development
Check my Gumroad page for commented source code, games and books.
// the game
var game;
// size of each tile, in pixels
var tileSize = 120;
// different kinds of tiles
var tileTypes = 5;
// the game array, the board will be stored here
var gameArray = [];
// field size, in tiles. This will represent a 4x4 tiles field
var fieldSize = 4;
// duration of each tween
var tweenDuration = 100;
// an object with all possible directions
var direction = {
left: 2,
up: 4,
right: 8,
down: 16
}
// creation of the game
window.onload = function() {
game = new Phaser.Game(480, 480);
game.state.add("PlayGame", playGame);
game.state.start("PlayGame");
}
var playGame = function(game){}
playGame.prototype = {
preload: function(){
// preloading the assets
game.load.spritesheet("tiles", "tiles.png", tileSize, tileSize);
},
create: function(){
// initializing game board
for(var i = 0; i < fieldSize; i++){
gameArray[i] = [];
for(var j = 0; j < fieldSize; j++){
// each array item is an object with a tile value (0: empty) and a sprite (null: no sprite)
gameArray[i][j] = {
tileValue : 0,
tileSprite: null
};
}
}
// function to add a new item, will be explained later
this.addItem();
// liteners to handle WASD keys. Each key calls handleKey function
this.upKey = game.input.keyboard.addKey(Phaser.Keyboard.W);
this.upKey.onDown.add(this.handleKey, this);
this.downKey = game.input.keyboard.addKey(Phaser.Keyboard.S);
this.downKey.onDown.add(this.handleKey, this);
this.leftKey = game.input.keyboard.addKey(Phaser.Keyboard.A);
this.leftKey.onDown.add(this.handleKey, this);
this.rightKey = game.input.keyboard.addKey(Phaser.Keyboard.D);
this.rightKey.onDown.add(this.handleKey, this);
// listener for input
game.input.onDown.add(this.beginSwipe, this);
},
// this function will add a new item to the board
addItem: function(){
// emptySpots is an array which will contain all the available empty tiles where to place a new item
var emptySpots = [];
// now we loop through the game board to check for empty tiles
for(var i = 0; i < fieldSize; i++){
for(var j = 0; j < fieldSize; j++){
// remember we define an empty tile as a tile whose tileValue is zero
if(gameArray[i][j].tileValue == 0){
// at this time we push a Point with tile coordinates into emptySpots array
emptySpots.push(new Phaser.Point(j, i));
}
}
}
// newSpot is a randomly picked item in emptySpots array
var newSpot = Phaser.ArrayUtils.getRandomItem(emptySpots);
// if newSpot is not null this means we have a place where to put a new tile
if(newSpot != null){
// selecting a random value between 1 and tileTypes
var tileType = game.rnd.between(1, tileTypes);
// updating game array with the new tile value and sprite
gameArray[newSpot.y][newSpot.x] = {
tileValue: tileType,
tileSprite: game.add.sprite(newSpot.x * tileSize, newSpot.y * tileSize, "tiles", tileType - 1)
}
// we start with the alpha at zero to create a "fade in" tween
gameArray[newSpot.y][newSpot.x].tileSprite.alpha = 0;
// here is the fade in effect
var fadeTween = game.add.tween(gameArray[newSpot.y][newSpot.x].tileSprite).to({
alpha: 1
}, tweenDuration, Phaser.Easing.Linear.None, true);
// now the player can move
fadeTween.onComplete.add(function(e){
// the player can move again
this.canMove = true;
}, this);
}
},
// this function handles player movements
handleKey: function(e){
// time to check for the keycode which generated the event
switch(e.keyCode){
// "A" key (left)
case Phaser.Keyboard.A:
this.handleMovement(direction.left);
break;
// "W" key (up)
case Phaser.Keyboard.W:
this.handleMovement(direction.up);
break;
// "D" key (right)
case Phaser.Keyboard.D:
this.handleMovement(direction.right);
break;
// "S" key (down)
case Phaser.Keyboard.S:
this.handleMovement(direction.down);
break;
}
},
// function to move a tile
moveTile: function(row, col, toRow, toCol){
// another moving tile
this.movingTiles ++;
// moving the tile
var moveTween = game.add.tween(gameArray[row][col].tileSprite).to({
x: gameArray[row][col].tileSprite.x + tileSize * (toCol - col),
y: gameArray[row][col].tileSprite.y + tileSize * (toRow - row)
}, tweenDuration, Phaser.Easing.Linear.None, true);
moveTween.onComplete.add(function(e){
this.endMove();
}, this);
// copying the content of the current tile on the destination tile
gameArray[toRow][toCol] = {
tileValue: gameArray[row][col].tileValue,
tileSprite: gameArray[row][col].tileSprite
}
// setting current tile to empty
gameArray[row][col] = {
tileValue: 0,
tileSprite: null
}
// the player moved!
this.playerMoved = true;
},
// function to move and remove a tile
moveAndRemove: function(row, col, toObject, toRow, toCol){
// another moving tile
this.movingTiles ++;
var moveTween = game.add.tween(gameArray[row][col].tileSprite).to(toObject, tweenDuration, Phaser.Easing.Linear.None, true);
moveTween.onComplete.add(function(e){
e.destroy();
this.endMove();
}, this);
// looking at tile type to see what to do next - at the moment we only manage bombs
switch(gameArray[row][col].tileValue){
// bomb
case 4:
// let's add detonation coordinates to detonations array
this.detonations.push(new Phaser.Point(toCol, toRow));
}
gameArray[row][col] = {
tileValue: 0,
tileSprite: null
}
},
// function to just remove a tile
removeTile: function(row, col){
// another moving tile (ok we are removing it but it's the same)
this.movingTiles ++;
var removeTween = game.add.tween(gameArray[row][col].tileSprite).to({
alpha: 0,
width: 0
}, tweenDuration, Phaser.Easing.Bounce.Out, true);
removeTween.onComplete.add(function(e){
this.movingTiles --;
gameArray[row][col].tileSprite.destroy();
gameArray[row][col] = {
tileValue: 0,
tileSprite: null
}
if(this.movingTiles == 0){
this.addItem();
}
}, this);
},
// function to end a move
endMove: function(){
// one less tile to move
this.movingTiles --;
// if there aren't moving tiles...
if(this.movingTiles == 0){
// do we have to handle detonations or just add a new item?
if(this.detonations.length > 0){
// let's wait half a second before detonate tiles
game.time.events.add(Phaser.Timer.SECOND / 2, this.handleDetonations, this);
}
else{
this.addItem();
}
}
},
// handling detonations
handleDetonations: function(){
// looping through all detonations
for(var i = 0; i < this.detonations.length; i++){
// removing the bomb
this.removeTile(this.detonations[i].y, this.detonations[i].x)
// handle detonations
if(this.detonations[i].y - 1 >= 0 && gameArray[this.detonations[i].y - 1][this.detonations[i].x].tileValue != 0){
this.removeTile(this.detonations[i].y - 1, this.detonations[i].x)
}
if(this.detonations[i].y + 1 < fieldSize && gameArray[this.detonations[i].y + 1][this.detonations[i].x].tileValue != 0){
this.removeTile(this.detonations[i].y + 1, this.detonations[i].x)
}
if(this.detonations[i].x - 1 >= 0 && gameArray[this.detonations[i].y][this.detonations[i].x - 1].tileValue != 0){
this.removeTile(this.detonations[i].y, this.detonations[i].x - 1)
}
if(this.detonations[i].x + 1 < fieldSize && gameArray[this.detonations[i].y][this.detonations[i].x + 1].tileValue != 0){
this.removeTile(this.detonations[i].y, this.detonations[i].x + 1)
}
}
},
// function to start the swipe. We only remove the listener and add a new one to be triggered when the input ends
beginSwipe: function(e){
game.input.onDown.remove(this.beginSwipe, this);
game.input.onUp.add(this.endSwipe, this);
},
// function called when the swipe is over. We'll see if it's a swipe and try to determine its direction
endSwipe: function(e){
// switching listeners as before
game.input.onUp.remove(this.endSwipe, this);
game.input.onDown.add(this.beginSwipe, this);
// swipeTime is the time passed from the beginning of the swipe until the completion of the swipe
var swipeTime = e.timeUp - e.timeDown;
// you get swipeDistace by subtracting the starting swipe position from the final swipe position.
// you will get a vector with the actual amount of pixels the player moved
var swipeDistance = Phaser.Point.subtract(e.position, e.positionDown);
// the magnitude of a vector is the length of the vector itself
var swipeMagnitude = swipeDistance.getMagnitude();
// the normal of a vector is a vector with the same direction but with magnitude equal to 1
var swipeNormal = Phaser.Point.normalize(swipeDistance);
// we decide we have a swipe when:
// * we moved the input by at least 20 pixels (magnitude)
// * the movement took less than one second
// * the orizontal or vertical absolute value of x or y normal components are greater than 0.8,
// that means the movement was mostly horizontal or vertical
if(swipeMagnitude > 20 && swipeTime < 1000 && (Math.abs(swipeNormal.x) > 0.8 || Math.abs(swipeNormal.y) > 0.8)){
if(swipeNormal.x > 0.8){
this.handleMovement(direction.right);
}
if(swipeNormal.x < -0.8){
this.handleMovement(direction.left);
}
if(swipeNormal.y > 0.8){
this.handleMovement(direction.down);
}
if(swipeNormal.y < -0.8){
this.handleMovement(direction.up);
}
}
},
// handling swipes
handleMovement: function(d){
// variable to count the number of moving tiles
this.movingTiles = 0;
// we have to see if the player actually moved
this.playerMoved = false;
// first of all, let's see if the player can move
if(this.canMove){
// if the player can move, let's set canMove to false to prevent the player to move twice
this.canMove=false;
// initialize a detonation array. Will store all detonations, if any, to perform at the end of the turn
this.detonations = [];
this.detonations.length = 0;
switch(d){
case direction.left:
// we scan for game field, from TOP to BOTTOM and from LEFT to RIGHT starting from the 2nd column
for(var i = 0; i < fieldSize; i++){
for(var j = 1; j < fieldSize; j++){
// we can move a tile if it's not empty and the tile on its left is empty
if(gameArray[i][j].tileValue != 0 && gameArray[i][j - 1].tileValue == 0){
// moving the tile
this.moveTile(i, j, i, j - 1);
}
else{
// we can match a tile if it's not empty and the tile on its left has the same value
if(gameArray[i][j].tileValue != 0 && gameArray[i][j - 1].tileValue == gameArray[i][j].tileValue){
// removing the item
this.moveAndRemove(i, j, {x: gameArray[i][j].tileSprite.x - tileSize}, i, j - 1)
}
}
}
}
break;
case direction.up:
// we scan for game field, from TOP to BOTTOM and from LEFT to RIGHT starting from the 2nd row
// applying the same concepts seen before
for(var i = 1; i < fieldSize; i++){
for(var j = 0; j < fieldSize; j++){
if(gameArray[i][j].tileValue != 0 && gameArray[i - 1][j].tileValue == 0){
this.moveTile(i, j, i - 1, j);
}
else{
if(gameArray[i][j].tileValue != 0 && gameArray[i - 1][j].tileValue == gameArray[i][j].tileValue){
this.moveAndRemove(i, j, {y: gameArray[i][j].tileSprite.y - tileSize}, 1 - 1, j)
}
}
}
}
break;
case direction.right:
// we scan for game field, from TOP to BOTTOM and from RIGHT to LEFT starting from the next-to-last column
// applying the same concepts seen before
for(var i = 0; i < fieldSize; i++){
for(var j = fieldSize - 2; j >= 0; j--){
if(gameArray[i][j].tileValue != 0 && gameArray[i][j + 1].tileValue == 0){
this.moveTile(i, j, i, j + 1);
}
else{
if(gameArray[i][j].tileValue != 0 && gameArray[i][j + 1].tileValue == gameArray[i][j].tileValue){
this.moveAndRemove(i, j, {x: gameArray[i][j].tileSprite.x + tileSize}, i, j + 1)
}
}
}
}
break;
case direction.down:
// we scan for game field, from BOTTOM to TOP and from LEFT to RIGHT starting from the next-to-last row
// applying the same concepts seen before
for(var i = fieldSize - 2; i >= 0; i--){
for(var j = 0; j < fieldSize; j++){
if(gameArray[i][j].tileValue != 0 && gameArray[i + 1][j].tileValue == 0){
this.moveTile(i, j, i + 1, j);
}
else{
if(gameArray[i][j].tileValue != 0 && gameArray[i + 1][j].tileValue == gameArray[i][j].tileValue){
this.moveAndRemove(i, j, {y: gameArray[i][j].tileSprite.y + tileSize}, i + 1, j)
}
}
}
}
break;
}
// checking for invalid move
if(!this.playerMoved){
this.canMove = true;
}
}
}
}
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.