HTML5 prototype of iOS hit “Hero Slide” made with Phaser – controlling the game with swipes

Read all posts about "" game
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;
               }    
                           
          }
     }
}
Without using any external library, it’s very easy to detect swipes with Phaser, featuring swipe distance, swipe time and swipe angle, mostly based on Phaser Point class. We almost have a complete game to play, download the source code and play with it while you wait for next step.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

215 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stairs
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers