Case study: building a puzzle game using AS3 and OOP
Page 1/1
[ November 10, 2007 ] Eitan Avgil
Learn how to create a simple "match the colors" puzzle game using OOP techniques in Actionscript 3
The game is a simple color match game. The goal is to order all colors by the color of the left column. Right now there are up to 6 colors but it can be easily extend to n colors.
The idea behind this simple game's logic is simple. Clicking one button – will check if it's near the empty place, and if it is – it will move to the empty place coordinates. This looks like there is an empty place there but actually there is a regular square – same class as all squares with a special color and no interactivity. The switching has no animation but because the squares are so close it looks like they are moving. Once all squares are in position the timer is stopped.
We have 4 objects (4 classes) in this game:
Main class
Matrix
Logic
Box
Main class
The main class is responsible of the following tasks:
create a Matrix instance, passing its parameters;
create a Logic instance, passing it the array from the Matrix and the WhiteBox;
make the AddChild of the Matrix;
create and manage the textField and the timer.
package {
import flash.display.Sprite;
import flash.text.TextField;
import flash.events.MouseEvent;
import flash.events.Event;
import flash.utils.Timer;
import flash.events.TimerEvent;
/**
* main application. calls the matrix and send rows & columns
* creates logic. set 'start' button and set timer & textfield;
* @author Eitan Avgil
*
*/
public class Main extends Sprite
{
private var _matrix:Matrix;
private var _tf:TextField;
private var _logic:Logic;
private var _timer:Timer;
public function Main() {
_matrix = new Matrix(3,5);
_matrix.x = 60;
_matrix.y = 80;
_tf = new TextField();
_tf.border = true;
_tf.height = 20;
_tf.x = 10;
_tf.y = 10;
_tf.width = 150;
_tf.selectable = false;
var startButton:Sprite = new Sprite();
startButton.graphics.beginFill(0xCCCCCC)
startButton.graphics.drawRect(0,0,50,20)
var tf:TextField = new TextField()
tf.text = "start";
tf.selectable = false;
startButton.addChild(tf);
startButton.x = 50;
startButton.y = 50;
addChild(startButton)
startButton.addEventListener(MouseEvent.CLICK,onClick);
}
/**
* start the game by clicking the start text
* @param evt
*
*/
private function onClick(evt:MouseEvent):void{
removeChild(evt.currentTarget as Sprite);
addChild(_matrix);
addChild(_tf);
_logic = new Logic(_matrix.boxesArray,_matrix.whiteBox);
_logic.addEventListener(Event.COMPLETE,onGameEnd);
_timer = new Timer(10);
_timer.addEventListener(TimerEvent.TIMER,onTimerTic);
_timer.start()
}
/**
* end game text
* @param evt
*
*/
private function onGameEnd(evt:Event):void{
_timer.stop();
_tf.text = "DONE >> "+numberToTime(_timer.currentCount)
}
/**
* timer counter
* @param evt
*
*/
private function onTimerTic(evt:TimerEvent):void{
_tf.text = numberToTime(Number(Timer(evt.target).currentCount));
}
/**
* rounds the timer to 2 decimal after point
* @param n
* @return
*
*/
private function numberToTime(n:uint):String{
return String(n/100);
}
}
}
Box class
This class represent the single square. The class gets a color code parameter in its constructor and has to draw a square with this color. Once this square is clicked, the instance dispatches an event. The class that manages all boxes will catch this event and will know what to do with it.
Some boxes are not interactive (the whitebox and the left column boxes) so they will get a special treat by removing their click dispatch. If we would make this a perfect OOP model, these instances would be an extension of the Box class, but I decided that this project would show how to break a game to classes and object and did not want to add too much OOP.
Each Box gets (through a setter function) its initial ycoordinate, so it will be "able", when asked, to tell us if it is sitting in the correct row or in a wrong row.
These are the public methods of the square:
Constructor – gets a color code, transparency and border thickness
set heightInit – to set the init Y coordinate
setEmptyBox – to set the whitebox
disableInteractive – remove the mouseEvent listening if this box is not interactive
isInOriginalHeight – box self check if it is in the correct row (returns true/false)
The class also holds a static const of the square width, and a static event name.
package
{
import flash.display.Sprite;
import flash.events.MouseEvent;
import flash.events.Event;
/**
* a single square. has no logic. dispatches click event when clicked - thats it.
* @author Eitan Avgil
*
*/
public class Box extends Sprite {
// this height value is here so the box will "know" if it is in the right row
private var _originalHeight:Number;
private var _colorCode:uint;
private var _borderAlpha:Number;
private var _borderWidth:Number;
//color array - matches code to a constant color code
private const COLOR_ARRAY:Array = [0xFF0000,
0x00FF00,
0x0000FF,
0xDD00DD,
0X00FFFF,
0xFFFF00];
// defenition of box size
public static const BOX_SIZE:uint = 20;
//clicked string event name
public static const BOX_CLICKED:String = "clicked";
//C'tor
public function Box(colorCode:uint,borderAlpha:Number=0,borderWidth:Number=0){
_colorCode = colorCode;
_borderAlpha = borderAlpha;
_borderWidth = borderWidth;
drawRect(COLOR_ARRAY[_colorCode]);
addEventListener(MouseEvent.CLICK,onMouseClick)
}
// just draw graphic rectangle
private function drawRect(color:uint):void{
graphics.beginFill(color);
graphics.lineStyle(_borderWidth,0xFFFFFF,_borderAlpha)
graphics.drawRect(0,0,BOX_SIZE,BOX_SIZE)
graphics.endFill();
}
//inner click listening
private function onMouseClick(evt:MouseEvent):void{
// dispatches event to whome it may listen
//trace(Box(evt.target).colorCode)
dispatchEvent(new Event(BOX_CLICKED));
}
// remember the initialized Y coordinate for checking later on.
public function set heightInit(value:Number):void{
_originalHeight = value;
}
// the whiteBox
public function setEmptyBox():void{
graphics.clear();
_colorCode = undefined;
drawRect(0xFFFFFF);
alpha=0.2;
}
//remove interactive for the non-interactive boxes
public function disableInteractive():void{
removeEventListener(MouseEvent.CLICK,onMouseClick);
}
// check if current instance is in it's initialize (correct) Y coordinate.
public function isInOriginalHeight():Boolean{
if(this.y==_originalHeight) return true;
return false
}
}
}
Matrix class
This class has a single instance in the game, and it creates and arrange all boxes. Its responsibility is to mix all squares (only interactive squares) and let other classes get the array of all boxes:
create, arrange mix and gather all boxes in an array. That's it.
We could also write the logic part of the game in it, but we wanted to show how to divide the creation and the logic, so we decided to make 2 separated classes.
Matrix class has the following public methods:
Constructor – gets rows and columns numbers
get boxesArray – return an array of all interactive boxes
get whiteBox – returns the instance of the whitebox
Although the shuffling function is a private method, we would like to say a thing or two about it. The main idea of the shuffling here is swapping objects coordinates. No array commands and no 2 dimensions array are involve here. So, how is it done?
When we create the boxes, we push them into an array (simple one dimention array) in the order we create them. The mixing function runs on the array, takes the instance of the current index and just replace its coordinates with those of another random box from the array. In this way, every box is being replaced at least once.
All boxes are in the array, only in mixed coordinates.
package
{
import flash.display.Sprite;
import flash.geom.Point;
/**
* a visual class - places boxes by rows & columns
* places the empty box
* scramble all boxes
* @author Eitan Avgil
*/
public class Matrix extends Sprite{
private var _whiteBox:Box
private var _rows:uint;
private var _cols:uint;
private var _gap:uint; // space between the boxes
private var _boxes:Array; // 2 dimentions array of organized colored boxes
public static const GAP:uint = 5;
public function Matrix(rows:uint,cols:uint){
_rows = rows;
_cols = cols;
_boxes = new Array();
buildMatrix();
buildGuideBoxes();
addWhiteBox();
scramble();
}
// build the matrix. and pushes all boxes to an array
private function buildMatrix():void{
var i:uint;
var j:uint;
var box:Box;
for(i=0;i< _rows;i++){
for(j=0;j< _cols;j++){
// create a new box and push it to the row
box = new Box(i);
addChild(box);
box.x = j*(Box.BOX_SIZE+GAP);
box.y = i*(Box.BOX_SIZE+GAP);
box.heightInit = box.y;
_boxes.push(box);
}
}
}
// build the left collumn boxes
private function buildGuideBoxes():void{
var j:uint;
var box:Box;
for(j=0;j< _rows;j++){
box = new Box(j,1,2);
box.disableInteractive();
addChild(box);
box.x = -Box.BOX_SIZE-(GAP*3);
box.y = j*(Box.BOX_SIZE+GAP);
}
}
// creates the non-colored box
private function addWhiteBox():void{
_whiteBox = new Box(0);
_whiteBox.setEmptyBox();
_whiteBox.disableInteractive();
addChild(_whiteBox);
_whiteBox.y = -Box.BOX_SIZE-GAP;
_whiteBox.heightInit = _whiteBox.y;
}
// scramble all boxes. make sure whitebox and first box is nor scrambled
private function scramble():void{
var point:Point
var box:Box;
var tmpBox:Box
var rnd:uint;
var max:uint = _boxes.length-1;
for (var i:uint=1;i< max;i++){
box = _boxes[i];
point = new Point(box.x,box.y);
rnd=(Math.random()*max)+1;
tmpBox = _boxes[rnd];
switchPlaces(box,tmpBox);
}
}
// switches places between 2 boxes
private function switchPlaces(box:Box,box2:Box):void{
var point:Point = new Point(box.x,box.y);
box.x = box2.x;
box.y = box2.y;
box2.x = point.x;
box2.y = point.y;
}
public function get boxesArray():Array{
return _boxes;
}
public function get whiteBox():Box{
return _whiteBox;
}
}
}
Logic class
This class is the brain of the game. It gets an array (the array created by the matrix class) and just listen to events from its elements. After adding the eventListeners it sits and waits that one of the boxes dispatches the "I was clicked" event.
When the Logic class gets this event, it checks if the box that dispatched the event is a box next to the whitebox, by checking its coordinates. It checks if they have both (clicked box and whitebox) the same Y coordinate and if so, if the horizontal offset between them is equal to one box width (plus the gap); same thing for the other dimension.
If, and only if, the clicked box is next to the whitebox we switch their coordinates. After switching them, the Logic class checks if the whitebox is in its original place (out of the colored rows) and if yes, it checks all boxes positions. We divided this check in two parts because we don't need to check on each switch if all boxes are in place. The game can be over only when the whitebox is in its place, so that way we can save unnecessary checks.
The check of all boxes is very simple. Because we gave each box the ability to "know" if it is in the correct Y coordinate, all we have to do now is just a simple for loop and just ask each box "are you ok there?". If each box wouldn't know if it is placed correctly, the checking would be a bit more complex (what is your color? what is your Y position? is this position right for this color?).
A better OOP designed game would separate the view from the data, so the check would be simple on the data side, but this is too OOP for this small game.
package
{
import flash.events.EventDispatcher;
import flash.events.Event;
import flash.geom.Point;
public class Logic extends EventDispatcher{
private var _boxes:Array;
private var _whiteBox:Box;
//C'tor
public function Logic(boxes:Array,whiteBox:Box){
_boxes = boxes;
_whiteBox = whiteBox;
addListenersToBoxes();
}
// add click listener to the interactive boxes.
private function addListenersToBoxes():void{
var box:Box;
for(var i:uint=0;i< _boxes.length;i++){
box = _boxes[i] as Box;
box.addEventListener(Box.BOX_CLICKED,onBoxClicked)
}
}
// the function that is attached to clicks on every interactive box
private function onBoxClicked(evt:Event):void{
var box:Box = evt.currentTarget as Box;
checkBox(box);
}
// checks if the given box is next to the whiteBox
private function checkBox(box:Box):void{
if((box.x==_whiteBox.x && (Math.abs(box.y-_whiteBox.y)==Box.BOX_SIZE+Matrix.GAP))
|| (box.y==_whiteBox.y && (Math.abs(box.x-_whiteBox.x)==Box.BOX_SIZE+Matrix.GAP))){
switchPlaces(box,_whiteBox);
}
}
//switches places between the white box and a given box
private function switchPlaces(box:Box,box2:Box):void{
var point:Point = new Point(box.x,box.y);
box.x = box2.x;
box.y = box2.y;
box2.x = point.x;
box2.y = point.y;
checkWhiteBox();
}
// check if the whitebox is in it's place. only if it is -
// continue checking all other boxes.
private function checkWhiteBox():void{
if(_whiteBox.isInOriginalHeight()){
//trace("whitebox in place - let's check all other boxes")
checkAllBoxes();
}
}
// ask each box if it is in it's original height
private function checkAllBoxes():void{
for (var i:uint=0;i< _boxes.length;i++){
if(!Box(_boxes[i]).isInOriginalHeight()){
//trace("sorry - not all boxes are in their places");
return;
}
}
//trace("all in place")
dispatchEvent(new Event(Event.COMPLETE));
}
}
}
Summary
This application was built to show some OO principles:
how to break the application into different objects, and the responsibility of each object;
how to build a visual matrix and mix the boxes;
how to pass informations (array of boxes) and separate the logic layer and the visual layer;
how to establish communication between classes: lower class shoots events to higher class, higher class use direct access through instance pointer;
how static elements are used between classes (events names, constant numbers).
Here's the Adobe Flex project files packed in one sigle rar: Boxes.rar
Page 1/1
Name:
Eitan Avgil
Location:
Israel
Age:
32
Flash experience:
About 5 years experience in flash programming, 2 of them in OOP environment; about one year in AS3.
Runs a flash community (in hebrew) called Flashoo and an AS3 blog.