In this tutorial, we are going to create a sliding tile puzzle game component using actionscript. Sliding puzzle games work by spreading an image accross a number of tiles then jumbling the tiles up, so you have to re-assemble the image by moving the tiles one at a time. There is always one empty space, and you can only move a tile into a space that is in direct contact with it.
The component will accept any image, and generate a puzzle from a few simple settings. This tutorial is as much a learning exercise for me as for you dear reader, so please feel free to show me corrections and places where I could've done better.
Requirements:
- Flash 8 or later.
- Some actionscript knowledge (well, this is a programming site!).
- If you don't like the image I'm providing, you'll need your own.
- An actionscript editor is optional, but I like to code in SE|PY, you might like it too.
Getting Started
During this tutorial we will create four actionscript classes. I like to keep my classes seperate from my other source files, so in your working directory, create a new folder and call it "com". Now we're ready to begin.
The code
As I've already mentioned, this component is comprised of four classes. These are:
- eventBroadcaster.as - The event broadcaster is a very simple class that allows us to access the Flash event model easily.
- bitmapGenerator.as - The bitmap generator uses the bitmapData object to split your image into several pieces that can then be attached to the tiles.
- slidingPuzzleTile.as - Each tile is an instance of this class. Properties such as ID and initial positions are stored, and there is movement method. This class also extends the event broadcaster.
- slidingPuzzleMain.as - This class extends the event broadcaster, and governs the main game. It creates the tiles, and handles the logic required to move the tiles.
So, lets get down to it. We'll start with the easy one.
eventBroadcaster
as
import mx.events.EventDispatcher;
class com.eventBroadcaster extends MovieClip {
public var addEventListener : Function;
public var removeEventListener : Function;
public var dispatchEvent : Function;
public function eventBroadcaster() {
EventDispatcher.initialize( this );
}
}
As you can see, this is a very simple class that sets up the event related functions we are going to use later and initializes the EventDispatcher. Save the class in your com folder as eventBroadcaster.as.
bitmapGenerator
as
/*
* Imports
*/
import flash.display.BitmapData;
import flash.geom.Rectangle;
import flash.geom.Point;
class com.bitmapGenerator {
/*
* Properties
*/
private var mainImage :BitmapData;
private var tileW :Number;
private var tileH :Number;
private var bitmapArray :Array;
/*
* Constructor
*/
function bitmapGenerator() {
//empty
}
/*
* Split the source image into a grid of bitmapData objects and return as 2D array
*/
public function createTiles(clipRef:String,tileX:Number,tileY:Number):Array {
mainImage = BitmapData.loadBitmap(clipRef);
tileW = mainImage.width / tileX;
tileH = mainImage.height / tileY;
bitmapArray = new Array();
for (var i:Number = 0; i < tileX; i++) {
bitmapArray = new Array();
for (var n:Number = 0; n < tileY; n++) {
var tempData:BitmapData = new BitmapData(tileW,tileH);
var tempRect = new Rectangle((tileW * i),(tileH * n),tileW,tileH);
tempData.copyPixels(mainImage,tempRect,new Point(0,0));
bitmapArray[i][n] = tempData;
}
}
return bitmapArray;
}
}
The bitmapGenerator is quite a small class with only one method, createTiles, which receives three properties. These are the linkage identifier of your image in the library as a string, and the number of tiles you want to split the image into along the x and y axis.
The first things that happen are a bitmapData object called mainImage is created and populated with your image from the library, and the dimensions of mainImage are then used with the required number of tiles to determine each tiles dimensions. An array is created, which is populated by more arrays in the outer for loop to create a two dimensional array.
A temporary bitmapData object is created, using the tile dimensions. Next, a rectangle is created to the dimensions of one tile, and used as a marker for pixel data to be copied from mainImage to our temporary bitmap object. The temporary bitmap Data is inserted into the two dimensional array and the loop iterates. When the array is fully populated it is returned.
Save this file in the com folder as bitmapGenerator.as.
slidingPuzzleTile
as
/*
* Imports
*/
import com.eventBroadcaster;
import flash.display.BitmapData;
import mx.utils.Delegate;
import mx.transitions.Tween;
class com.slidingPuzzleTile extends eventBroadcaster {
/*
* Properties
*/
// reference for click event
public static var TILECLICK = "onTileClicked";
//unique tile id
public var tileID :Number;
//tile dimensions
private var tileW :Number;
private var tileH :Number;
//Initial x and y positions
public var xIndex :Number;
public var yIndex :Number;
//tile movieclip
public var targMC :MovieClip;
/*
* Constructor
*/
function slidingPuzzleTile(mc:MovieClip,bmp:BitmapData,id:Number,xI:Number,yI:Number,bC:N
umber,bW:Number,bA:Number) {
tileID = id;
tileW = bmp.width;
tileH = bmp.height;
xIndex = xI;
yIndex = yI;
targMC = mc;
init(bmp,bC,bW,bA);
}
/*
* Attach bitmapData to tile moveclip, create a border and set the onRelease listener
*/
private function init(bmp:BitmapData,bC:Number,bW:Number,bA:Number):Void {
targMC.attachBitmap(bmp,1);
if (bW > 0) {
var tileBorder:MovieClip = targMC.createEmptyMovieClip("tileHighlight",2);
var innerLine:Number = Math.floor(bW/2);
tileBorder.lineStyle(bW,bC,bA);
tileBorder.moveTo(innerLine,innerLine);
tileBorder.lineTo(tileW-innerLine,innerLine);
tileBorder.lineTo(tileW-innerLine,tileH-innerLine);
tileBorder.lineTo(innerLine,tileH-innerLine);
tileBorder.lineTo(innerLine,innerLine);
}
targMC.onRelease = Delegate.create(this, tileClick);
}
/*
* Dispatch event on tile click
*/
public function tileClick():Void {
dispatchEvent({ type:TILECLICK, target:this, tile:tileID });
}
/*
* Recieve a direction and call the animation function
*/
public function moveTile(dir:String):Void {
switch (dir) {
case "up":
doTween("_y",targMC._y,targMC._y-tileH);
break;
case "down":
doTween("_y",targMC._y,targMC._y+tileH);
break;
case "left":
doTween("_x",targMC._x,targMC._x-tileW);
break;
case "right":
doTween("_x",targMC._x,targMC._x+tileW);
break;
}
}
/*
* Move the tile
*/
private function doTween(dir:String,curr:Number,dist:Number):Void {
var myTween:Tween = new Tween(targMC, dir, mx.transitions.easing.Strong.easeOut, curr, dist, .4, true);
}
}
The slidingPuzzleTile class is instantiated once for every tile in the puzzle. Lets look first at its properties. There is a reference for the tile clicked event, which will be dispatched to slidingPuzzleMain. The other properties are a unique ID, the tiles dimensions, it's initial array coordinates and a reference to its movieclip. The initial array coordinates are used when checking if the puzzle has been completed.
There are several arguments received by the constructor. These are: a reference to the tiles movieclip, the bitmapData for the tile to display, the tiles ID, it's coordinates in the tiles array stored in slidingPuzzleMain, the tile's border color, its border width and its border alpha. The movieclip reference, array position and ID are stored, and the tile dimensions are found from the bitmapData object, then the bitmap object is passed to the init method, along with the border properties.
In the init method the bitmapData is first attached to the tile movieclip, then the requested border width is checked. If the requested width is larger than zero, an empty movieclip is attached to the tile and the border is drawn around the edge. The border is inset onto the tile to prevent the tiles from being pushed apart. Finally, the onRelease is set up, and delegated to a method called tileClick.
The tileClick method is very simple. Its only purpose it to dispatch the onTileClicked event back to slidingPuzzleMain.
The remaining two methods in this class handle movement, if it required. If slidingPuzzleMain decides that the tile can move it calls moveTile, passing a single string argument which specifies the movement direction. The moveTile method then calls doTween, passing to it the direction movement, and the start and end positions. An instance of the tween class is instantiated to perform the animation.
This file can now be saved in the com folder as slidingPuzzleTile.as. Now we get on to the big one...
slidingPuzzleMain
This is a much bigger class, so we'll break it down into chunks. First of all we'll start the class, look at the properties and write the constructor. Hopefully the comments are suitably descriptive.
as
/*
* Imports
*/
import com.eventBroadcaster;
import com.bitmapGenerator;
import com.slidingPuzzleTile;
class com.slidingPuzzleMain extends eventBroadcaster {
/*
* Properties
*/
// references for events
public static var TILECLICK :String = "onTileClick";
public static var COMPLETE :String = "onPuzzleComplete";
// tiles array holds references to the tile objects
public var tiles :Array;
// bitmaps array holds references to the slices of the initial image
private var bitmaps :Array;
// bitmap generator class breaks up the initial images into smaller pieces
private var bitmapGen :bitmapGenerator;
// tile properties
private var tilesX :Number;
private var tilesY :Number;
private var borderCol :Number;
private var borderWidth :Number;
private var borderAlpha :Number;
//holds the number of moves to jumble the puzzle by
private var initMoves :Number;
// holds the linkage id of the image to use
private var imageRef :String;
/*
* constructor
*/
function slidingPuzzleMain() {
prepareBitmaps();
createTiles();
}
After the imports and the class declaration we find the properties. The first two declare the two events that this class will dispatch. Next are two arrays. These will be 2 dimensional arrays (an array of arrays) that represent our image when it's cut up into tiles, like a grid or table. The first is to hold the references to each tile instance, and the second is to hold the output from the bitmap generator.
The rest of the properties are holders for values that you will set with the component or property inspector. We'll come back to the setting of those later.
Finally we have the constructor, which calls the next two methods:
as
/*
* Instantiate the bitmapGenerator to slice the image and return the seperate bitmaps
*/
private function prepareBitmaps():Void {
bitmapGen = new bitmapGenerator(this);
bitmaps = bitmapGen.createTiles(imageRef,tilesX,tilesY);
}
/*
* Create the tiles
*/
private function createTiles():Void {
var tileID:Number = 0;
tiles = new Array();
//for every tile on the x axis
for (var i:Number = 0; i < tilesX; i++) {
tiles[i] = new Array();
//for every tile on the y axis
for (var n:Number = 0; n < tilesY; n++) {
var tempMC:MovieClip = this.createEmptyMovieClip("tile"+tileID,this.getNextHighestDepth());
tiles[i][n] = new slidingPuzzleTile(tempMC,bitmaps[i][n],tileID,i,n,borderCol,borderWidth,borderAl
pha);
tiles[i][n].addEventListener("onTileClicked",this);
tempMC._x = tempMC._width * i;
tempMC._y = tempMC._height * n;
tileID++;
}
}
// remove the last tile to create the required empty square
tiles[tiles.length-1][tiles[tiles.length-1].length-1].targMC.removeMovieClip();
tiles[tiles.length-1][tiles[tiles.length-1].length-1] = "empty";
//jumble the puzzle
jumble();
}
The first of these methods instantiates the bitmap generator and calls it's createTiles method with some properties. These are the linkage identifier of your image in the library, and the number of tiles you want to split the image into along the x and y axis. the createTiles method returns a two dimensional array of bitmapData objects for us to turn into tiles, and the createTiles method is ready an waiting to do just that.
In the createTiles method there are two for loops, one inside the other. These loops populate the two dimensional tiles array with instances of the slidingPuzzleTile class by first creating a temporary movieclip reference. This reference is passed to the slidingPuzzleTile constructor, along with a bitmapData object, a unique id number, the tile's initial position in the tiles array (used later), and details of what border to create. An event listener is added to catch the onTileClicked event from the tile, and it's x and y position is set based on it's dimensions and the number of the loop iterations.
Finally, the last instance of the tiles array has it's movie clip removed, and the reference to the tile object is replaced by a string containing "empty". This is the blank square that allows us to move tiles around the puzzle. The jumble method is called to randomise the tiles so the game can begin.
as
/*
* Performs a number of moves to jumble the puzzle
*/
private function jumble():Void {
//hold possible tiles to move
var possibles:Array = new Array();
//hold location of empty tile
var emptyPos:Object = new Object();
//hold previous tile ID, to prevent back-tracking
var prevID:Number;
for (var i:Number = initMoves; i > 0; i--) {
possibles = [];
//find the empty tile
emptyPos = findEmpty();
//if surrounding tiles exist, add to possibles array
if (tiles[emptyPos.xI][emptyPos.yI - 1] && tiles[emptyPos.xI][emptyPos.yI - 1].tileID != prevID) {
possibles.push(tiles[emptyPos.xI][emptyPos.yI - 1]);
}
if (tiles[emptyPos.xI][emptyPos.yI + 1] && tiles[emptyPos.xI][emptyPos.yI + 1].tileID != prevID) {
possibles.push(tiles[emptyPos.xI][emptyPos.yI + 1]);
}
if (tiles[emptyPos.xI - 1][emptyPos.yI] && tiles[emptyPos.xI - 1][emptyPos.yI].tileID != prevID) {
possibles.push(tiles[emptyPos.xI - 1][emptyPos.yI]);
}
if (tiles[emptyPos.xI + 1][emptyPos.yI] && tiles[emptyPos.xI + 1][emptyPos.yI].tileID != prevID) {
possibles.push(tiles[emptyPos.xI + 1][emptyPos.yI]);
}
//select a random array value
var randNum:Number = Math.floor(Math.random() * possibles.length);
prevID = possibles[randNum].tileID;
possibles[randNum].tileClick();
}
}
/*
* Loop through the tiles array and return the location of
* the empty tile
*/
private function findEmpty():Object {
for (var i:Number = 0; i < tilesX; i++) {
for (var n:Number = 0; n < tilesY; n++) {
if (tiles[i][n] == "empty") {
var tempObj:Object = new Object({ xI:i, yI:n });
return tempObj;
}
}
}
}
The jumble method is quite large. The idea here is that for a set number of iterations the code finds where the currently empty space is using the findEmpty method, then it populates the possibles array with up to three surrounding tiles. This number will be less if the space is against a side of the puzzle, and the previously moved tile will always be ignored. Once the possibles array has been populated an entry is selected at random, it's ID is stored to ensure it is ignored in the next iteration, and the tile's tileClick method is called which causes it to move. How it does that is dealt with next:
as
/*
* Respond to onTileClicked event by checking for an available
* space and moving if possible. If puzzle is complete disable
* buttons and dispatch complete event.
*/
private function onTileClicked(eo:Object):Void {
dispatchEvent({ type:TILECLICK, target:this });
var space:String = canMove(findTile(eo.tile));
if (space != "no") {
eo.target.moveTile(space);
//if puzzle is complete
if(checkPositions()) {
disableButtons();
dispatchEvent({ type:COMPLETE, target:this });
}
}
}
This method is called by the event listener we set up earlier on the tiles. The event object (eo) contains the ID of tile that was clicked so it can be identified. First though, we dispatch an event to notify any outside observers that a tile has been clicked.
That done a string called [i]space is created and populated by the output of the can move method (see next code block). If the tile is able to move, it is informed of where the vacant space is in relation to itself and it moves. The positions of all the tiles are now checked to see if the puzzle is complete, and if it is the tile buttons are disabled and the puzzle complete event is dispatched.
as
/*
* Loop through the tiles array and return the array location of
* the desired tile as an object
*/
private function findTile(tI:Number):Object {
for (var i:Number = 0; i < tilesX; i++) {
for (var n:Number = 0; n < tilesY; n++) {
if (tiles[i][n].tileID == tI) {
var tempObj:Object = new Object({ xI:i, yI:n });
return tempObj;
}
}
}
}
/*
* Check for an empty space ajoining the selected tile and return the availiable
* direction, or "no"
*/
private function canMove(tIndex:Object):String {
if (tiles[tIndex.xI][tIndex.yI - 1] == "empty") {
tiles[tIndex.xI][tIndex.yI - 1] = tiles[tIndex.xI][tIndex.yI];
tiles[tIndex.xI][tIndex.yI] = "empty";
return "up";
} else if (tiles[tIndex.xI][tIndex.yI + 1] == "empty") {
tiles[tIndex.xI][tIndex.yI + 1] = tiles[tIndex.xI][tIndex.yI];
tiles[tIndex.xI][tIndex.yI] = "empty";
return "down";
} else if (tiles[tIndex.xI - 1][tIndex.yI] == "empty") {
tiles[tIndex.xI - 1][tIndex.yI] = tiles[tIndex.xI][tIndex.yI];
tiles[tIndex.xI][tIndex.yI] = "empty";
return "left";
} else if (tiles[tIndex.xI + 1][tIndex.yI] == "empty") {
tiles[tIndex.xI + 1][tIndex.yI] = tiles[tIndex.xI][tIndex.yI];
tiles[tIndex.xI][tIndex.yI] = "empty";
return "right";
}
return "no";
}
/*
* Check if the puzzle is complete
*/
private function checkPositions():Boolean {
//if the bottom right space is empty
if (tiles[tiles.length-1][tiles[tiles.length-1].length-1] == "empty") {
for (var i:Number = 0; i < tilesX; i++) {
for (var n:Number = 0; n < tilesY; n++) {
//if the tile's current x position matches it's starting point
if (tiles[i][n].xIndex != i && i != (tiles.length - 1)) {
return false;
}
//if the tile's current y position matches it's starting point
if (tiles[i][n].yIndex != n && n != (tiles[i].length - 1)) {
return false;
}
}
}
} else {
return false;
}
//all tiles are in the correct place
return true;
}
These are the three methods that provide the checks to the onTileClicked method. The first is pretty simple, it loops through the two dimenstional tiles array looking for a specific tileID. When found it returns the coordinates if the tile object in the two dimensional array as an object.
The canMove method investigates the array positions on all four sides of the selected tile. If it discovers an empty square it moves the instance of the current tiles to the empty index, and changes it's old index to be "empty". This done, it returns the direction for the tile to move in as a string. If there is no availiable empty space it returns "no".
The last method here is the checkPositions method. It loops through the tiles array, and checks each tile to see if it is occupying the array index that it started from. If all the the tiles are in their original places the puzzle is complete, and checkPositions returns true.
as
/*
* Enable all tile buttons
*/
private function enableButtons():Void {
for (var i:Number = 0; i < tilesX; i++) {
for (var n:Number = 0; n < tilesY; n++) {
tiles[i][n].targMC.enabled = true;
}
}
}
/*
* Disable all tile buttons
*/
private function disableButtons():Void {
for (var i:Number = 0; i < tilesX; i++) {
for (var n:Number = 0; n < tilesY; n++) {
tiles[i][n].targMC.enabled = false;
}
}
}
/*
* Restart puzzle
*/
public function resetPuzzle():Void {
jumble();
enableButtons();
}
The three methods above are all very simple, and the first two are almost identical. enableButtons and disableButtons do exaclty as they say; they loop through the tiles array, and enable or disable each tile respectively. Disable is used when the puzzle is complete, and enable is used in the resetPuzzle method, which jumbles the tiles again and enables the tiles ready to begin again. The resetPuzzle method will be called from outside the class, so it is public.
as
/*
* Component property tags
*/
[Inspectable(type=String)]
public function set image_linkage(ref:String):Void {
imageRef = ref;
}
[Inspectable(type=Number)]
public function set horizontal_tiles(ref:Number):Void {
tilesX = ref;
}
[Inspectable(type=Number)]
public function set vertical_tiles(ref:Number):Void {
tilesY = ref;
}
[Inspectable(type=Color)]
public function set tile_border_color(ref:Number):Void {
borderCol = ref;
}
[Inspectable(type=Number,defaultValue=1)]
public function set tile_border_width(ref:Number):Void {
borderWidth = ref;
}
[Inspectable(type=Number,defaultValue=100)]
public function set tile_border_alpha(ref:Number):Void {
borderAlpha = ref;
}
[Inspectable(type=Number)]
public function set moves_to_finish(ref:Number):Void {
initMoves = ref;
}
}
Finally, the inspectable set methods. These allow the properties to be set in the component or property inspector in flash, making the puzzle really easy to use once it's built. The string and number properties are fairly obvious, but new to me at the time of writing is the Color type. This causes a color palette to appear in the property inspector in place of a text box.
So that's it for the classes. Phew. Save your file in the com folder as slidingPuzzleMain.as, and then lets look at the flash side.
In Flash
The majority of the work is done for us in the classes above, now it's time to take it easy. In this section you are going to need an image. If you don't have one handy, use this one. It's the same one I used while testing this tutorial!
Click to view attachment
So, create a new AS2 document. The dimensions are not really important, but make sure the stage is at least as big as the image you intend to use. Save the document in your working folder (the one with the com folder in it). Import your chosen image into the library, then right click on it and select "Linkage...". Tick the "Export for Actionscript" box, and change the linkage identifier if you really want to. Click OK.
Next, create an empty movieclip, put it on the stage and give it the instance name "puzzle". This movieclip will represent the top left corner of your puzzle, so position it accordingly. Create a new layer on the timeline, and still on the same frame enter the following code:
as
stop();
import com.eventBroadCaster;
puzzle.addEventListener("onPuzzleComplete", this);
function onPuzzleComplete():Void {
nextFrame();
}
This sets up a listener for the onPuzzleComplete event that you're going to recieve, and creates a function that will move the playhead to the next frame when it fires.
Now create another column of frames on the timeline, and a new layer. On frame two, in your new layer, create a text field as big as you like with a jubilant message inside. I went for "SUCCESS!". Make this into a button by selecting it and pressing F8 (next week - egg sucking by thehat), and give it the instance name "again". Then in frame two of your actions layer insert the following:
as
stop();
again.onRelease = function() {
puzzle.resetPuzzle();
prevFrame();
}
This simply starts the puzzle again and hides your happy message.
Finally, go back to the library, right click on your empty movieclip and select "Component Definition...". In the class textbox put "com.slidingPuzzleMain" and select OK. Right click the movieclip again and Select "Linkage...", then tick the "Export for Actionscript" box and in the class textbox put "com.slidingPuzzleMain" and select OK. Now click on the empty movieclip you put on the stage, and in the properties panel select the parameters tab. You should now see the list of options to setup your puzzle game.
Click to view attachment
Go ahead and fill these in as you like, making sure you get the image_linkage correct (it's the linkage identifier of your image in the library) then CTRL+Enter and try it out!
Wow, so that's it. I hope it all works well for you, and you followed this tutorial easily enough. It's my first one, and all feedback is gratefully recieved. Thanks for reading!