Get the full commented source code of

HTML5 Suika Watermelon Game

Talking about SameGame game, Game development, HTML5, Javascript and Phaser.

This is an important update to the SameGame series because I making the game endless, allowing you to create most of the games mentioned in the post 10 successful games you can easily create starting from the SameGame engine and I am using object pooling to handle tiles management. Object pooling is a technique which stores a collection of a particular object that an application will create and keep on hand for those situations where creating each instance is expensive. In this case, each time you pick and remove tiles, their sprites should be destroyed. Then, new sprites should be created to replace the ones you just destroyed. That is, if you make a four tiles combo, you have to destroy the 4 old sprites and create 4 new sprites. Then you make a 11 tiles combo. This means 11 sprites destroyed and 11 new sprites to create. Although I am sure Phaser has a good memory management and garbage collection, this can be very resource-consuming in the long run. So the idea is never to delete removed tiles, which will be temporarily stored in a repository (in this case an array) until a new tile is needed, and we just recover the previously stored tile. This also happens when you buy a pizza at a restaurant, you complain because it’s not tasty, and they pretend to change your bad pizza with a new one but they just serve you the same pizza again. It happens. No doubt. Anyway, this is the game we are going to create:
Select a tile with at least another tile of the same color around it, and see what happens. No sprites are destroyed. It’s just object pooling. Here is the source code:
// 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);
			}
		}
          
          // tilePool is the array which will contain removed tiles to be recycled
          this.tilePool = [];
          
          // waiting for user input
          game.input.onDown.add(this.pickTile, this);
	},
     
     // 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 an image
          var theTile = game.add.sprite(tileXPos, tileYPos, "tiles");
		
          // 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
          var tileValue = game.rnd.integerInRange(0, gameOptions.colors.length - 1);
          
          // tinting the tile
          theTile.tint = gameOptions.colors[tileValue];
          
          // adding the image to "tilesArray" array, creating an object
          this.tilesArray[row][col] = {
               tileSprite: theTile, // tile image
               isEmpty: false, // is it an empty tile? not at the moment
               coordinate: new Phaser.Point(col, row), // storing tile coordinate, useful during flood fill
               value: tileValue // the value (color) of the tile
          };
          
          // also adding it to "tileGroup" group
	     this.tileGroup.add(theTile);	
	},
     
     // this function is executed at each user input (click or touch)
     pickTile: function(e){          
     
          // can the player pick a tile?
          if(this.canPick){
          
               // determining x and y position of the input inside tileGroup
               var posX = e.x - this.tileGroup.x;
               var posY = e.y - this.tileGroup.y;
               
               // transforming coordinates into actual rows and columns
               var pickedRow = Math.floor(posY / gameOptions.tileSize);
               var pickedCol = Math.floor(posX / gameOptions.tileSize);
               
               // checking if row and column are inside the actual game field
               if(pickedRow >= 0 && pickedCol >= 0 && pickedRow < gameOptions.fieldSize.rows && pickedCol < gameOptions.fieldSize.cols){
               
                    // this is the tile we picked
                    var pickedTile = this.tilesArray[pickedRow][pickedCol];
                    
                    // 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(pickedTile.coordinate, pickedTile.value);
                    
                    // do we have more than one tile in the array?
                    if(this.filled.length > 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].tileSprite).to({
                    alpha: 0
               }, 300, Phaser.Easing.Linear.None, true);
               
               // placing the sprite in the array of sprites to be recycled
               this.tilePool.push(this.tilesArray[this.filled[i].y][this.filled[i].x].tileSprite);
               
               // once the tween has been completed...
               tween.onComplete.add(function(e){ 
                    
                    // 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);
               
               // now the tile is empty
               this.tilesArray[this.filled[i].y][this.filled[i].x].isEmpty = true;
          }
     },
     
     // 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 >= 0; i--){
               for(var j = 0; j < gameOptions.fieldSize.cols; j++){
               
                    // if we have a tile...
                    if(!this.tilesArray[i][j].isEmpty){
                    
                         // let's count how many holes we can find below this tile
                         var holesBelow = this.countSpacesBelow(i, j);
                         
                         // 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, false);                                                                   
                         }
                    }
                    
                         
               }
          }
          
          // if we looped trough all tiles but did not fill anything...
          if(!filled){
          
               // let's see if there are horizontal holes to fill
               this.canPick = true;      
          }
          
          // now it's time to reuse tiles saved in the pool (tilePool array),
          // let's start with a loop through each column
          for(i = 0; i < gameOptions.fieldSize.cols; i++){
          
               // counting how many empty spaces we have in each column
               var topHoles = this.countSpacesBelow(-1, i);
               
               // then for each empty space...
               for(j = topHoles - 1; j >= 0; j--){
               
                    // get the tile to be reused from the pool
                    var reusedTile = this.tilePool.shift();
                    
                    // y position is above the field, to make tile "fall down"
                    reusedTile.y =  (j - topHoles) * gameOptions.tileSize + gameOptions.tileSize / 2;
                    
                    // x position is just the column
                    reusedTile.x = i * gameOptions.tileSize + gameOptions.tileSize / 2;
                    
                    // setting alpha back to 1
                    reusedTile.alpha = 1;
                    
                    // setting a new tile value
                    var tileValue = game.rnd.integerInRange(0, gameOptions.colors.length - 1);
                    
                    // tinting the tile with the new color
                    reusedTile.tint = gameOptions.colors[tileValue];    
                    
                    // setting the item with the new values
                    this.tilesArray[j][i] = {
                         tileSprite: reusedTile,
                         isEmpty: false,
                         coordinate: new Phaser.Point(i, j),
                         value: tileValue
                    }  
                    
                    // and finally make the tile fall down
                    this.moveDownTile(0, i, j, true);                    
               }
          }
     },
     
     // function to count how many empty tiles we have under a given tile
     countSpacesBelow: function(row, col){
          var result = 0;
          for(var i = row + 1; i < gameOptions.fieldSize.rows; i++){
               if(this.tilesArray[i][col].isEmpty){
                    result ++;
               }     
          }
          return result;
     },
     
     // function to move down a tile
     moveDownTile: function(fromRow, fromCol, toRow, justMove){
     
          // a tile can be just moved (when it's a "new" tile falling from above) or 
          // must be moved updating the game field (when it's an "old" tile falling down from its previous position)
          // "justMove" flag handles this operation
          if(!justMove){
          
               // saving the tile itself and its value in temporary variables
               var tileToMove = this.tilesArray[fromRow][fromCol].tileSprite;
               var tileValue = this.tilesArray[fromRow][fromCol].value;
          
               // adjusting tilesArray items actually creating the tile in the new position...
               this.tilesArray[toRow][fromCol] = {
                    tileSprite: tileToMove,
                    isEmpty: false,
                    coordinate: new Phaser.Point(fromCol, toRow),
                    value: tileValue
               }  
               
               // the old place now is set to null
               this.tilesArray[fromRow][fromCol].isEmpty = true;        
          }
          
          // distance to travel, in pixels, by the tile
          var distanceToTravel = (toRow * gameOptions.tileSize + gameOptions.tileSize / 2) - this.tilesArray[toRow][fromCol].tileSprite.y
          
          // a tween manages the movement
          var tween = game.add.tween(this.tilesArray[toRow][fromCol].tileSprite).to({
               y: toRow * gameOptions.tileSize + gameOptions.tileSize / 2         
          }, distanceToTravel / 2, 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.canPick = true;
               }
          }, this)
          
     },
     
     // 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].isEmpty){ 
                    result ++;
               }
          }
          return result;
     },
     
     // flood fill function, for more information
     // http://emanueleferonato.com/2008/06/06/flash-flood-fill-implementation/
     floodFill: function(p, n){
          if(p.x < 0 || p.y < 0 || p.x >= gameOptions.fieldSize.cols || p.y >= gameOptions.fieldSize.rows){
               return;
          }
          if(!this.tilesArray[p.y][p.x].isEmpty && this.tilesArray[p.y][p.x].value == n && !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 && this.filled[i].y == p.y){
                    return true;
               }
          }
          return false;
     }
}
Using object pooling whenever possible will dramatically decrease the resources used by your game. Download the source code of the entire project and have a go.

Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.