Talking about SameGame game, Game development, HTML5, Javascript and Phaser.
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 > 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 >= 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://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] != null && 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;
}
}
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.