Creation of a Flash AS3 match 3 engine by dragging rows and columns rather than swapping items

Read all posts about "" game

Did you play 10000000 a couple of summers ago? It’s a fun mix of RPG and match 3 genres and had a lot of success.

In the game you have to match elements of the same kind, but unlike other famous match 3 game such as Bejeweled, you won’t do it by swapping tiles but by dragging an entire row or a column

Another example of this way of matching stuff is Awesome Blossom.

So we are going to create the engine which handles gems horizontal and vertical movement. It’s a little more complicated than the swap engine, but we are going to get it working in eight steps.

If you are interested in the swap engine, have a look at Complete Bejeweled game in less than 2KB, Complete Bejeweled prototype made with jQuery and Complete Bejeweled game in less than 2KB – legible version.

Before we start coding, let me remember you the code you are going to see is not optimized because every step was written to perform a task – like “place the gems” or “select a gem” – with only that task in mind and not with an overall view of the gameplay. We lost some optimization but on the other hand the result is more understandable for readers because every step adds a feature from scratch.

That said, let’s start:

1 – PLACING GEMS ON THE STAGE

Before we can drag rows and columns, we have to place some gems on the stage. This code creates a 6×6 grid of gems, and each gem is a Tile object which is basically a MovieClip with 6 frames, one for each color:

package {
	import flash.display.Sprite;
	public class Main extends Sprite {
		private var tile:Tile;
		private var tileSize:Number=80
		public function Main() {
			for (var i:Number=0; i<6; i++) {
				for (var j:Number=0; j<6; j++) {
					tile=new Tile();
					tile.x=j*tileSize+tileSize/2;
					tile.y=i*tileSize+tileSize/2;
					addChild(tile);
					tile.gotoAndStop(Math.ceil(Math.random()*6));
				}
			}
		}
	}
}

And this is the result:

We have some random tiles placed on the stage, now we must be able to drag them

2 – SELECTING AND DRAGGING A TILE

Adding some mouse event will allows us to select, drag and place a tile around the stage. In this early version we will be able to freely move any tile anywhere, and Display Objects positions in the Display List will make upper left tiles “disappear” under lower right tiles. This is not a problem because you won’t be able to move freely a tile around the stage, anyway at the moment just focus on picking, moving and releasing a tile.

This is the script with new lines highlighted, notice how I am using a Stage mouse listener and detect which tile I clicked using math rather than creating one listener for each tile.

package {
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	public class Main extends Sprite {
		private var tile:Tile;
		private var tileSize:Number=80;
		private var tileArray:Array;
		private var movingTile:Tile;
		public function Main() {
			tileArray=new Array();
			for (var i:Number=0; i<6; i++) {
				tileArray[i]=new Array();
				for (var j:Number=0; j<6; j++) {
					tile=new Tile();
					tile.x=j*tileSize+tileSize/2;
					tile.y=i*tileSize+tileSize/2;
					addChild(tile);
					tile.gotoAndStop(Math.ceil(Math.random()*6));
					tileArray[i][j]=tile;
				}
			}
			stage.addEventListener(MouseEvent.MOUSE_DOWN,startDragging);
		}
		private function startDragging(e:MouseEvent):void {
			movingTile=tileArray[Math.floor(mouseY/tileSize)][Math.floor(mouseX/tileSize)]
			stage.removeEventListener(MouseEvent.MOUSE_DOWN,startDragging);
			stage.addEventListener(MouseEvent.MOUSE_MOVE,dragging);
			stage.addEventListener(MouseEvent.MOUSE_UP,stopDragging);
		}
		private function dragging(e:MouseEvent):void {
			movingTile.x=mouseX;
			movingTile.y=mouseY;
		}
		private function stopDragging(e:MouseEvent):void {
			stage.removeEventListener(MouseEvent.MOUSE_MOVE,dragging);
			stage.removeEventListener(MouseEvent.MOUSE_UP,stopDragging);
			stage.addEventListener(MouseEvent.MOUSE_DOWN,startDragging);
		}
	}
}

Here it is the result, now you can drag and drop tiles, with all issues described before:

Now we have to limit tile movements.

3 – MOVING ONLY HORIZONTALLY OR VERTICALLY

To move tiles only horizontally and vertically, we have to figure out if the player wants to move the tiles horizontally or vertically and lock the movement in the right direction, no matter how the player moves the mouse.

Do do this, we are analyzing the first 5 pixels of movement (it could be any number, but I found five pixels is good enough), and with the help of trigonometry we will be able to say if we are moving horizontally or vertically. From now on, this will be the only movement allowed.

package {
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	public class Main extends Sprite {
		private var tile:Tile;
		private var tileSize:Number=80;
		private var tileArray:Array;
		private var movingTile:Tile;
		private var startX:Number;
		private var startY:Number;
		private var dragDirection:String="";
		public function Main() {
			tileArray=new Array();
			for (var i:Number=0; i<6; i++) {
				tileArray[i]=new Array();
				for (var j:Number=0; j<6; j++) {
					tile=new Tile();
					tile.x=j*tileSize+tileSize/2;
					tile.y=i*tileSize+tileSize/2;
					addChild(tile);
					tile.gotoAndStop(Math.ceil(Math.random()*6));
					tileArray[i][j]=tile;
				}
			}
			stage.addEventListener(MouseEvent.MOUSE_DOWN,startDragging);
		}
		private function startDragging(e:MouseEvent):void {
			movingTile=tileArray[Math.floor(mouseY/tileSize)][Math.floor(mouseX/tileSize)];
			startX=mouseX;
			startY=mouseY;
			stage.removeEventListener(MouseEvent.MOUSE_DOWN,startDragging);
			stage.addEventListener(MouseEvent.MOUSE_MOVE,dragging);
			stage.addEventListener(MouseEvent.MOUSE_UP,stopDragging);
		}
		private function dragging(e:MouseEvent):void {
			switch (dragDirection) {
				case "" :
					var distX:Number=mouseX-startX;
					var distY:Number=mouseY-startY;
					var dist:Number=distX*distX+distY*distY;
					if (dist>25) {
						var dragAngle=Math.abs(Math.atan2(distY,distX));
						if ((dragAngle>Math.PI/4 && dragAngle<3*Math.PI/4)) {
							dragDirection="vertical";
						}
						else {
							dragDirection="horizontal";
						}
					}
					break;
				case "horizontal" :
					movingTile.x=mouseX;
					break;
				case "vertical" :
					movingTile.y=mouseY;
					break;
			}
		}
		private function stopDragging(e:MouseEvent):void {
			dragDirection="";
			stage.removeEventListener(MouseEvent.MOUSE_MOVE,dragging);
			stage.removeEventListener(MouseEvent.MOUSE_UP,stopDragging);
			stage.addEventListener(MouseEvent.MOUSE_DOWN,startDragging);
		}
	}
}

Here is the result:

Try to pick up and drag tiles to see what happens. But dragging one tile is not enough

4 – MOVING AN ENTIRE ROW/COLUMN

The game will allow us to move an entire row or column so the first thing we have to do after we know if we are moving horizontally or vertically, is moving the selected tile and all other tiles in the same row or column.

Basically is the same script you saw in the previous step, it’s just you are moving six tiles rather than only one.

package {
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	public class Main extends Sprite {
		private var tile:Tile;
		private var tileSize:Number=80;
		private var tileArray:Array;
		private var movingTile:Tile;
		private var startX:Number;
		private var startY:Number;
		private var dragDirection:String="";
		public function Main() {
			tileArray=new Array();
			for (var i:Number=0; i<6; i++) {
				tileArray[i]=new Array();
				for (var j:Number=0; j<6; j++) {
					tile=new Tile();
					tile.x=j*tileSize+tileSize/2;
					tile.y=i*tileSize+tileSize/2;
					addChild(tile);
					tile.gotoAndStop(Math.ceil(Math.random()*6));
					tileArray[i][j]=tile;
				}
			}
			stage.addEventListener(MouseEvent.MOUSE_DOWN,startDragging);
		}
		private function startDragging(e:MouseEvent):void {
			movingTile=tileArray[Math.floor(mouseY/tileSize)][Math.floor(mouseX/tileSize)];
			startX=mouseX;
			startY=mouseY;
			stage.removeEventListener(MouseEvent.MOUSE_DOWN,startDragging);
			stage.addEventListener(MouseEvent.MOUSE_MOVE,dragging);
			stage.addEventListener(MouseEvent.MOUSE_UP,stopDragging);
		}
		private function dragging(e:MouseEvent):void {
			switch (dragDirection) {
				case "" :
					var distX:Number=mouseX-startX;
					var distY:Number=mouseY-startY;
					var dist:Number=distX*distX+distY*distY;
					if (dist>25) {
						var dragAngle=Math.abs(Math.atan2(distY,distX));
						if ((dragAngle>Math.PI/4 && dragAngle<3*Math.PI/4)) {
							dragDirection="vertical";
						}
						else {
							dragDirection="horizontal";
						}
					}
					break;
				case "horizontal" :
					for(var i:Number=0;i<6;i++){
						tileArray[Math.floor(startY/tileSize)][i].x=i*tileSize+tileSize/2+mouseX-startX;
					}
					break;
				case "vertical" :
					for(i=0;i<6;i++){
						tileArray[i][Math.floor(startX/tileSize)].y=i*tileSize+tileSize/2+mouseY-startY;
					}
					break;
			}
		}
		private function stopDragging(e:MouseEvent):void {
			dragDirection="";
			stage.removeEventListener(MouseEvent.MOUSE_MOVE,dragging);
			stage.removeEventListener(MouseEvent.MOUSE_UP,stopDragging);
			stage.addEventListener(MouseEvent.MOUSE_DOWN,startDragging);
		}
	}
}

And here it is the result:

Now you can move an entire row or column, but if you move the mouse too far, tiles will disappear off the stage rather than appearing on the other side.

5 – MAKING TILES WRAP AROUND THE STAGE

Tiles leaving the stage to the left must appear on the right, tiles leaving the stage to the bottom must appear on the top, and so on. It’s not hard: we just use a modulo operator to make tiles appear on the other side at the stage.

package {
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	public class Main extends Sprite {
		private var tile:Tile;
		private var tileSize:Number=80;
		private var tileArray:Array;
		private var movingTile:Tile;
		private var startX:Number;
		private var startY:Number;
		private var dragDirection:String="";
		public function Main() {
			tileArray=new Array();
			for (var i:Number=0; i<6; i++) {
				tileArray[i]=new Array();
				for (var j:Number=0; j<6; j++) {
					tile=new Tile();
					tile.x=j*tileSize+tileSize/2;
					tile.y=i*tileSize+tileSize/2;
					addChild(tile);
					tile.gotoAndStop(Math.ceil(Math.random()*6));
					tileArray[i][j]=tile;
				}
			}
			stage.addEventListener(MouseEvent.MOUSE_DOWN,startDragging);
		}
		private function startDragging(e:MouseEvent):void {
			movingTile=tileArray[Math.floor(mouseY/tileSize)][Math.floor(mouseX/tileSize)];
			startX=mouseX;
			startY=mouseY;
			stage.removeEventListener(MouseEvent.MOUSE_DOWN,startDragging);
			stage.addEventListener(MouseEvent.MOUSE_MOVE,dragging);
			stage.addEventListener(MouseEvent.MOUSE_UP,stopDragging);
		}
		private function dragging(e:MouseEvent):void {
			switch (dragDirection) {
				case "" :
					var distX:Number=mouseX-startX;
					var distY:Number=mouseY-startY;
					var dist:Number=distX*distX+distY*distY;
					if (dist>25) {
						var dragAngle=Math.abs(Math.atan2(distY,distX));
						if ((dragAngle>Math.PI/4 && dragAngle<3*Math.PI/4)) {
							dragDirection="vertical";
						}
						else {
							dragDirection="horizontal";
						}
					}
					break;
				case "horizontal" :
					for (var i:Number=0; i<6; i++) {
						tileArray[Math.floor(startY/tileSize)][i].x=(i*tileSize+tileSize/2+mouseX-startX)%(tileSize*6);
						if (tileArray[Math.floor(startY/tileSize)][i].x<0) {
							tileArray[Math.floor(startY/tileSize)][i].x+=tileSize*6;
						}
					}

					break;
				case "vertical" :
					for (i=0; i<6; i++) {
						tileArray[i][Math.floor(startX/tileSize)].y=(i*tileSize+tileSize/2+mouseY-startY)%(tileSize*6);
						if (tileArray[i][Math.floor(startX/tileSize)].y<0) {
							tileArray[i][Math.floor(startX/tileSize)].y+=tileSize*6;
						}
					}
					break;
			}
		}
		private function stopDragging(e:MouseEvent):void {
			dragDirection="";
			stage.removeEventListener(MouseEvent.MOUSE_MOVE,dragging);
			stage.removeEventListener(MouseEvent.MOUSE_UP,stopDragging);
			stage.addEventListener(MouseEvent.MOUSE_DOWN,startDragging);
		}
	}
}

Try it by yourself:

Now your tiles do not disappear anymore and wrap nicely around the stage. Everything would be perfect, but that white space is annoying.

6 – DEALING WITH TILES ABOUT TO WRAP

When a tile is about to wrap, to enhance the gameplay you should be able to see it both on the side it is disappearing and in the side it is appearing. We will need a temporary Tile object to create a fake tile to be conveniently placed over blank spaces.

package {
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	public class Main extends Sprite {
		private var tile:Tile;
		private var tileSize:Number=80;
		private var tileArray:Array;
		private var movingTile:Tile;
		private var startX:Number;
		private var startY:Number;
		private var dragDirection:String="";
		private var tempTile:Tile;
		public function Main() {
			tileArray=new Array();
			for (var i:Number=0; i<6; i++) {
				tileArray[i]=new Array();
				for (var j:Number=0; j<6; j++) {
					tile=new Tile();
					tile.x=j*tileSize+tileSize/2;
					tile.y=i*tileSize+tileSize/2;
					addChild(tile);
					tile.gotoAndStop(Math.ceil(Math.random()*6));
					tileArray[i][j]=tile;
				}
			}
			stage.addEventListener(MouseEvent.MOUSE_DOWN,startDragging);
		}
		private function startDragging(e:MouseEvent):void {
			movingTile=tileArray[Math.floor(mouseY/tileSize)][Math.floor(mouseX/tileSize)];
			startX=mouseX;
			startY=mouseY;
			stage.removeEventListener(MouseEvent.MOUSE_DOWN,startDragging);
			stage.addEventListener(MouseEvent.MOUSE_MOVE,dragging);
			stage.addEventListener(MouseEvent.MOUSE_UP,stopDragging);
		}
		private function dragging(e:MouseEvent):void {
			switch (dragDirection) {
				case "" :
					var distX:Number=mouseX-startX;
					var distY:Number=mouseY-startY;
					var dist:Number=distX*distX+distY*distY;
					if (dist>25) {
						tempTile=new Tile();
						addChild(tempTile);
						tempTile.visible=false;
						var dragAngle=Math.abs(Math.atan2(distY,distX));
						if ((dragAngle>Math.PI/4 && dragAngle<3*Math.PI/4)) {
							dragDirection="vertical";
						}
						else {
							dragDirection="horizontal";
						}
					}

					break;
				case "horizontal" :
					tempTile.visible=true;
					tempTile.y=Math.floor(startY/tileSize)*tileSize+tileSize/2;
					for (var i:Number=0; i<6; i++) {
						tileArray[Math.floor(startY/tileSize)][i].x=(i*tileSize+tileSize/2+mouseX-startX)%(tileSize*6);
						if (tileArray[Math.floor(startY/tileSize)][i].x<0) {
							tileArray[Math.floor(startY/tileSize)][i].x+=tileSize*6;
						}
						if (Math.abs(Math.floor((mouseX-startX)/(tileSize/2)))%2) {
							if (mouseX-startX>=0) {
								tempTile.x=6*tileSize+(mouseX-startX)%(tileSize/2);
							}
							else {
								if ((mouseX-startX)%(tileSize/2)==0) {
									tempTile.x=6*tileSize;
								}
								else {
									tempTile.x=6*tileSize+tileSize/2+(mouseX-startX)%(tileSize/2);
								}
							}
						}
						else {
							if (mouseX-startX>=0) {
								tempTile.x=(mouseX-startX)%(tileSize/2)-tileSize/2;
							}
							else {
								tempTile.x=(mouseX-startX)%(tileSize/2);
							}
						}
					}
					break;
				case "vertical" :
					tempTile.visible=true;
					tempTile.x=Math.floor(startX/tileSize)*tileSize+tileSize/2;
					for (i=0; i<6; i++) {
						tileArray[i][Math.floor(startX/tileSize)].y=(i*tileSize+tileSize/2+mouseY-startY)%(tileSize*6);
						if (tileArray[i][Math.floor(startX/tileSize)].y<0) {
							tileArray[i][Math.floor(startX/tileSize)].y+=tileSize*6;
						}
						if (Math.abs(Math.floor((mouseY-startY)/(tileSize/2)))%2) {
							if (mouseY-startY>=0) {
								tempTile.y=6*tileSize+(mouseY-startY)%(tileSize/2);
							}
							else {
								tempTile.y=6*tileSize+tileSize/2+(mouseY-startY)%(tileSize/2);
							}
						}
						else {
							if (mouseY-startY>0) {
								tempTile.y=(mouseY-startY)%(tileSize/2)-tileSize/2;
							}
							else {
								if ((mouseY-startY)%(tileSize/2)==0) {
									tempTile.y=6*tileSize+tileSize/2;
								}
								else {
									tempTile.y=(mouseY-startY)%(tileSize/2);
								}
							}
						}
					}
					break;
			}
		}
		private function stopDragging(e:MouseEvent):void {
			removeChild(tempTile);
			dragDirection="";
			stage.removeEventListener(MouseEvent.MOUSE_MOVE,dragging);
			stage.removeEventListener(MouseEvent.MOUSE_UP,stopDragging);
			stage.addEventListener(MouseEvent.MOUSE_DOWN,startDragging);
		}
	}
}

Here is the result:

Draw a row or a column and watch the flashing tile covering the white space. Now we just have to assign it a color.

7 – DEALING WITH TILES ABOUT TO WRAP, ONCE FOR ALL

To complete the wrap process, we have to give the temporary tile the same color of the tile which is about to swap. The way we do it changes a bit according to the direction of the tile we are going to swap, but it easily manageable:

package {
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	public class Main extends Sprite {
		private var tile:Tile;
		private var tileSize:Number=80;
		private var halfTile:Number=tileSize/2;
		private var tileArray:Array;
		private var movingTile:Tile;
		private var startX:Number;
		private var startY:Number;
		private var dragDirection:String="";
		private var tempTile:Tile;
		public function Main() {
			tileArray=new Array();
			for (var i:Number=0; i<6; i++) {
				tileArray[i]=new Array();
				for (var j:Number=0; j<6; j++) {
					tile=new Tile();
					tile.x=j*tileSize+halfTile;
					tile.y=i*tileSize+halfTile;
					addChild(tile);
					tile.val=Math.ceil(Math.random()*6);
					tile.gotoAndStop(tile.val);
					tileArray[i][j]=tile;
				}
			}
			stage.addEventListener(MouseEvent.MOUSE_DOWN,startDragging);
		}
		private function startDragging(e:MouseEvent):void {
			movingTile=tileArray[Math.floor(mouseY/tileSize)][Math.floor(mouseX/tileSize)];
			startX=mouseX;
			startY=mouseY;
			stage.removeEventListener(MouseEvent.MOUSE_DOWN,startDragging);
			stage.addEventListener(MouseEvent.MOUSE_MOVE,dragging);
			stage.addEventListener(MouseEvent.MOUSE_UP,stopDragging);
		}
		private function dragging(e:MouseEvent):void {
			var distX:Number=mouseX-startX;
			var distY:Number=mouseY-startY;
			switch (dragDirection) {
				case "" :
					var dist:Number=distX*distX+distY*distY;
					if (dist>25) {
						tempTile=new Tile();
						addChild(tempTile);
						tempTile.visible=false;
						var dragAngle=Math.abs(Math.atan2(distY,distX));
						if ((dragAngle>Math.PI/4 && dragAngle<3*Math.PI/4)) {
							dragDirection="vertical";
						}
						else {
							dragDirection="horizontal";
						}
					}

					break;
				case "horizontal" :
					var deltaX=(Math.floor(distX/tileSize)%6);
					if (deltaX>=0) {
						tempTile.gotoAndStop(tileArray[Math.floor(startY/tileSize)][5-deltaX].val);
					}
					else {
						deltaX=deltaX*-1-1;
						tempTile.gotoAndStop(tileArray[Math.floor(startY/tileSize)][deltaX].val);
					}
					tempTile.visible=true;
					tempTile.y=Math.floor(startY/tileSize)*tileSize+halfTile;
					for (var i:Number=0; i<6; i++) {
						tileArray[Math.floor(startY/tileSize)][i].x=(i*tileSize+halfTile+distX)%(tileSize*6);
						if (tileArray[Math.floor(startY/tileSize)][i].x<0) {
							tileArray[Math.floor(startY/tileSize)][i].x+=tileSize*6;
						}
						if (Math.abs(Math.floor(distX/halfTile))%2) {
							if (distX>=0) {
								tempTile.x=6*tileSize+distX%halfTile;
							}
							else {
								if (distX%halfTile==0) {
									tempTile.x=6*tileSize;
								}
								else {
									tempTile.x=6*tileSize+halfTile+distX%halfTile;
								}
							}
						}
						else {
							if (distX>=0) {
								tempTile.x=distX%halfTile-halfTile;
							}
							else {
								if (distX%halfTile==0) {
									tempTile.x=6*tileSize+halfTile;
								}
								else {
									tempTile.x=distX%halfTile;
								}
							}
						}
					}
					break;
				case "vertical" :
					var deltaY=(Math.floor(distY/tileSize)%6);
					if (deltaY>=0) {
						tempTile.gotoAndStop(tileArray[5-deltaY][Math.floor(startX/tileSize)].val);
					}
					else {
						deltaY=deltaY*-1-1;
						tempTile.gotoAndStop(tileArray[deltaY][Math.floor(startX/tileSize)].val);
					}
					tempTile.visible=true;
					tempTile.x=Math.floor(startX/tileSize)*tileSize+halfTile;
					for (i=0; i<6; i++) {
						tileArray[i][Math.floor(startX/tileSize)].y=(i*tileSize+halfTile+distY)%(tileSize*6);
						if (tileArray[i][Math.floor(startX/tileSize)].y<0) {
							tileArray[i][Math.floor(startX/tileSize)].y+=tileSize*6;
						}
						if (Math.abs(Math.floor(distY/halfTile))%2) {
							if (distY>=0) {
								tempTile.y=6*tileSize+distY%halfTile;
							}
							else {
								if (distY%halfTile==0) {
									tempTile.y=6*tileSize
								}
								else {
									tempTile.y=6*tileSize+halfTile+distY%halfTile;
								}
							}
						}
						else {
							if (distY>=0) {
									tempTile.y=distY%halfTile-halfTile;
							}
							else {
								if (distY%halfTile==0) {
									tempTile.y=6*tileSize+halfTile;
								}
								else {
									tempTile.y=distY%halfTile;
								}
							}
						}
					}
					break;
			}
		}
		private function stopDragging(e:MouseEvent):void {
			removeChild(tempTile);
			dragDirection="";
			stage.removeEventListener(MouseEvent.MOUSE_MOVE,dragging);
			stage.removeEventListener(MouseEvent.MOUSE_UP,stopDragging);
			stage.addEventListener(MouseEvent.MOUSE_DOWN,startDragging);
		}
	}
}

Try it by yourself:

Now tiles wrap around the stage with no errors. Now we only have to place them down when the player releases the mouse

8 – UPDATING THE STAGE WHEN THE PLAYER STOPS DRAGGING

There are two things to do: first, we have to snap tiles to the grid, because we don’t want the player to place tiles outside the grid, then we have to update the array of tiles, which is the logical representation of the game field:

package {
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	public class Main extends Sprite {
		private var tile:Tile;
		private var tileSize:Number=80;
		private var halfTile:Number=tileSize/2;
		private var tileArray:Array;
		private var movingTile:Tile;
		private var startX:Number;
		private var startY:Number;
		private var dragDirection:String="";
		private var tempTile:Tile;
		public function Main() {
			tileArray=new Array();
			for (var i:Number=0; i<6; i++) {
				tileArray[i]=new Array();
				for (var j:Number=0; j<6; j++) {
					tile=new Tile();
					tile.x=j*tileSize+halfTile;
					tile.y=i*tileSize+halfTile;
					addChild(tile);
					tile.val=Math.ceil(Math.random()*6);
					tile.gotoAndStop(tile.val);
					tileArray[i][j]=tile;
				}
			}
			stage.addEventListener(MouseEvent.MOUSE_DOWN,startDragging);
		}
		private function startDragging(e:MouseEvent):void {
			movingTile=tileArray[Math.floor(mouseY/tileSize)][Math.floor(mouseX/tileSize)];
			startX=mouseX;
			startY=mouseY;
			stage.removeEventListener(MouseEvent.MOUSE_DOWN,startDragging);
			stage.addEventListener(MouseEvent.MOUSE_MOVE,dragging);
			stage.addEventListener(MouseEvent.MOUSE_UP,stopDragging);
		}
		private function dragging(e:MouseEvent):void {
			var distX:Number=mouseX-startX;
			var distY:Number=mouseY-startY;
			switch (dragDirection) {
				case "" :
					var dist:Number=distX*distX+distY*distY;
					if (dist>25) {
						tempTile=new Tile();
						addChild(tempTile);
						tempTile.visible=false;
						var dragAngle=Math.abs(Math.atan2(distY,distX));
						if ((dragAngle>Math.PI/4 && dragAngle<3*Math.PI/4)) {
							dragDirection="vertical";
						}
						else {
							dragDirection="horizontal";
						}
					}

					break;
				case "horizontal" :
					var deltaX=(Math.floor(distX/tileSize)%6);
					if (deltaX>=0) {
						tempTile.gotoAndStop(tileArray[Math.floor(startY/tileSize)][5-deltaX].val);
					}
					else {
						deltaX=deltaX*-1-1;
						tempTile.gotoAndStop(tileArray[Math.floor(startY/tileSize)][deltaX].val);
					}
					tempTile.visible=true;
					tempTile.y=Math.floor(startY/tileSize)*tileSize+halfTile;
					for (var i:Number=0; i<6; i++) {
						tileArray[Math.floor(startY/tileSize)][i].x=(i*tileSize+halfTile+distX)%(tileSize*6);
						if (tileArray[Math.floor(startY/tileSize)][i].x<0) {
							tileArray[Math.floor(startY/tileSize)][i].x+=tileSize*6;
						}
						if (Math.abs(Math.floor(distX/halfTile))%2) {
							if (distX>=0) {
								tempTile.x=6*tileSize+distX%halfTile;
							}
							else {
								if (distX%halfTile==0) {
									tempTile.x=6*tileSize;
								}
								else {
									tempTile.x=6*tileSize+halfTile+distX%halfTile;
								}
							}
						}
						else {
							if (distX>=0) {
								tempTile.x=distX%halfTile-halfTile;
							}
							else {
								if (distX%halfTile==0) {
									tempTile.x=6*tileSize+halfTile;
								}
								else {
									tempTile.x=distX%halfTile;
								}
							}
						}
					}
					break;
				case "vertical" :
					var deltaY=(Math.floor(distY/tileSize)%6);
					if (deltaY>=0) {
						tempTile.gotoAndStop(tileArray[5-deltaY][Math.floor(startX/tileSize)].val);
					}
					else {
						deltaY=deltaY*-1-1;
						tempTile.gotoAndStop(tileArray[deltaY][Math.floor(startX/tileSize)].val);
					}
					tempTile.visible=true;
					tempTile.x=Math.floor(startX/tileSize)*tileSize+halfTile;
					for (i=0; i<6; i++) {
						tileArray[i][Math.floor(startX/tileSize)].y=(i*tileSize+halfTile+distY)%(tileSize*6);
						if (tileArray[i][Math.floor(startX/tileSize)].y<0) {
							tileArray[i][Math.floor(startX/tileSize)].y+=tileSize*6;
						}
						if (Math.abs(Math.floor(distY/halfTile))%2) {
							if (distY>=0) {
								tempTile.y=6*tileSize+distY%halfTile;
							}
							else {
								if (distY%halfTile==0) {
									tempTile.y=6*tileSize;
								}
								else {
									tempTile.y=6*tileSize+halfTile+distY%halfTile;
								}
							}
						}
						else {
							if (distY>=0) {
								tempTile.y=distY%halfTile-halfTile;
							}
							else {
								if (distY%halfTile==0) {
									tempTile.y=6*tileSize+halfTile;
								}
								else {
									tempTile.y=distY%halfTile;
								}
							}
						}
					}
					break;
			}
		}
		private function stopDragging(e:MouseEvent):void {
			var tempArray:Array=new Array();
			if (dragDirection=="horizontal") {
				var distX:Number=mouseX-startX;
				var deltaX:Number=(Math.round(distX/tileSize));
				removeChild(tempTile);
				if (distX>=0) {
					for (var i:Number=0; i<6; i++) {
						tempArray[(deltaX+i)%6]=tileArray[Math.floor(startY/tileSize)][i].val;
					}
				}
				else {
					deltaX*=-1;
					for (i=0; i<6; i++) {
						tempArray[i]=tileArray[Math.floor(startY/tileSize)][(deltaX+i)%6].val;
					}
				}
				for (i=0; i<6; i++) {
					tileArray[Math.floor(startY/tileSize)][i].val=tempArray[i];
					tileArray[Math.floor((startY/tileSize))][i].gotoAndStop(tempArray[i]);
					tileArray[Math.floor(startY/tileSize)][i].x=i*tileSize+halfTile;
				}
			}
			else {
				var distY:Number=mouseY-startY;
				var deltaY:Number=(Math.round(distY/tileSize));
				removeChild(tempTile);
				if (distY>=0) {
					for (i=0; i<6; i++) {
						tempArray[(deltaY+i)%6]=tileArray[i][Math.floor(startX/tileSize)].val;
					}
				}
				else {
					deltaY*=-1;
					for (i=0; i<6; i++) {
						tempArray[i]=tileArray[(deltaY+i)%6][Math.floor(startX/tileSize)].val;
					}
				}
				for (i=0; i<6; i++) {
					tileArray[i][Math.floor(startX/tileSize)].val=tempArray[i];
					tileArray[i][Math.floor((startX/tileSize))].gotoAndStop(tempArray[i]);
					tileArray[i][Math.floor(startX/tileSize)].y=i*tileSize+halfTile;
				}
			}
			dragDirection="";
			stage.removeEventListener(MouseEvent.MOUSE_MOVE,dragging);
			stage.removeEventListener(MouseEvent.MOUSE_UP,stopDragging);
			stage.addEventListener(MouseEvent.MOUSE_DOWN,startDragging);
		}
	}
}

And finally we have our working engine, have a try:

Drag rows and columns as you want. Now you can create your game logic to handle tiles once they stop. You can make them disappear or fall, if you want me to explain some of these features, just leave a comment.

Download the source code.

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

215 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
// Stairs
// 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