Talking about Drag and Match game, Game development, HTML5, Javascript and Phaser.
Finally we are done. If you enjoyed the Drag and Match engine, now it’s completed with tweens, removable tiles, combos, object pooling and everything you need to run your own drag and match game.
Although the prototype is fully working, I will optimize the code a bit during next days while I move on to next prototype, and I would also like to create a class to handle all match-3 game stuff such as board creation, move management, routine to check for valid moves, and so on.
But at the moment I am quite satisfied of the result:
What can I say… the prototype has all the features you should expect, so give it a try, drag to match three or more tiles of the same color and see what happens. if you have a mobile device you can play directly from this link.
Tweens are very slow to let you see how the engine is working.
The source code is huge, about 500 lines, so I am giving it to you without comments at the moment, while I am studying a way to turn it into something more clear, but if you followed the series about Drag and Match engine you shouldn’t get into too much trouble.
var game;
var gameOptions = {
gameWidth: 400,
gameHeight: 400,
spritesheetSize: 50,
tileSize: 50,
fieldSize: 6,
tileTypes: 6,
offsetX: 50,
offsetY: 50,
tweenSpeed: 100,
fadeSpeed: 1000,
fallSpeed: 250
}
var NO_DRAG = 0;
var HORIZONTAL_DRAG = 1;
var VERTICAL_DRAG = 2;
var GAME_STATE_IDLE = 0;
var GAME_STATE_DRAG = 1;
var GAME_STATE_STOP = 2;
window.onload = function() {
game = new Phaser.Game(gameOptions.gameWidth, gameOptions.gameHeight);
game.state.add("PlayGame", playGame)
game.state.start("PlayGame");
}
var playGame = function(game) {}
playGame.prototype = {
preload: function() {
game.load.spritesheet("tiles", "tiles.png", gameOptions.spritesheetSize, gameOptions.spritesheetSize);
game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
game.scale.pageAlignHorizontally = true;
game.scale.pageAlignVertically = true;
},
create: function() {
this.tileArray = [];
this.tilePool = [];
this.tileGroup = game.add.group();
this.tileGroup.x = gameOptions.offsetX;
this.tileGroup.y = gameOptions.offsetY;
this.tileMask = game.add.graphics(this.tileGroup.x, this.tileGroup.y);
this.tileMask.beginFill(0xffffff);
this.tileMask.drawRect(0, 0, gameOptions.fieldSize * gameOptions.tileSize, gameOptions.fieldSize * gameOptions.tileSize);
this.tileGroup.mask = this.tileMask;
this.tileMask.visible = true;
for(var i = 0; i < gameOptions.fieldSize; i++) {
this.tileArray[i] = [];
for(j = 0; j < gameOptions.fieldSize; j++) {
this.addTile(i, j);
}
}
this.addTempTile();
game.input.onDown.add(this.pickTile, this);
this.gameState = GAME_STATE_IDLE;
},
addTile: function(row, col) {
var theTile = game.add.sprite(col * gameOptions.tileSize, row * gameOptions.tileSize, "tiles");
theTile.width = gameOptions.tileSize;
theTile.height = gameOptions.tileSize;
do {
var randomTile = game.rnd.integerInRange(0, gameOptions.tileTypes - 1);
this.tileArray[row][col] = {
tileSprite: theTile,
tileValue: randomTile,
isEmpty: false
};
} while (this.isMatch(row, col));
theTile.frame = randomTile;
this.tileGroup.add(theTile);
},
addTempTile: function() {
this.tempTile = game.add.sprite(0, 0, "tiles");
this.tempTile.width = gameOptions.tileSize;
this.tempTile.height = gameOptions.tileSize;
this.tempTile.visible = false;
this.tileGroup.add(this.tempTile);
},
pickTile: function(e) {
this.movingRow = Math.floor((e.position.y - gameOptions.offsetY) / gameOptions.tileSize);
this.movingCol = Math.floor((e.position.x - gameOptions.offsetX) / gameOptions.tileSize);
if(this.movingRow >= 0 && this.movingCol >= 0 && this.movingRow < gameOptions.fieldSize && this.movingCol < gameOptions.fieldSize) {
this.dragDirection = NO_DRAG;
game.input.onDown.remove(this.pickTile, this);
game.input.onUp.add(this.releaseTile, this);
game.input.addMoveCallback(this.moveTile, this);
}
},
update: function() {
switch(this.gameState) {
case GAME_STATE_DRAG:
this.handleDrag();
break;
case GAME_STATE_STOP:
this.handleStop();
break;
}
this.gameState = GAME_STATE_IDLE;
},
handleDrag: function() {
switch(this.dragDirection) {
case HORIZONTAL_DRAG:
this.tempTile.visible = false;
this.tempTile.y = this.movingRow * gameOptions.tileSize;
var deltaX = (Math.floor(this.distX / gameOptions.tileSize) % gameOptions.fieldSize);
if(deltaX >= 0) {
this.tempTile.frame = this.tileArray[this.movingRow][gameOptions.fieldSize - 1 - deltaX].tileValue;
}
else {
deltaX = deltaX * -1 - 1;
this.tempTile.frame = this.tileArray[this.movingRow][deltaX].tileValue;
}
for(var i = 0; i < gameOptions.fieldSize; i++) {
this.tileArray[this.movingRow][i].tileSprite.x = (i * gameOptions.tileSize + this.distX) % (gameOptions.tileSize * gameOptions.fieldSize);
if(this.tileArray[this.movingRow][i].tileSprite.x < 0) {
this.tileArray[this.movingRow][i].tileSprite.x += gameOptions.tileSize * gameOptions.fieldSize;
}
}
var tileX = this.distX % gameOptions.tileSize;
if(tileX > 0) {
this.tempTile.x = tileX - gameOptions.tileSize;
this.tempTile.visible = true;
}
if(tileX < 0) {
this.tempTile.x = tileX;
this.tempTile.visible = true;
}
break;
case VERTICAL_DRAG:
this.tempTile.visible = false;
this.tempTile.x = this.movingCol * gameOptions.tileSize;
var deltaY = (Math.floor(this.distY / gameOptions.tileSize) % gameOptions.fieldSize);
if(deltaY >= 0) {
this.tempTile.frame = this.tileArray[gameOptions.fieldSize - 1 - deltaY][this.movingCol].tileValue;
} else {
deltaY = deltaY * -1 - 1;
this.tempTile.frame = this.tileArray[deltaY][this.movingCol].tileValue;
}
for(var i = 0; i < gameOptions.fieldSize; i++) {
this.tileArray[i][this.movingCol].tileSprite.y = (i * gameOptions.tileSize + this.distY) % (gameOptions.tileSize * gameOptions.fieldSize);
if(this.tileArray[i][this.movingCol].tileSprite.y < 0) {
this.tileArray[i][this.movingCol].tileSprite.y += gameOptions.tileSize * gameOptions.fieldSize;
}
}
var tileY = this.distY % gameOptions.tileSize;
if(tileY > 0) {
this.tempTile.y = tileY - gameOptions.tileSize;
this.tempTile.visible = true;
}
if(tileY < 0) {
this.tempTile.y = tileY;
this.tempTile.visible = true;
}
break;
}
},
handleStop: function() {
switch(this.dragDirection) {
case HORIZONTAL_DRAG:
var shiftAmount = Math.floor(this.distX / (gameOptions.tileSize / 2));
shiftAmount = Math.ceil(shiftAmount / 2) % gameOptions.fieldSize;
var tempArray = [];
if(shiftAmount > 0) {
for(var i = 0; i < gameOptions.fieldSize; i++) {
tempArray[(shiftAmount + i) % gameOptions.fieldSize] = this.tileArray[this.movingRow][i].tileValue;
}
}
else {
for(var i = 0; i < gameOptions.fieldSize; i++) {
tempArray[i] = this.tileArray[this.movingRow][(Math.abs(shiftAmount) + i) % gameOptions.fieldSize].tileValue;
}
}
var offset = this.distX % gameOptions.tileSize;
if(Math.abs(offset) > gameOptions.tileSize / 2) {
if(offset < 0) {
offset = offset + gameOptions.tileSize;
} else {
offset = offset - gameOptions.tileSize;
}
}
for(i = 0; i < gameOptions.fieldSize; i++) {
this.tileArray[this.movingRow][i].tileValue = tempArray[i];
this.tileArray[this.movingRow][i].tileSprite.frame = tempArray[i];
this.tileArray[this.movingRow][i].tileSprite.x = i * gameOptions.tileSize + offset;
game.add.tween(this.tileArray[this.movingRow][i].tileSprite).to({
x: i * gameOptions.tileSize
}, gameOptions.tweenSpeed, Phaser.Easing.Cubic.Out, true);
}
var tempDestination = -gameOptions.tileSize
if(offset < 0) {
this.tempTile.x += gameOptions.tileSize * gameOptions.fieldSize;
tempDestination = gameOptions.fieldSize * gameOptions.tileSize;
}
var tween = game.add.tween(this.tempTile).to({
x: tempDestination
}, gameOptions.tweenSpeed, Phaser.Easing.Cubic.Out, true);
tween.onComplete.add(function() {
if(this.matchInBoard()) {
this.handleMatches();
} else {
if(shiftAmount != 0) {
shiftAmount *= -1;
tempArray = [];
if(shiftAmount > 0) {
for(var i = 0; i < gameOptions.fieldSize; i++) {
tempArray[(shiftAmount + i) % gameOptions.fieldSize] = this.tileArray[this.movingRow][i].tileValue;
}
} else {
for(var i = 0; i < gameOptions.fieldSize; i++) {
tempArray[i] = this.tileArray[this.movingRow][(Math.abs(shiftAmount) + i) % gameOptions.fieldSize].tileValue;
}
}
for(i = 0; i < gameOptions.fieldSize; i++) {
this.tileArray[this.movingRow][i].tileValue = tempArray[i];
this.tileArray[this.movingRow][i].tileSprite.frame = tempArray[i];
this.tileArray[this.movingRow][i].tileSprite.x = i * gameOptions.tileSize;
var tween = game.add.tween(this.tileArray[this.movingRow][i].tileSprite).to({
alpha: 0.5
}, gameOptions.tweenSpeed / 8, Phaser.Easing.Bounce.Out, true, 0, 8, true);
}
tween.onComplete.add(function() {
if(tween.manager.getAll().length == 1) {
game.input.onDown.add(this.pickTile, this);
}
}, this)
} else {
game.input.onDown.add(this.pickTile, this);
}
}
}, this)
break;
case VERTICAL_DRAG:
var shiftAmount = Math.floor(this.distY / (gameOptions.tileSize / 2));
shiftAmount = Math.ceil(shiftAmount / 2) % gameOptions.fieldSize;
var tempArray = [];
if(shiftAmount > 0) {
for(var i = 0; i < gameOptions.fieldSize; i++) {
tempArray[(shiftAmount + i) % gameOptions.fieldSize] = this.tileArray[i][this.movingCol].tileValue;
}
} else {
for(var i = 0; i < gameOptions.fieldSize; i++) {
tempArray[i] = this.tileArray[(Math.abs(shiftAmount) + i) % gameOptions.fieldSize][this.movingCol].tileValue;
}
}
var offset = this.distY % gameOptions.tileSize;
if(Math.abs(offset) > gameOptions.tileSize / 2) {
if(offset < 0) {
offset = offset + gameOptions.tileSize;
} else {
offset = offset - gameOptions.tileSize;
}
}
for(var i = 0; i < gameOptions.fieldSize; i++) {
this.tileArray[i][this.movingCol].tileValue = tempArray[i];
this.tileArray[i][this.movingCol].tileSprite.frame = tempArray[i];
this.tileArray[i][this.movingCol].tileSprite.y = i * gameOptions.tileSize + offset;
game.add.tween(this.tileArray[i][this.movingCol].tileSprite).to({
y: i * gameOptions.tileSize
}, gameOptions.tweenSpeed, Phaser.Easing.Cubic.Out, true);
}
var tempDestination = -gameOptions.tileSize
if(offset < 0) {
this.tempTile.y += gameOptions.tileSize * gameOptions.fieldSize;
tempDestination = gameOptions.fieldSize * gameOptions.tileSize;
}
var tween = game.add.tween(this.tempTile).to({
y: tempDestination
}, gameOptions.tweenSpeed, Phaser.Easing.Cubic.Out, true);
tween.onComplete.add(function() {
if(this.matchInBoard()) {
this.handleMatches();
} else {
if(shiftAmount != 0) {
shiftAmount *= -1;
tempArray = [];
if(shiftAmount > 0) {
for(var i = 0; i < gameOptions.fieldSize; i++) {
tempArray[(shiftAmount + i) % gameOptions.fieldSize] = this.tileArray[i][this.movingCol].tileValue;
}
} else {
for(var i = 0; i < gameOptions.fieldSize; i++) {
tempArray[i] = this.tileArray[(Math.abs(shiftAmount) + i) % gameOptions.fieldSize][this.movingCol].tileValue;
}
}
for(var i = 0; i < gameOptions.fieldSize; i++) {
this.tileArray[i][this.movingCol].tileValue = tempArray[i];
this.tileArray[i][this.movingCol].tileSprite.frame = tempArray[i];
this.tileArray[i][this.movingCol].tileSprite.y = i * gameOptions.tileSize;
var tween = game.add.tween(this.tileArray[i][this.movingCol].tileSprite).to({
alpha: 0.5
}, gameOptions.tweenSpeed / 8, Phaser.Easing.Bounce.Out, true, 0, 8, true);
}
tween.onComplete.add(function() {
if(tween.manager.getAll().length == 1) {
game.input.onDown.add(this.pickTile, this);
}
}, this)
} else {
game.input.onDown.add(this.pickTile, this);
}
}
}, this)
break;
}
this.dragDirection = NO_DRAG;
},
handleMatches: function() {
this.tilesToRemove = [];
for(var i = 0; i < gameOptions.fieldSize; i++) {
this.tilesToRemove[i] = [];
for(var j = 0; j < gameOptions.fieldSize; j++) {
this.tilesToRemove[i][j] = 0;
}
}
this.handleHorizontalMatches();
this.handleVerticalMatches();
for(var i = 0; i < gameOptions.fieldSize; i++) {
for(var j = 0; j < gameOptions.fieldSize; j++) {
if(this.tilesToRemove[i][j] != 0) {
var tween = game.add.tween(this.tileArray[i][j].tileSprite).to({
alpha: 0
}, gameOptions.fadeSpeed, Phaser.Easing.Linear.None, true);
this.tilePool.push(this.tileArray[i][j].tileSprite);
tween.onComplete.add(function(e) {
if(tween.manager.getAll().length == 1) {
this.fillVerticalHoles();
}
}, this);
this.tileArray[i][j].isEmpty = true;
}
}
}
},
fillVerticalHoles: function() {
for(var i = gameOptions.fieldSize - 2; i >= 0; i--) {
for(var j = 0; j < gameOptions.fieldSize; j++) {
if(!this.tileArray[i][j].isEmpty) {
var holesBelow = this.countSpacesBelow(i, j);
if(holesBelow) {
this.moveDownTile(i, j, i + holesBelow, false);
}
}
}
}
for(i = 0; i < gameOptions.fieldSize; i++) {
var topHoles = this.countSpacesBelow(-1, i);
for(j = topHoles - 1; j >= 0; j--) {
var reusedTile = this.tilePool.shift();
reusedTile.y = (j - topHoles) * gameOptions.tileSize;
reusedTile.x = i * gameOptions.tileSize;
reusedTile.alpha = 1;
var randomTile = game.rnd.integerInRange(0, gameOptions.tileTypes - 1);
reusedTile.frame = randomTile;
this.tileArray[j][i] = {
tileSprite: reusedTile,
tileValue: randomTile,
isEmpty: false
}
this.moveDownTile(0, i, j, true);
}
}
},
moveDownTile: function(fromRow, fromCol, toRow, justMove) {
if(!justMove) {
var spriteSave = this.tileArray[fromRow][fromCol].tileSprite;
var valueSave = this.tileArray[fromRow][fromCol].tileValue;
this.tileArray[toRow][fromCol] = {
tileSprite: spriteSave,
tileValue: valueSave,
isEmpty: false
};
this.tileArray[fromRow][fromCol].isEmpty = true;
}
var distanceToTravel = toRow - this.tileArray[toRow][fromCol].tileSprite.y / gameOptions.tileSize
var tween = game.add.tween(this.tileArray[toRow][fromCol].tileSprite).to({
y: toRow * gameOptions.tileSize
}, distanceToTravel * gameOptions.fallSpeed, Phaser.Easing.Linear.None, true);
tween.onComplete.add(function() {
if(tween.manager.getAll().length == 1) {
if(this.matchInBoard()) {
this.handleMatches();
} else {
game.input.onDown.add(this.pickTile, this);
}
}
}, this)
},
handleHorizontalMatches: function() {
for(var i = 0; i < gameOptions.fieldSize; i++) {
var colorStreak = 1;
var currentColor = -1;
var startStreak = 0;
for(var j = 0; j < gameOptions.fieldSize; j++) {
if(this.tileAt(i, j).tileValue == currentColor) {
colorStreak++;
}
if(this.tileAt(i, j).tileValue != currentColor || j == gameOptions.fieldSize - 1) {
if(colorStreak > 2) {
var endStreak = j - 1
if(this.tileAt(i, j).tileValue == currentColor) {
endStreak = j;
}
for(var k = startStreak; k <= endStreak; k++) {
this.tilesToRemove[i][k]++;
}
}
currentColor = this.tileAt(i, j).tileValue
colorStreak = 1;
startStreak = j;
}
}
}
},
handleVerticalMatches: function() {
for(var i = 0; i < gameOptions.fieldSize; i++) {
var colorStreak = 1;
var currentColor = -1;
var startStreak = 0;
for(var j = 0; j < gameOptions.fieldSize; j++) {
if(this.tileAt(j, i).tileValue == currentColor) {
colorStreak++;
}
if(this.tileAt(j, i).tileValue != currentColor || j == gameOptions.fieldSize - 1) {
if(colorStreak > 2) {
var endStreak = j - 1
if(this.tileAt(j, i).tileValue == currentColor) {
endStreak = j;
}
for(var k = startStreak; k <= endStreak; k++) {
this.tilesToRemove[k][i]++;
}
}
currentColor = this.tileAt(j, i).tileValue
colorStreak = 1;
startStreak = j;
}
}
}
},
moveTile: function(e) {
this.gameState = GAME_STATE_DRAG;
this.distX = e.position.x - e.positionDown.x;
this.distY = e.position.y - e.positionDown.y;
if(this.dragDirection == NO_DRAG) {
var distance = e.position.distance(e.positionDown);
if(distance > 5) {
var dragAngle = Math.abs(Math.atan2(this.distY, this.distX));
if((dragAngle > Math.PI / 4 && dragAngle < 3 * Math.PI / 4)) {
this.dragDirection = VERTICAL_DRAG;
} else {
this.dragDirection = HORIZONTAL_DRAG;
}
}
}
},
releaseTile: function() {
this.gameState = GAME_STATE_STOP;
game.input.onUp.remove(this.releaseTile, this);
game.input.deleteMoveCallback(this.moveTile, this);
},
tileAt: function(row, col) {
if(row < 0 || row >= gameOptions.fieldSize || col < 0 || col >= gameOptions.fieldSize) {
return false;
}
return this.tileArray[row][col];
},
isHorizontalMatch: function(row, col) {
return this.tileAt(row, col).tileValue == this.tileAt(row, col - 1).tileValue && this.tileAt(row, col).tileValue == this.tileAt(row, col - 2).tileValue;
},
isVerticalMatch: function(row, col) {
return this.tileAt(row, col).tileValue == this.tileAt(row - 1, col).tileValue && this.tileAt(row, col).tileValue == this.tileAt(row - 2, col).tileValue;
},
isMatch: function(row, col) {
return this.isHorizontalMatch(row, col) || this.isVerticalMatch(row, col);
},
matchInBoard: function() {
for(var i = 0; i < gameOptions.fieldSize; i++) {
for(var j = 0; j < gameOptions.fieldSize; j++) {
if(this.isMatch(i, j)) {
return true;
}
}
}
return false;
},
countSpacesBelow: function(row, col) {
var result = 0;
for(var i = row + 1; i < gameOptions.fieldSize; i++) {
if(this.tileArray[i][col].isEmpty) {
result++;
}
}
return result;
}
}
And if you want to play with the source code, here is your download link. Have fun.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.