HTML5 SameGame engine powered by Phaser – adding animations

Read all posts about "" game
If you enjoyed the HTML5 SameGame example I showed you last week, here is the second part with animations and a deeply commented source code. Three different animations have been featured: * The fade-out animation when tiles are removed, using a tween on alpha property. * The animation of tiles falling down, using a tween on y property * The animation of tiles scrolling to the left, using a tween on x property with a bounce easing. Adding animations made the game more polished, as you can see.
And here is the complete source code, as promised:
// the game itself
var game;

// this object contains all customizable game options
// changing them will affect gameplay
var gameOptions = {
	gameWidth: 800,    // game width, in pixels 
	gameHeight: 800,   // game height, in pixels
	tileSize: 100,     // tile size, in pixels 
	fieldSize: {       // field size, an object
          rows: 8,      // rows in the field, in units
          cols: 8       // columns in the field, in units
     },
     colors: [0xff0000, 0x00ff00, 0x0000ff, 0xffff00] // tile colors
}

// function to be execute once the page loads
window.onload = function() {
	
     // creation of a new Phaser Game
	game = new Phaser.Game(gameOptions.gameWidth, gameOptions.gameHeight);
     
     // adding "TheGame" state
     game.state.add("TheGame", TheGame);
     
     // launching "TheGame" state
     game.state.start("TheGame");
}

/* ****************** TheGame state ****************** */ 

var TheGame = function(){};

TheGame.prototype = {

     // function to be executed when the game preloads
     preload: function(){
     
          // setting background color to dark grey
          game.stage.backgroundColor = 0x222222;
          
          // load the only graphic asset in the game, a white tile which will be tinted on the fly
          game.load.image("tiles", "assets/sprites/tile.png"); 
     },
     
     // function to be executed as soon as the game has completely loaded
  	create: function(){
     
          // scaling the game to cover the entire screen, while keeping its ratio
          game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
          
          // horizontally centering the game
		game.scale.pageAlignHorizontally = true;
          
          // vertically centering the game
		game.scale.pageAlignVertically = true;
          
          // this function will create the level
  		this.createLevel();
  	}, 
	createLevel: function(){
     
          // canPick tells if we can pick a tile, we start with "true" has at the moment a tile can be picked
          this.canPick = true;   
          
          // tiles are saved in an array called tilesArray
          this.tilesArray = [];
          
          // this group will contain all tiles
		this.tileGroup = game.add.group();
          
          // we are centering the group, both horizontally and vertically, in the canvas
          this.tileGroup.x = (game.width - gameOptions.tileSize * gameOptions.fieldSize.cols) / 2;
          this.tileGroup.y = (game.height -  gameOptions.tileSize * gameOptions.fieldSize.rows) / 2;
          
          // two loops to create a grid made by "gameOptions.fieldSize.rows" x "gameOptions.fieldSize.cols" columns
  		for(var i = 0; i < gameOptions.fieldSize.rows; i++){
               this.tilesArray[i] = [];
			for(var j = 0; j < gameOptions.fieldSize.cols; j++){
                    
                    // this function adds a tile at row "i" and column "j"
				this.addTile(i, j);
			}
		}
	},
     
     // function to add a tile at "row" row and "col" column
	addTile: function(row, col){
          
          // determining x and y tile position according to tile size 
		var tileXPos = col * gameOptions.tileSize + gameOptions.tileSize / 2;
		var tileYPos = row * gameOptions.tileSize + gameOptions.tileSize / 2;
		
          // tile is added as a button which will call "pickTile" function if triggered
          var theTile = game.add.button(tileXPos, tileYPos, "tiles", this.pickTile, this);
		
          // setting tile registration point to its center
          theTile.anchor.set(0.5);
          
          // adjusting tile width and height according to tile size
          theTile.width = gameOptions.tileSize;
          theTile.height = gameOptions.tileSize;
          
          // time to assign the tile a random value, which is also a random color
          theTile.value = game.rnd.integerInRange(0, gameOptions.colors.length - 1);
          
          // tinting the tile
          theTile.tint = gameOptions.colors[theTile.value];
          
          // saving tile coordinate as a Point, to quickly have access to this information
          theTile.coordinate = new Phaser.Point(col, row);
          
          // adding the button to "tilesArray" array
          this.tilesArray[row][col] = theTile;
          
          // also adding it to "tileGroup" group
	     this.tileGroup.add(theTile);	
	},
     
     // this function is executed each time a tile is selected
     pickTile: function(e){
     
          // can the player pick a tile?
          if(this.canPick){
          
               // the most secure way to have a clean and empty array
               this.filled = [];
               this.filled.length = 0;
               
               // performing a flood fill on the selected tile
               // this will populate "filled" array
               this.floodFill(e.coordinate, e.value);
               
               // do we have more than one tile in the array?
               if(this.filled.length &gt; 1){
               
                    // ok, this is a valid move and player won't be able to pick another tile until all animations have been played
                    this.canPick = false;
                    
                    // function to destroy selected tiles
                    this.destroyTiles();
               }
          }
     },
     
     // this function will destroy all tiles we can find in "filled" array
     destroyTiles: function(){      
     
          // looping through the array
          for(var i = 0; i < this.filled.length; i++){
          
               // fading tile out with a tween
               var tween = game.add.tween(this.tilesArray[this.filled[i].y][this.filled[i].x]).to({
                    alpha: 0
               }, 300, Phaser.Easing.Linear.None, true);
               
               // once the tween has been completed...
               tween.onComplete.add(function(e){ 
               
                    // remove the tile 
                    e.destroy();  
                    
                    // we don't know how many tiles we have already removed, so counting the tweens
                    // currently in use is a good way, at the moment
                    // if this was the last tween (we only have one tween running, this one) 
                    if(tween.manager.getAll().length == 1){
                    
                         // call fillVerticalHoles function to make tiles fall down
                         this.fillVerticalHoles();     
                    }                                
               }, this);
               
               // tilesArray item is set to null, this means it's empty now
               this.tilesArray[this.filled[i].y][this.filled[i].x] = null;
          }
     },
     
     // this function will make tiles fall down
     fillVerticalHoles: function(){
     
          // filled is a variable which tells us if we filled a hole
          var filled = false;
          
          // looping through the entire gamefield
          for(var i = gameOptions.fieldSize.rows - 2; i &gt;= 0; i--){
               for(var j = 0; j < gameOptions.fieldSize.cols; j++){
               
                    // if we have a tile...
                    if(this.tilesArray[i][j] != null){
                    
                         // let's count how many holes we can find below this tile
                         var holesBelow = 0;
                         for(var z = i + 1; z < gameOptions.fieldSize.rows; z++){
                              if(this.tilesArray[z][j] == null){
                                   holesBelow ++;
                              }    
                         }
                         
                         // if holesBelow is greater than zero...
                         if(holesBelow){  
                         
                              // we filled a hole, or at least we are about to do it
                              filled = true;
                              
                              // function to move down a tile at column "j" from "i" to "i + holesBelow" row
                              this.moveDownTile(i, j, i + holesBelow);                                                                   
                         }
                    }     
               }
          }
          
          // if we looped trough all tiles but did not fill anything...
          if(!filled){
          
               // let's see if there are horizontal holes to fill
               this.fillHorizontalHoles();      
          }
     },
     
     // function to move down a tile
     moveDownTile: function(fromRow, fromCol, toRow){
     
          // adjusting tilesArray items actually copying the tile to move in the new position...
          this.tilesArray[toRow][fromCol] = this.tilesArray[fromRow][fromCol];
          
          // ... and updating the coordinate
          this.tilesArray[toRow][fromCol].coordinate = new Phaser.Point(fromCol, toRow);
          
          // a tween manages the movement
          var tween = game.add.tween(this.tilesArray[toRow][fromCol]).to({
               y: toRow * gameOptions.tileSize + gameOptions.tileSize / 2         
          }, 250, Phaser.Easing.Linear.None, true);
          
          // same thing as before to see how many tweens remain alive, and if this is the last
          // active tween, call "fillHorizontalHoles" function
          tween.onComplete.add(function(){
               if(tween.manager.getAll().length == 1){
                    this.fillHorizontalHoles();
               }
          }, this)
          
          // the old place now is set to null
          this.tilesArray[fromRow][fromCol] = null;
     },
     
     // this function will make tiles slide to the left
     fillHorizontalHoles: function(){
     
          // we did not fill anything at the moment
          var filled = false
          
          // looping though all columns
          for(i = 0; i < gameOptions.fieldSize.cols - 1; i++){
               
               // if this is an empty column (we can see it thanks to tilesInColumn function)
               if(this.tilesInColumn(i) == 0){
               
                    // looping through the columns at its right...
                    for(j = i + 1; j < gameOptions.fieldSize.cols; j++){
                    
                         // ...until we find a non-empty column
                         if(this.tilesInColumn(j) != 0){
                         
                              // at this time all tiles in this columns must be moved to the left
                              for(z = 0; z < gameOptions.fieldSize.rows; z++){
                                   if(this.tilesArray[z][j] != null){
                                   
                                        // we filled!!
                                        filled = true;
                                        
                                        // function to move left a tile at row "z" from "j" to "i" column
                                        this.moveLeftTile(z, j, i)
                                   }    
                              }
                              break;
                         }     
                    }
               }
          }
          
          // if we did not fill anything...
          if(!filled){
          
               // the move is over and the player can pick another tile
               this.canPick = true;     
          }
     },
     
     // this is basically the same concept seen at "moveDownTile", we just move tiles left with a bounce effect
     moveLeftTile: function(fromRow, fromCol, toCol){
          this.tilesArray[fromRow][toCol] = this.tilesArray[fromRow][fromCol];
          this.tilesArray[fromRow][toCol].coordinate = new Phaser.Point(toCol, fromRow);
          var tween = game.add.tween(this.tilesArray[fromRow][toCol]).to({
               x: toCol * gameOptions.tileSize + gameOptions.tileSize / 2  
          }, 500, Phaser.Easing.Bounce.Out, true);
          tween.onComplete.add(function(){
               if(tween.manager.getAll().length == 1){
                    this.canPick = true;     
               }
          }, this)
          this.tilesArray[fromRow][fromCol] = null;
     },
     
     // function which counts tiles in a column
     tilesInColumn: function(col){
          var result = 0;
          for(var i = 0; i < gameOptions.fieldSize.rows; i++){
               if(this.tilesArray[i][col] != null){ 
                    result ++;
               }
          }
          return result;
     },
     
     // flood fill function, for more information
     // http://www.emanueleferonato.com/2008/06/06/flash-flood-fill-implementation/
     floodFill: function(p, n){
          if(p.x < 0 || p.y < 0 || p.x &gt;= gameOptions.fieldSize.cols || p.y &gt;= gameOptions.fieldSize.rows){
               return;
          }
          if(this.tilesArray[p.y][p.x] != null &amp;&amp; this.tilesArray[p.y][p.x].value == n &amp;&amp; !this.pointInArray(p)){
               this.filled.push(p);
               this.floodFill(new Phaser.Point(p.x + 1, p.y), n);
               this.floodFill(new Phaser.Point(p.x - 1, p.y), n); 
               this.floodFill(new Phaser.Point(p.x, p.y + 1), n);
               this.floodFill(new Phaser.Point(p.x, p.y - 1), n);       
          }
     },
     
     // there isn't a built-in javascript method to see if an array contains a point, so here it is.
     pointInArray: function(p){
          for(var i = 0; i < this.filled.length; i++){
               if(this.filled[i].x == p.x &amp;&amp; this.filled[i].y == p.y){
                    return true;
               }
          }
          return false;
     }
}
You can also download the full source code of the example, let me know if you can create a full SameGame clone out of it.

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

214 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
// 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