Talking about Drag and Match game, Game development, HTML5, Javascript and Phaser.
In most cases, small things make simple games look great. If you look at my latest HTML5 Drag and Match engine updated to Phaser 2.6.2 with masks and a lot of custom options you will see when you drag and then release the mouse there isn’t any kind of animation and tiles just jump in their final position. This is not possible in a game made in 2017. You have to add an animation to make tiles smoothly move in their final position. That what I did and what I am going to show you in this post. We will use a fast tween to place tiles in their final place once the player releases the input. Here is the modified game: I made the tiles a bit bigger (actually, the game size a bit smaller) to let you know what happens. Try to drag a row or a column, release and see the quick tween as tiles move into their final position. I added some lines to the source code, which are obviously commented and highlighted, mostly located inhandleStop
method:
// the game itself
var game;
// global game options
var gameOptions = {
// width of the game, in pixels
gameWidth: 400,
// height of the game, in pixels
gameHeight: 400,
// size of the sprite sheet, in pixels
spritesheetSize: 50,
// size of each tile, in pixels
tileSize: 50,
// size of the field, in tiles
fieldSize: 6,
// different tile types
tileTypes: 6,
// distance from the left of the screen to the left of the board, in pixels
offsetX: 50,
// distance from the top of the screen to the top of the board, in pixels
offsetY: 50,
// duration of the tween to adjust tiles, in milliseconds
tweenSpeed: 100
}
// some constants to be used in the game
// I am not dragging
var NO_DRAG = 0;
// I am dragging horizontally
var HORIZONTAL_DRAG = 1;
// I am dragging vertically
var VERTICAL_DRAG = 2;
// The game state is "doing nothing"
var GAME_STATE_IDLE = 0;
// When the player is dragging a row/column
var GAME_STATE_DRAG = 1;
// When the player stops dragging
var GAME_STATE_STOP = 2;
// when the window has been fully loaded
window.onload = function() {
// creation of a Game instance
game = new Phaser.Game(gameOptions.gameWidth, gameOptions.gameHeight);
// adding "PlayGame" state
game.state.add("PlayGame", playGame)
// starting "PlayGame" state
game.state.start("PlayGame");
}
var playGame = function(game){}
playGame.prototype = {
// when the state preloads
preload: function(){
// loading the spritesheet with all tile images
game.load.spritesheet("tiles", "tiles.png", gameOptions.spritesheetSize, gameOptions.spritesheetSize);
// setting the game on maximum scale mode to cover the entire screen
game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
game.scale.pageAlignHorizontally = true;
game.scale.pageAlignVertically = true;
},
// once the state has been created
create: function(){
// tileArray is the array which will contain all tiles
this.tileArray = [];
// creation of the group which will contain all tiles
this.tileGroup = game.add.group();
// adjusting group position according to offset
this.tileGroup.x = gameOptions.offsetX;
this.tileGroup.y = gameOptions.offsetY;
// creation of a mask with the same size of the board to be placed in the same position of the group
// this way we are hiding everything is outside the board
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;
// filling the board with tiles thanks to "addTile" method
for(var i = 0; i < gameOptions.fieldSize; i++){
this.tileArray[i] = [];
for(j = 0; j < gameOptions.fieldSize; j++){
this.addTile(i, j);
}
}
// adding the temporary tile thanks to addTempTile method
this.addTempTile();
// waiting for player input to call pickTile method
game.input.onDown.add(this.pickTile, this);
// the game has just been created, so we are doing nothing
this.gameState = GAME_STATE_IDLE;
},
// function to add a tile at a given row and column
addTile: function(row, col){
// choosing a random tile
var randomTile = game.rnd.integerInRange(0, gameOptions.tileTypes - 1);
// creation of the sprite in the proper position
var theTile = game.add.sprite(col * gameOptions.tileSize, row * gameOptions.tileSize, "tiles");
// setting tile width and height
theTile.width = gameOptions.tileSize;
theTile.height = gameOptions.tileSize;
// showing the frame according to tile value
theTile.frame = randomTile;
// saving the value inside a custom property
theTile.value = randomTile;
// inserting the tile in tileArray array
this.tileArray[row][col] = theTile;
// adding the sprite to tileGroup group
this.tileGroup.add(theTile);
},
// function to add the temporary tile
addTempTile: function(){
// creation of the sprite, no matter the position, we won't show it at the moment
this.tempTile = game.add.sprite(0, 0, "tiles");
// setting its width and height
this.tempTile.width = gameOptions.tileSize;
this.tempTile.height = gameOptions.tileSize;
// setting the sprite to non visible
this.tempTile.visible = false;
// adding the sprite to tileGroup group
this.tileGroup.add(this.tempTile);
},
// function to triggered when the player touches/clicks on the canvas
pickTile: function(e){
// determining row and column according to input position, tile size and offset
this.movingRow = Math.floor((e.position.y - gameOptions.offsetY) / gameOptions.tileSize);
this.movingCol = Math.floor((e.position.x - gameOptions.offsetX) / gameOptions.tileSize);
// if row and column are actually inside game field...
if(this.movingRow >= 0 && this.movingCol >= 0 && this.movingRow < gameOptions.fieldSize && this.movingCol < gameOptions.fieldSize){
// at the moment we aren't dragging
this.dragDirection = NO_DRAG;
// removing the listener which waits for the input to begin
game.input.onDown.remove(this.pickTile, this);
// adding a listener which waits for the input to end then call releaseTile method
game.input.onUp.add(this.releaseTile, this);
// adding a listener which waits for the input to move then call moveTile method
game.input.addMoveCallback(this.moveTile, this);
}
},
// function to be executed at each frame
update:function(){
// checking game state to see what to do
switch(this.gameState){
// we are dragging
case GAME_STATE_DRAG:
// call handleDrag method
this.handleDrag();
break;
// we just stopped dragging
case GAME_STATE_STOP:
// call handleStop method
this.handleStop();
break;
}
// at the end of the function, we set gameState again to idle
this.gameState = GAME_STATE_IDLE;
},
// function to handle - and draw on the canvas - the game when the player drags a row/column
handleDrag:function(){
// two different things to do according to drag direction
switch(this.dragDirection){
// horizontal drag
case HORIZONTAL_DRAG:
// hiding temporary tile
this.tempTile.visible = false;
// placing the temporary tile in the proper row
this.tempTile.y = this.movingRow * gameOptions.tileSize;
// deltaX is the amount of tiles we are moving
var deltaX = (Math.floor(this.distX / gameOptions.tileSize) % gameOptions.fieldSize);
// deltaX >= 0 means we are moving to the right (or not moving)
if (deltaX >= 0) {
// temporary tile frame is now the same as the rightmost visible tile
this.tempTile.frame = this.tileArray[this.movingRow][gameOptions.fieldSize - 1 - deltaX].value;
}
// we are moving to the left
else{
// temporary tile frame is now the same as the leftmost visible tile
deltaX = deltaX * -1 - 1;
this.tempTile.frame = this.tileArray[this.movingRow][deltaX].value;
}
// looping through all the moving row
for(var i = 0; i < gameOptions.fieldSize; i++){
// adjusting each tile horizontal position
this.tileArray[this.movingRow][i].x = (i * gameOptions.tileSize + this.distX) % (gameOptions.tileSize * gameOptions.fieldSize);
// if tile position is less than zero...
if (this.tileArray[this.movingRow][i].x < 0) {
// ... place it on the opposite side of the game field
this.tileArray[this.movingRow][i].x += gameOptions.tileSize * gameOptions.fieldSize;
}
}
// tileX is the amount of pixels we are moving, capped to gameOptions.tileSize
var tileX = this.distX % gameOptions.tileSize;
// if the amount is greater than zero (moving to the right)
if(tileX > 0){
// placing temporary tile before the leftmost tile
this.tempTile.x = tileX - gameOptions.tileSize;
// showing temportary tile
this.tempTile.visible = true;
}
// if the amount is less than zero (moving to the left)
if(tileX < 0){
// placing temporary tile before the leftmost tile
this.tempTile.x = tileX;
// showing temportary tile
this.tempTile.visible = true;
}
break;
// vertical drag, same concept seen in horizontal drag, just applied to Y axis
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].value;
}
else{
deltaY = deltaY * -1 - 1;
this.tempTile.frame = this.tileArray[deltaY][this.movingCol].value;
}
for(var i = 0; i < gameOptions.fieldSize; i++){
this.tileArray[i][this.movingCol].y = (i * gameOptions.tileSize + this.distY) % (gameOptions.tileSize * gameOptions.fieldSize);
if (this.tileArray[i][this.movingCol].y < 0) {
this.tileArray[i][this.movingCol].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;
}
},
// function to handle - and draw on the canvas - the game when the player stops dragging a row/column
handleStop:function(){
// two different things to do according to drag direction
switch(this.dragDirection){
// horizontal drag
case HORIZONTAL_DRAG:
// we have to find how many "half tiles" we dragged
var shiftAmount = Math.floor(this.distX / (gameOptions.tileSize / 2));
// and now let's see how many tiles we dragged, with a modulo operation because the max amount is fieldSize - 1
shiftAmount = Math.ceil(shiftAmount / 2) % gameOptions.fieldSize;
// creation of a temporary array
var tempArray = [];
// now the idea is to insert in tempArray array the tileArray items in the order they are at the end of the drag
// when shiftAmount is greater than 0, we dragged to the right
if(shiftAmount > 0){
for(var i = 0; i < gameOptions.fieldSize; i++){
tempArray[(shiftAmount + i) % gameOptions.fieldSize] = this.tileArray[this.movingRow][i].value;
}
}
// when shiftAmount is less than zero, we dragged to the left
else{
shiftAmount *= -1;
for(var i = 0; i < gameOptions.fieldSize; i++){
tempArray[i] = this.tileArray[this.movingRow][(shiftAmount + i) % gameOptions.fieldSize].value;
}
}
// the offset is the amount of pixels we dragged, with tileSize as maximum
var offset = this.distX % gameOptions.tileSize;
// if we dragged for more than half a tile...
if(Math.abs(offset) > gameOptions.tileSize / 2){
// adjusting the offset according we dragged to the left (less than zero) or to the right
if(offset < 0){
offset = offset + gameOptions.tileSize;
}
else{
offset = offset - gameOptions.tileSize;
}
}
// copying content from tempArray to tileArray and adjust tile position
for(i = 0; i < gameOptions.fieldSize; i++){
this.tileArray[this.movingRow][i].value = tempArray[i];
this.tileArray[this.movingRow][i].frame = tempArray[i];
this.tileArray[this.movingRow][i].x = i * gameOptions.tileSize + offset;
// tween to adjust tile position
game.add.tween(this.tileArray[this.movingRow][i]).to({
x: i * gameOptions.tileSize
}, gameOptions.tweenSpeed, Phaser.Easing.Cubic.Out, true);
}
// tempdestination is the destination of the temporary tile, outside to the left
var tempDestination = -gameOptions.tileSize
// if the offset is less than zero, then move temporary tile on the opposite side of the board
if(offset < 0){
this.tempTile.x += gameOptions.tileSize * gameOptions.fieldSize;
// also set temporary tile destinatiokn outside to the right
tempDestination = gameOptions.fieldSize * gameOptions.tileSize;
}
// then adjusting temporary tile position with a tween
var tween = game.add.tween(this.tempTile).to({
x: tempDestination
}, gameOptions.tweenSpeed, Phaser.Easing.Cubic.Out, true);
// we add the listener for a new input only when the tween is completed
tween.onComplete.add(function(){
game.input.onDown.add(this.pickTile, this);
}, this)
break;
// vertical drag follows the same concepts seen in horizontal drag
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].value;
}
}
else{
shiftAmount *= -1;
for(var i = 0; i < gameOptions.fieldSize; i++){
tempArray[i] = this.tileArray[(shiftAmount + i) % gameOptions.fieldSize][this.movingCol].value;
}
}
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].value = tempArray[i];
this.tileArray[i][this.movingCol].frame = tempArray[i];
this.tileArray[i][this.movingCol].y = i * gameOptions.tileSize + offset;
game.add.tween(this.tileArray[i][this.movingCol]).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(){
game.input.onDown.add(this.pickTile, this);
}, this)
break;
}
// we aren't dragging anymore
this.dragDirection = NO_DRAG;
},
// function to be triggered when the player moves the mouse/finger
moveTile: function(e){
// we are dragging
this.gameState = GAME_STATE_DRAG;
// determining horizontal and vertical distance between start and current input position
this.distX = e.position.x - e.positionDown.x;
this.distY = e.position.y - e.positionDown.y;
// if we aren't dragging yet...
if(this.dragDirection == NO_DRAG){
// how many pixels are we travelling with our finger/mouse?
var distance = e.position.distance(e.positionDown);
// more than 5 pixels? ok, that's enough to determine the direction
if(distance > 5) {
// trigonometry to know drag angle
var dragAngle = Math.abs(Math.atan2(this.distY, this.distX));
// if drag angle is between PI/4 and 3PI/4...
if((dragAngle > Math.PI / 4 && dragAngle < 3 * Math.PI / 4)) {
// ... we can say it's a vertical drag
this.dragDirection = VERTICAL_DRAG;
}
else {
// else it's an horizontal drag
this.dragDirection = HORIZONTAL_DRAG;
}
}
}
},
// function to be triggered when the player releases the mouse/finger
releaseTile: function(){
// we stopped dragging
this.gameState = GAME_STATE_STOP;
// removing listeners
game.input.onUp.remove(this.releaseTile, this);
game.input.deleteMoveCallback(this.moveTile, this);
}
}
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.