Home Projects Flash CS3 How to Create a Tile Game Like Destruct-O-Match on Neopets.com





Forgot your password?
Forgot your username?
No Account Yet? Create an account

Like it? Share it!

Add to: JBookmarks Add to: Facebook Add to: Windows Live Add to: Ximmy Add to: Digg Add to: Del.icoi.us Add to: Reddit Add to: Jumptags Add to: Upchuckr Add to: StumbleUpon Add to: Slashdot Add to: Netscape Add to: Yahoo Add to: Blogmarks Add to: Diigo Add to: Technorati Information

How to Create a Tile Game Like Destruct-O-Match on Neopets.com PDF Print E-mail
User Rating: / 2
PoorBest 
Projects - Flash CS3
Written by drooza   
Tuesday, 23 September 2008 19:02
Article Index
How to Create a Tile Game Like Destruct-O-Match on Neopets.com
Linking the Tile class
Creating the Tile Game Engine
Initializing Level Functions
In Game Functions
Clumping Functions
End Of Level / Game Functions
Creating the .fla
Let's Play the Game!
All Pages

If you aren't already familiar with Neopets.com then you should go check them out. I believe they have one of - if not - the biggest audiences of users of online games.  With just over 660 billion page views they have a massive audience.  Neopets is an online community based on an online pet and in order to feed this pet you must play online games (to gain money or neopoints) to buy food.  This mini-world even has their own economy and stock market!

Neopets.comOne of the games there caught my eye, Destruct-o-Match, which is in its third installment.  One of my favorite time-killing games.  I didn't just kill time playing the game but analyzing how it works and developing my own version. 

Now I'm going to show you how I did it using Flash CS3 and Actionscript 3.0.

First we'll start off with the idea of the game:  A group of colored tiles that when 2 or more are grouped together may be destroyed in exchange for points.  If you destroy all the blocks on the field, or you gain enough points, you advance to the next level

 

Second we'll start building the foundation of the game.  What other great place to start than the tiles!  Lets start off with a new class named Tile.

 /** Linkage class for the Library Item Tile. The library item must have 1 tile per
* frame with a flag: "blank" on the last frame to denote it's empty. This will
* enable easy customization and updating.
*
* @author dRooza
*/

package
{
import flash.display.MovieClip;
import fl.transitions.Tween;


public class Tile extends MovieClip
{
public var checked:Boolean; //flag to eliminate duplicate checking
public var highLighted:Boolean; //flag for destruction destroy
public var column:int; //coordinates for where
public var row:int; // tile is located

// pointer to tween so garbage collector doesn't remove prior to completion
public var tween:Tween;


/** Constructor initializes tile to a random frame and sets the boolean
* highlighted to false.
*/
public function Tile()
{
turnOff();
this.gotoAndStop(Math.round(Math.random() * (this.totalFrames - 1)));
}

/** Highlights the tile, marking it as checked.
*
*/
public function turnOn():void
{
this.alpha = .25;
this.highLighted = true;
markAsChecked();
}

/** Sets tile back to normal state: not checked nor highlighted
*
*/
public function turnOff():void
{
this.alpha = 1;
this.highLighted = false;
this.checked = false;
}

/** Checks to see if the tile is highlighted
* @return if the tile is highlighted
*/
public function isHighLighted():Boolean
{
return highLighted;
}

/** Set the tile as checked to prevent it from being checked again
*
*/
public function markAsChecked():void
{
checked = true;
}

/** Checks to see if the tile is highlighted
* @return if the tile has been checked
*/
public function isChecked():Boolean
{
return checked;
}

/** Removes the tile from the stage
*
*/
public function destroy()
{
if(this.parent != null)
{
this.parent.removeChild(this);
}
}
} //end class
} //end package

 


 

Maybe too much to soak in? If so, go back up and read it again.  If you don't know what some of those things mean, there are plenty of sites out there that will help you further decipher the code.  But if you read it slowly then it's just like reading English!  Don't forget to read all the comments, it's pretty self explanatory.

 Now save this file as Tile.as in the same directory as our flash file. After we have to set the linkage for the library item named Tile in the flash file.  

Setting up linkage for Tile image

As you can see in the picture, there are several frames 1-6 with the last frame blank with a flag on it. This flag is "blank".  This could very well be redundant and left over from a previous method of removing the tile, but I digress.  Notice that there is the Tile object in the library with the object registration on the top left of the rectangle (at the coordinates (0, 0)).  Now on the library panel with Tile selected there's a small blue button with an italics i in the center next to the garbage can that brings up the symbol properties.  If the advanced button isn't already clicked, do so!  This will show the lower half of the Symbol Properties window where you can link a class to a library item.  Once you click Export for Actionscript, Class and Base class should automatically fill in.  At this point the we don't have to worry about the Tile object and class anymore, they are finished.


 

Without the comments, TileGame will be 572 lines of code.  Since thats quite a few for an article such as this, we'll split the code up into several sections: import statements, class variables used and initialization methods;  end of level/game functions; intializing level / game function; in-game functions; and last but not least clumping functions!

 

Import Statements, Variables Used and Game Initialization Methods

 First we'll start with the first 100 lines of code which basically set the game up and "run" the game.

/** TileGame.as Tile Game engine using an array of arrays full of instances of
* type Tile. idea of the game: A group of colored tiles that when 2 or more
* are grouped together may be destroyed in exchange for points. If you
* destroy all the blocks on the field, or you gain enough points, you advance
* to the next level.
*
* @see Tile.as
* @author Drooza
*/

package
{
import flash.display.MovieClip;
import flash.display.Stage;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
import flash.text.TextFormat;
import fl.transitions.easing.*;
import fl.transitions.Tween;
import fl.transitions.TweenEvent;

public class TileGame extends MovieClip
{
private var colMax:int; //number of columns
private var rowMax:int; //number of rows
private var tileList:Array; //Will become Array of arrays
private var types:Number; //number of different colors
// in play

private var score:Number;
private var currentLevelScore:Number;
private var level:int;
private var levelScoreToBeat:Number;
public var score_txt:TextField;

public var gameState:int;
private var CONTINUE:int;
private var GAMEOVER:int;
private var NEXTLEVEL:int;
private var START:int;

private var nextLevel_mc:NextLevelButton; //instantiated library items
private var playAgain_mc:PlayAgainButton; // added to stage using as3

/** Class constructor. Sets the number of colors to start with sets
* game states.
*/
public function TileGame()
{
types = 4;
CONTINUE = 0;
GAMEOVER = 1;
NEXTLEVEL = 2;
START = 3;

stage.scaleMode = StageScaleMode.NO_SCALE;

gameState = START;

addEventListener(Event.ADDED_TO_STAGE, init);
}

/** Set the dimensions of the array and instantiates tileList.
* Here game engine is started.
*/
private function init(e:Event):void
{
colMax = 13;
rowMax = 11;
tileList = new Array(colMax);


initScoreField();

addEventListener(Event.ENTER_FRAME, onEnterFrame);
}

/** The heart of the game that runs every frame.
*
*/
private function onEnterFrame(e:Event):void
{
switch(gameState)
{
case CONTINUE:
break;
case GAMEOVER:
trace("game over");
showEndPopUp();
break;
case NEXTLEVEL:
trace("next level!");
if(currentLevelScore >= levelScoreToBeat)
{
nextLevelPopUp();
level ++;
}
else
{
gameState = GAMEOVER;
}
break;
case START:
trace ("start");
resetGameVariables();
fillArray(types);
showTiles();
//break; //no break needed to make gameState = CONTINUE
default: gameState = CONTINUE;
}

setScore();
}
/** Instantiates variables for a new game.
*
*/
private function resetGameVariables():void
{
trace("scores reset");
level = 1;
types = 3;
score = 0;
currentLevelScore = 0;
setScoreToBeat();
}

/** Fills tileList with Tile instantiations and assigns click handlers
* to each tile.
*/
private function fillArray(differentTypes:int):void
{
for(var col:int = 0; col < colMax; col ++)
{
//trace("Creating new row at column " + col);
tileList[col] = new Array(rowMax);

for(var row:int = 0; row < rowMax; row ++)
{
//trace("Creating tile.");
var tile:Tile = new Tile();
tile.column = col;
tile.row = row;
tile.x = (tile.width * col) + 15;
tile.y = -(tile.height * 2);
addChild(tile);
tileList[col][row] = tile;

if(tile.currentFrame > differentTypes)
{
tile.gotoAndStop(Math.random() * differentTypes);
}

tile.addEventListener(MouseEvent.CLICK, tileClickHandler);
}
}
}

Methodology is self explanatory thus far.  What you should see in this section is that it sets up the actual game engine in the onEnterFrame callback function and creates an array of arrays filled with instances of Tile.  This is the data structure used to make this game.  This algorithm is O(n2) because of the nested for-loops.  For this type of game, it should not really matter because there will never be more than a couple hundred tiles in play at any given time.  Optimizations would be futile because although the algorithm can change to something like O(n2 - n) it would still be O(n2).  I stayed away from over optimization.  The only "optimizatio" I made was to add that checked or not checked boolean variable to the tiles to remove any double checking for similar colors.

 


 

 

Level Initializing Functions

Second we'll take a look at our level initializing functions.  These functions set different aspects of game play, including the scores to beat, setting up the score board, setting the difficulty and refilling our data structure.

/** Depending on the level, this sets the score that the player must gain  
* in order to move on to the next level when there are no more moves
* left in play.
*/
private function setScoreToBeat():void
{
switch(level % 6)
{
case 1: levelScoreToBeat = 150;
break;
case 2: levelScoreToBeat = 110;
break;
case 3: levelScoreToBeat = 145;
break;
case 4: levelScoreToBeat = 130;
break;
case 5: levelScoreToBeat = 120;
break;
case 6: levelScoreToBeat = 145;
break;
default: levelScoreToBeat = 150;
}
}

/** The only text field in the game with a TextFormat applied
*
*/
private function initScoreField():void
{
score_txt = new TextField();
score_txt.autoSize = TextFieldAutoSize.LEFT;
score_txt.background = false;
score_txt.x = 300;
score_txt.y = 50;
score_txt.selectable = false;
score_txt.border = false;
score_txt.width = 200;
score_txt.height = 25;
var scoreFormat:TextFormat = new TextFormat();
scoreFormat.color = 0x000000;
scoreFormat.size = 24;
score_txt.defaultTextFormat = scoreFormat;
addChild(score_txt);
}

private function setScore():void
{
score_txt.text = "Score: " + score + " Level: " + level + "\n"
+ currentLevelScore + " of " + levelScoreToBeat;
}

private function startNextLevel(e:MouseEvent):void
{
types += .4; //determines how fast the difficulty rises
currentLevelScore = 0;
setScoreToBeat();
score += calculateBonus(tileCount());
destroyAll();
fillArray(Math.round(types));
showTiles();
gameState = CONTINUE;
removeChild(nextLevel_mc);
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}

Again pretty self explanatory if you actually read the code.  The only thing I feel may need explaining is the difficulty incrementer.  As you can see in the function startNextLevel types is incremented by .4,  this will ensure that the difficulty (based on the number of different types of blocks in play) will increase every 2-3 levels. If you want more levels to be played by the user, decrease this value.  Depending on the difficulty, this value can affect the bonus received at the end of each level.


 

 In Game Functions


/** If the player destroys more than 6 tiles, they get a special bonus
*
*/
private function calculateInGameBonus(tiles:int):Number
{
if(tiles > 6)
{
return tiles + Math.round(tiles * (2 / 3));
}
return tiles;
}

/** Make sure that all tiles are in their correct positions
*
*/
private function showTiles():void
{
for(var col:int = 0; col < tileList.length; col ++)
{
for(var row:int = 0; row < tileList[col].length; row ++)
{
if(tileList[col][row] != null)
{
var tile:Tile = tileList[col][row];
var targetX:Number = (tile.width * col) + 15;
var targetY:Number = (tile.height * row) + 150;
if(tile.x != targetX)
{
tile.x = targetX;
}

if(tile.y != targetY)
{
tile.tween = new Tween(tile, "y", None.easeNone, tile.y, targetY, 2, false);
tile.tween.addEventListener (TweenEvent.MOTION_FINISH, motionEnd);
//moveTileY.addEventListener(TweenEvent.MOTION_STOP, motionChange);
//moveTileY.addEventListener(TweenEvent.MOTION_CHANGE, motionChange);
}
else
{
tile.y = targetY;
}
}
}
}
}

private function tileCount():int
{
var total:int = 0;

for(var col:int = 0; col < tileList.length; col ++)
{
for(var row:int = 0; row < tileList[col].length; row ++)
{
if(tileList[col][row].currentFrame < tileList[col][row].totalFrames)
{
total ++;
}
}
}

return total;
}

/** Make sure when the tile animations stop that they're in the right place
*
*/
private function motionEnd (e:Event):void
{
if(! (e.target.obj is Tile))
throw new Error("Not a tile!");
var tile:Tile = e.target.obj as Tile;
var targetX:Number = (tile.width * tile.column) + 15;
var targetY:Number = (tile.height * tile.row) + 150;
if(tile.x != targetX)
{
trace(tile + "[" + tile.column + "][" + tile.row
+ "] is not at the correct X-Coordinate");
tile.x = targetX;
}
if(tile.y != targetY)
{
trace(tile + "[" + tile.column + "][" + tile.row
+ "] is not at the correct Y-Coordinate");
tile.y = targetY;
}
delete this;
}

If you haven't seen these two operators before: is and as, they are new to AS3.  Since "instanceof" has been deprecated, "is" has taken over.  The "as" operator works for type casting. Back to the code


private function motionChange (e:Event):void
{
//trace("motion stopped: " + e);
}

private function tileClickHandler(e:MouseEvent):void
{
var tile:Tile = Tile(e.target);
//trace("(" + tile.column + " ," + tile.row + ")");
if( ! tile.isHighLighted() )
{
dimAll();
}
else
{
var justScored = calculateInGameBonus( getNumberHighLighted() );
score += justScored;
currentLevelScore += justScored;

destroyHighLighted();
showTiles();
if( ! anyMovesLeft() )
{
gameState = NEXTLEVEL;
}
}

findSurrounding( tile , tile.currentFrame );
if(getNumberHighLighted() < 2)
{
dimAll();
}
}

/** Make sure all of the tiles are not highlighted
*
**/
private function dimAll():void
{
for(var col:int = 0; col < tileList.length; col ++)
{
for(var row:int = 0; row < tileList[col].length; row ++)
{
if(tileList[col][row] != null)
{
tileList[col][row].turnOff();
}
}
}
}

/** Recursively find all the surrounding tiles that are of the same color
* This is where that small optimization came in where it doesn't check
* the same tile twice when searching. Not so much an optimization as it
* is a smart function.
**/
private function findSurrounding(tile:Tile, tileFrameNumber:int):void
{
if(tile.currentFrame == tileFrameNumber)
{
var row = tile.row;
var col = tile.column;
tile.markAsChecked();
tile.turnOn();

//check bottom
if(row < rowMax - 1)
{
if(tileList[col][row + 1] != null)
{
if( ! tileList[col][row + 1].isHighLighted() )
{
findSurrounding(tileList[col][row + 1], tileFrameNumber);
}
}
} //else trace("bottom reached");

//check right
if(col < colMax - 1)
{
if(tileList[col + 1][row] != null)
{
if( ! tileList[col + 1][row].isHighLighted() )
{
findSurrounding(tileList[col + 1][row], tileFrameNumber);
}
}
} //else trace("right reached");

//check top
if(row > 0)
{
if(tileList[col][row - 1] != null)
{
if( ! tileList[col][row - 1].isHighLighted() )
{
findSurrounding(tileList[col][row - 1], tileFrameNumber);
}
}
} //else trace("top reached");

//check left
if(col > 0)
{
if(tileList[col - 1][row] != null)
{
if( ! tileList[col - 1][row].isHighLighted() )
{
findSurrounding(tileList[col - 1][row], tileFrameNumber);
}
}
} //else trace("left reached");
}

}

/** Retrieves all the tiles that are currently highlighted
*
**/
private function getNumberHighLighted():int
{
var total:int = 0;

for(var col:int = 0; col < tileList.length; col ++)
{
for(var row:int = 0; row < tileList[col].length; row ++)
{
if(tileList[col][row] != null)
{
if(tileList[col][row].isHighLighted())
{
total ++;
}
}
}
}

return total;
}

private function destroyHighLighted():int
{
var total:int = 0;

for(var col:int = 0; col < tileList.length; col ++)
{
for(var row:int = 0; row < tileList[col].length; row ++)
{
if(tileList[col][row] != null)
{
if(tileList[col][row].isHighLighted())
{
tileList[col][row].removeEventListener(MouseEvent.CLICK, tileClickHandler);
tileList[col][row].gotoAndStop("blank");
tileList[col][row].destroy();
// move blocks down
for(var i:int = row; i > 0; i --)
{
var temp:Tile = tileList[col][i];
temp.row --;
tileList[col][i] = tileList[col][i - 1];
tileList[col][i].row ++;
tileList[col][i - 1] = temp;
}

}
}
}
}
compressColumns();
dimAll();
return total;
}

private function anyMovesLeft():Boolean
{
dimAll();
for(var col:int = 0; col < tileList.length; col ++)
{
for(var row:int = 0; row < tileList[col].length; row ++)
{
var tile:Tile = tileList[col][row];
//trace("checking: " + col +":"+ row);
if(tile.currentFrame < tile.totalFrames)
{
findSurrounding( tile , tile.currentFrame );
//trace("highlighted:" + getNumberHighLighted());
if( getNumberHighLighted() > 1 )
{
dimAll();
return true;
}
}
dimAll();
}
}

return false;
}

private function destroyAll():void
{
for(var col:int = 0; col < tileList.length; col ++)
{
for(var row:int = 0; row < tileList[col].length; row ++)
{
tileList[col][row].destroy();
}
}
}

 
Phew! That was a good chunk of code.  These functions took a little bit of thinking and testing before getting right.  Sit down, take a piece of paper and draw out what needs to be done before sitting down to program.  This is always the best way to go about things.  Even though I didn't have a huge stack of use cases and sequence diagrams, I still managed to do an O.K. job only because I programmed this by myself.  It's best to have a PLAN!


 

Clumping Functions

Certain lines of code are commented out, only because I turned off some observability of the project when they are finished being debugged.  I find it a good idea to keep observability statements in the code in case trouble arises.

 private function compressColumns():void
{
for(var col:int = 0; col < tileList.length; col ++) // go through all columns
{
if( emptyColumn(tileList[col]) ) // if column is empty
{
//trace("column " + col + " is empty.");
swapNextNonEmptyColumn(col);
}
}
//showTiles();
}

/** This is why those blank frames are needed! AH HA!
* The way this checks for an empty list is only by checking the current frame
**/
private function emptyColumn(array:Array):Boolean
{
for(var i:int = 0; i < array.length; i++)
{
if(array[i].currentFrame < array[i].totalFrames)
{
return false;
}
}
return true;
}

private function swapNextNonEmptyColumn(col:Number):void
{
//trace("starting at column " + col + ", finding next non empty column");
var swap:int = col;
while( emptyColumn(tileList[swap]) ) //find next non empty column
{
swap++;
if(swap >= tileList.length)
{
break;
}
}
if(swap < tileList.length)
{
for(var row:int = tileList[swap].length - 1; row >= 0; row --) // start at the bottom
{
var temp:Tile = tileList[col][row]; // tile to move
temp.column = swap; // make it's column to the swap column
tileList[col][row] = tileList[swap][row]; // change reference
tileList[col][row].column = col; // make it's column one less
tileList[swap][row] = temp;
}
}
}

 

 



End Of Level / Game Functions

 

/**  If the user has cleared all the blocks they get a huge bonus!
*
**/
private function calculateBonus(tilesLeft:int):Number
{
switch(tilesLeft)
{
case 0: return 500;
case 1: return 250;
case 2: return 200;
case 3: return 150;
case 4: return 100;
case 5: return 50;
}
return 0;
}

private function nextLevelPopUp():void
{
removeEventListener(Event.ENTER_FRAME, onEnterFrame);
nextLevel_mc = new NextLevelButton;
nextLevel_mc.x = 290 - nextLevel_mc.width;
nextLevel_mc.y = 50;
addChild(nextLevel_mc);
nextLevel_mc.addEventListener(MouseEvent.CLICK, startNextLevel);
}

private function showEndPopUp():void
{
removeEventListener(Event.ENTER_FRAME, onEnterFrame);
playAgain_mc = new PlayAgainButton();
playAgain_mc.x = 290 - playAgain_mc.width;
playAgain_mc.y = 50;
addChild(playAgain_mc);
playAgain_mc.addEventListener(MouseEvent.CLICK, restartGame);
}

private function restartGame(e:Event):void
{
resetGameVariables();
destroyAll();
fillArray(types);
showTiles();
gameState = CONTINUE;
removeChild(playAgain_mc);
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}


Creating the .fla

This really isn't supposed to be the last thing to do when making a game like this, but it is shown last in the tutorial.

fla specs

Add TileGame to the document class under the properties tab making sure that you did create a new Actionscript 3.0 document using Flash Player 9+.  In case you didn't catch it, the two variables that were in the library must be created and linked just as the Tile class was created.  (What about Tile copy? - Answer: delete it.  It was something I was probably experimenting with.)

 Now let's finally play this game!

Later on we'll add a high-score table, viewable to each user.

 


 

 

 

 

 

 

Comments
Add New Search
Bug Found
drooza (SAdministrator) 2008-09-23 19:59:00

There's this one bug that happens sparingly. Sometimes the tiles don't
fall into correct place and are stuck, suspended in the air. If I remove the
animation totally, then no more bug. I've got that much.

I'm thinking I have
to check where the tile is located when the animation stops.
Bug Fix:
drooza (SAdministrator) 2008-11-07 09:54:10

Because a tween object is instantiated inside an if statement (oops) the
garbage collector is taking care of business and removing the
tween before it completes.

By making this tween a part of the Tile
class, this may fix the problem.

Other fixes in the comments section
of this page: http://livedocs.adobe.com/flash/9.0/ActionScrip...
Losing?
KeenEyes (76.174.154.xxx) 2008-10-09 18:52:29

How do you lose in this game again? lol
drooza (SAdministrator) 2008-10-09 19:11:10

If you don't achieve the points set by the game for that level, you lose.
donald (119.154.16.xxx) 2010-02-15 23:47:27

thanks for your toturial....its really nice and mean full. I want to write
more but these days I am doing preparation of different
online certifications and I found ccna certification is the best helping source which is providing 100% authentic material. I
also spend my extra time in surfing internet, listening music
and playing games. After my exams I would like to join your group.
Write comment
Name:
Email:
 
Title:
UBBCode:
[b] [i] [u] [url] [quote] [code] [img] 
 
:angry::0:confused::cheer:B)
:evil::silly::dry::lol::kiss:
:D:pinch::(:shock::X
:side::):P:unsure::woohoo:
:huh::whistle:;):s:!:
:?::idea::arrow:
Please input the anti-spam code that you can read in the image.

3.26 Copyright (C) 2008 Compojoom.com / Copyright (C) 2007 Alain Georgette / Copyright (C) 2006 Frantisek Hliva. All rights reserved."

Last Updated ( Saturday, 22 November 2008 13:05 )
 

Make a Donation!

Advertisement
Banner

HomeProjectsSnippetsContact Me

Copyright © 2008, Drooza.com