HTML5 prototype of iOS hit “Hero Slide” made with Phaser – handling animations and allowing...

Datetime:2016-08-22 23:10:11          Topic: HTML5           Share

To continue theHero Slide prototype added a couple of weeks ago, we need to properly handle animations and allow only legal moves.

Let’s make a small recap:

Instep 1 we saw how to

* Initialize the game

* Place random tiles on the map

* Move tiles with WASD keys

Then instep 2 we added these features:

* Add animation to tile creation and movement

* Match tiles

Finally instep 3 we covred:

* code optimization

* explosions

Talking about explosions, it wasn’t a nice effect due to a messy animation management, so in this 4th step we will see how to syncronize animations and don’t let new items appear if the player did not perform a legal move.

Move tiles with WASD keys and try to match normal tiles and bombs – focus the canvas first.

The key to handle animations is to keep track of how many tiles we are moving with movingTiles variable, which increases each time we are about to move a tile with a tween, and decreases once the tween is completed. This time we can know when every tween has been completed and act accordingly.

Each player move now is managed this way.

* We see if it’s a valid move. If not, we wait for another move.

* We move the tiles, disabiling player movement.

* We match all tiles according to game rules.

* For each bomb matched – if any – we make an explosion.

* We allow the player to move again.

And this is the commented source 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;
 
// 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);    
    },
    
    // 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){
    
          // 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;
              
              // time to check for the keycode which generated the event
              switch(e.keyCode){
              
                    // "A" key (left)
                    case Phaser.Keyboard.A:
                        
                        // 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;
                    
                    // "W" key (up)
                    case Phaser.Keyboard.W:
                    
                        // 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;
                    
                    // "D" key (right)
                    case Phaser.Keyboard.D:
                    
                        // 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;
                    
                    // "S" key (down)
                    case Phaser.Keyboard.S:
                        
                        // 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;
              }    
          }
    },
    
    // 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
          }, tweenDuration,  Phaser.Easing.Linear.None, 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){
                    this.handleDetonations();
              }    
              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)    
              }
          }    
    }
}

Next time I will try to convert it into something playable, meanwhile download the source code .

From null to full HTML5 cross platform game

I will take you by hand from the bare bones of JavaScript programming through the creation of a full cross platform HTML5 game, with detailed explainations and source code.

If you don't know where to start, then From null to full HTML5 cross platform game is the book for you.





About List