Talking about Hero Slide game, Game development, HTML5, Javascript and Phaser.
In 2016 (actually also in 2015, 2014, and so on back at least until 2010) you can’t create a tile game without allowing players to control it with gestures. That’s why I am showing you how to control our Hero Slide prototype using swipes. Let’s make a recap of the steps we saw so far: In step 1 we saw how to * Initialize the game * Place random tiles on the map * Move tiles with WASD keys Then in step 2 we added these features: * Add animation to tile creation and movement * Match tiles In step 3 we covered: * code optimization * explosions Finally in step 4 we improved the game by: * adding animations * allowing players to perform only legal moves The game was controlled with WASD keys, so it’s time to allow players to move tiles using swipes. Here is the game: Move tiles with WASD keys or with swipes and try to match normal tiles and bombs – focus the canvas first. I also added a different easing to the animation which removes the bombs, as well as a small delay to let the player figure out what’s happening. Detecting swipes with Phaser is really easy, due to its wonderful event management, have a look at the commented code:
// 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.