This next part is fun stuff: making our class dispatch custom events and having our client code listen for them. It so happens that there are three events taking place in our class that our client code might be interested in knowing about, so that it can react to them.
The first is when a clip is dragged and a match is made. The second is when a clip is dragged and a match is not made. The third is when all of the items have been matched. The first two are easy, and the third only requires us to create a variable to keep track of how many items have been matched.
In order for our class to dispatch events, it must extend the EventDispatcher class. This little detail just means we need to add the words extends EventDispatcher at the end of our class declaration, and we also have to import that class in our import statements:
import flash.events.EventDispatcher;
Our next step is to create some public static constants to store the strings of text that we will dispatch. This step, while not technically required, is an established convention and a good practice which I have written about elsewhere. We will name these public static constants MATCH_MADE, NO_MATCH, and ALL_DONE. It is also conventional to make these names all uppercase with separate words indicated with underscores. One glance, and you can tell those are constants! Also, unlike ordinary variables that might wait till the constructor runs to get their values, these constants are static, meaning that they belong to the class and not to individual instances, and we assign them their values right away. Typically, they get the same text assigned to them as the variable name, but written in camel case instead (example: MATCH_MADE = "matchMade"). But the truth of the matter is that this string of text could be absolutely anything. However you would probably not want to duplicate any of the already established ones in the language, just in case. The word "static" you can think of as meaning "only one copy," and "belonging to the class." To access a public static constant, from outside code you just use the name of the class, then a dot, then the name of the variable. It's exactly the same convention used for the built-in constants in the language (like MouseEvent.CLICK, which is just a constant that stores the string "click").
The three lines of code declaring these constants have been added to the top of the variables list, starting at line 11 (see the code listing below).
The other strategic aspect is where to dispatch the events from. This is easy. Find the place in the code where the clip is being dropped. Then find the place where the clip either matches or doesn't. In the place where the clip matches, dispatch the event for MATCH_MADE. In the place where the code determines that the clip doesn't match, dispatch the event for NO_MATCH. For the third one, though, to prepare for the event dispatch, we need to create a variable. We'll call it matchesMade, add it to the variables list, initialize it to 0 in the constructor, and then increment it by 1 each time a match is made. Then we need to put in code that compares this variable's value to the length of the dragArray. If they are equal, then all the matches have been made, and so we dispatch the ALL_DONE event. You'll see all that in the finished code below:
package com.jessamin.games {
//import statements go here:
import flash.display.MovieClip;
import flash.display.Stage;
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.MouseEvent;
public class DragGame extends EventDispatcher {
//variable list goes here:
public static const NO_MATCH:String = "noMatch";
public static const MATCH_MADE:String = "matchMade";
public static const ALL_DONE:String = "allDone";
private var matchesMade:int;
private var _stage:Stage;
private var _dragArray:Array;
private var _matchArray:Array;
private var _posArray:Array;
private var _hitTestMethod:IHitTestMethod;
private var currentClip:MovieClip;
private var startX:Number;
private var startY:Number;
public function DragGame(stage,
dragArray,
matchArray,
posArray = null,
hitTestMethod = null) {
//save the five supplied arguments into the private variables:
_stage = stage;
_dragArray = dragArray;
_matchArray = matchArray;
_posArray = posArray;
_hitTestMethod = hitTestMethod;
//make hit test point the default
//if nothing is supplied:
if (_hitTestMethod == null) {
_hitTestMethod = new HitTestPoint();
}
//add event listeners for MOUSE_DOWN to all the drag clips:
for (var i:int = 0; i < _dragArray.length; i++) {
_dragArray[i].buttonMode = true;
_dragArray[i].addEventListener(MouseEvent.MOUSE_DOWN, item_onMouseDown);
}
matchesMade = 0;
}
private function item_onMouseDown(event:MouseEvent):void {
currentClip = MovieClip(event.currentTarget);
startX = currentClip.x;
startY = currentClip.y;
currentClip.parent.addChild(currentClip); //bring to the front
currentClip.startDrag();
_stage.addEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
}
private function stage_onMouseUp(event:MouseEvent):void {
_stage.removeEventListener(MouseEvent.MOUSE_UP, stage_onMouseUp);
currentClip.stopDrag();
var index:int = _dragArray.indexOf(currentClip);
var matchClip:MovieClip = MovieClip(_matchArray[index]);
//here we modify the hit testing to use our interface:
if (_hitTestMethod.hitTest(matchClip, currentClip)) {
//match was made, so:
if (_posArray == null) {
currentClip.x = matchClip.x;
currentClip.y = matchClip.y;
} else {
currentClip.x = _posArray[index].x;
currentClip.y = _posArray[index].y;
}
//make it not draggable anymore:
currentClip.removeEventListener(MouseEvent.MOUSE_DOWN, item_onMouseDown);
currentClip.buttonMode = false;
//increment matches made. dispatch events
matchesMade++;
dispatchEvent(new Event(DragGame.MATCH_MADE));
if (matchesMade == _dragArray.length) {
//only dispatch this one if all the matches have been made
dispatchEvent(new Event(DragGame.ALL_DONE));
}
} else {
//match was not made, so send the clip back where it started:
currentClip.x = startX;
currentClip.y = startY;
//dispatch the bad news: no match made
dispatchEvent(new Event(DragGame.NO_MATCH));
}
}
public function set hitTestMethod(value:IHitTestMethod):void {
this._hitTestMethod = value;
}
}
}
This class has grown fairly large, but it's really not all that complicated either. There is really only a constructor, two event handler functions, and another public function for setting the hit testing method. Since we took it from timeline code, you've gotten to see it transformed into a custom class. Now let's see how easy it is to put to work from our client code.
Going back to the fla file we left a couple of pages ago, let's return to the actions layer. Delete all the code there except for the lines that declare the arrays. Next, let's add a line to import our new game classes:
import com.jessamin.games.*;
We'll just use the * to import them all. Next, we'll make an instance of the DragGame class, and pass it the array references (and the stage reference) at the same time:
var dragGame:DragGame = new DragGame(stage, dragArray, matchArray, posArray);
You might wonder about the array names being the exact same names as the names in the class's parameters list. There really is no clash, it's perfectly fine to use the same names.
By supplying a posArray, we know that the clips will snap to the values given in the posArray when we make a match. If we hadn't supplied this argument, they would snap to the x and y of the matching clips. Finally, by not supplying a fifth argument for a hit test method, we know that our class will supply the default one, which is an instance of HitTestPoint.
Here is the complete code (fla file, first frame) so far:
import com.jessamin.games.*;
var dragArray:Array = [square_word, circle_word, triangle_word];
var matchArray:Array = [squareMatch, circleMatch, triangleMatch];
var posArray:Array = [ {x:276, y:207}, {x:443, y:207}, {x:107, y:207} ];
var dragGame:DragGame = new DragGame(stage, dragArray, matchArray, posArray);
And we find that when we run the swf, it behaves identically to the way it did before when all the code was on the timeline. As an experiment, try removing the last argument, posArray. Now run the file. The dragging clips will snap to the middle of the matching clips, as though this were a build type game. Okay, now put it back again.
Another experiment: If we put in a line like this right after the line that instantiates dragGame:
dragGame.hitTestMethod = new HitTestObject();
... we will find that now our code uses the other method of hit testing, and that the clips make a match in the more loose way we saw earlier (bounding boxes collide). This is polymorphism at work. With one line of code, we can completely change how the collisions are determined! (Note that we could have also done this in the DragGame's constructor, by making use of that fifth parameter).
By the way, we left out the part where the code reduces the alpha of the clips in the matchArray. I figured that this aspect should be controlled by the client code in the fla file, and since it has a reference to the same array, it's not a problem. But you have to always make these kind of decisions as to what should be left in the class and what should be controlled from outside of it. It's best to just let the class handle the things that we know are probably not going to vary from project to project, and we won't always want match clips to have 0 alpha.
Now let's add some event listeners to our instance of the DragGame class. Just for the occasion, I imported some sound files to the library of the fla file. I did this by going to the file menu and choosing Import, then Import to library. I imported three short WAV fles that I had on hand (actually, I got them from Jessamin). The next step was to right click each one in the library, and check off "Export for actionscript" and then make up a class name for each one. When the user makes a correct match, a nice sound ("plink!") will play as the clip locks into place. When the user gets it wrong, a sound like a rubber band being strummed will sound ("boiiing"), and when the user matches all the objects, an applause sound will play.
Naturally, all this will happen in response to our class dispatching those custom events, and us listening for them in the fla file.
So do you see the methodology here? We add an event listener for an event to the same instance that is going to be dispatching it, in this case our instance named dragGame. There is no such thing as just setting up a generic event listener that listens for events from everywhere. The event listener has to be added to the same object that is going to be dispatching it. That's how it works.
Here's the final version of the code in the fla:
import com.jessamin.games.*;
var dragArray:Array = [square_word, circle_word, triangle_word];
var matchArray:Array = [squareMatch, circleMatch, triangleMatch];
var posArray:Array = [ {x:276, y:207}, {x:443, y:207}, {x:107, y:207} ];
var dragGame:DragGame = new DragGame(stage, dragArray, matchArray, posArray);
dragGame.addEventListener(DragGame.MATCH_MADE, onMatch);
dragGame.addEventListener(DragGame.NO_MATCH, onFlub);
dragGame.addEventListener(DragGame.ALL_DONE, onDone);
function onMatch(event:Event):void {
var matchSound:Sound = new MatchSound();
matchSound.play();
}
function onFlub(event:Event):void {
var flubSound:Sound = new FlubSound();
flubSound.play();
}
function onDone(event:Event):void {
var applause:Sound = new Applause();
applause.play();
}
And here's how the swf behaves (WARNING: this swf plays sounds!!!):
This has been a fun tutorial. I feel like I'm done, though, and I hope you got a lot out of it! It's been my pleasure to bring it to you. As always, there are lots of ways that you can customize this and try more and different things. Here are just a few suggestions:
I always hope that my tutorials will be food for thought, and that you will see them not as "set in stone" but rather as a launching point for ideas of your own. Anyway, till next time.... (I may yet add another page and answer #4 myself. I don't like having to refresh the page to reset the game)....