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!
One 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 */
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.
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 */
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);
/** 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);
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 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; }
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
/** 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();
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; }
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.
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.
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.
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.
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.