Read Foundation Game Design with ActionScript 3.0, Second Edition Online
Authors: Rex van der Spuy
You'll also notice that the names of both the private_levelOne
and_levelTwo
objects begin with an underscore character (_). This is a naming convention that is entirely optional, but one that I'll be using in this book. Preceding the names of private variables with underscore characters helps you tell at a glance which properties are private. It can help you to easily distinguish which variables are accessible throughout the whole class and which variables are only accessible inside a method. You'll see how this will be helpful in some of the code examples ahead.
Now that you know theMonsterMayhem
application class does almost nothing except create and display a_levelOne
object, where is all the code for the game? Most of it is in theLevelOne
andLevelTwo
classes.
These are the classes thatMonsterMayhem
uses:
MonsterMayhem.as
LevelOne.as
LevelTwo.as
Background.as
Character.as
Explosion.as
GameOver.as
Monster.as
Star.as
MonsterMayhem
is the application class and adds the levels to the game, as you've just seen.LevelOne
andLevelTwo
contain all the game programming. All the other classes are game object classes. You'll first take a look at howLevelOne
is added to the stage and then at how all the specific features of the game are programmed.
You've just seen that when the application class creates the_levelOne
object, it passes it a reference to the stage as an argument, like this:
_levelOne = new LevelOne(
stage
);
Remember that the application class,MonsterMayhem
, is the only class that has direct access to thestage
object. If any other objects need to access the stage for some reason, the application class has to pass that class a reference to the stage when it creates an object. The reference to the stage gets passed to an object through its constructor method.LevelOne
needs to know about the stage because one of the things is has to do is attach keyboard event listeners to the stage so that players can move the character with the arrow keys. To keep a reference to the stage inside itself,LevelOne
also needs a variable to store it. Here's the code for theLevelOne
constructor and its variable called_stage
that it uses to store the stage reference (the highlighted code is the important stuff):
private var _stage:Object;
public function LevelOne(stage:Object)
{
_stage = stage;
this.addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
}
When the application class creates the_levelOne
object, it sends it a reference to thestage
. The_levelOne
object then stores that reference in its own variable called_stage
. This means that whenever the code in theLevelOne
class uses the variable_stage
, it can access the main stage in theMonsterMayhem
application class.
Figure 8-4
illustrates how the reference to the stage gets passed from theMonsterMayhem
application class to the_levelOne
object.
Figure 8-4.
The application class sends LevelOne a reference to the stage.
LevelOne
needs this reference to the stage because it has to attach thekeyDownHandler
andKeyUpHandler
to it. Here's how it does this using its new_stage
variable:
_stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDownHandler);
_stage.addEventListener(KeyboardEvent.KEY_UP, keyUpHandler);
You'll see this in the context of the full class soon.
There's one additional technical problem that comes up if you give classes access to the stage like you've just done. You have to make sure that those objects are actually on the stage before you try to access the stage object. If you create an object with thenew
keyword and that object tries to access the stage before it's been added to the stage by the application class with thestage.addChild()
method, you'll see this error message:
Error #1009: Cannot access a property or
method of a null object reference
If you're adding objects to the game, there's a good chance that many of those objects will need to access the stage as soon as they're created. They might need to do this to set stage boundaries or access the properties of other objects that are also on the stage. If they can't find thestage
object because they haven't been added to the stage yet, AS3.0's compiler will “throw its hands in the air” and you'll see the preceding error message.
To solve this problem, you need to initialize objects only when they've been added to the stage, not before. There's a special event listener calledADDED_TO_STAGE
that does just that. It waits for the object to be added to the stage and runs any code that you need it to.
TheLevelOne
class needs access to the stage to attach the keyboard event listeners that move the game character, so it needs to wait until it's been added to the stage before trying to do that. Here's how it uses theADDED_TO_STAGE
event listener (the important code is highlighted):
public function LevelOne(stage:Object)
{
_stage = stage;
this.addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
}
private function addedToStageHandler(event:Event):void
{
startGame();
this.removeEventListener
(Event.ADDED_TO_STAGE, addedToStageHandler);
}
private function startGame():void
{
//All the directives that setup and
//initialize the game
}
As soon as aLevelOne
object is created by the application class, the constructor method sets up theADDED_TO_STAGE
listener.
this.addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
It waits until the object appears on the stage. As soon as it does, it runs theaddedToStageHandler
. This has the job of calling thestartGame
method and also of removing the listener, like this:
private function addedToStageHandler(event:Event):void
{
startGame();
this.removeEventListener
(Event.ADDED_TO_STAGE, addedToStageHandler);
}
ThestartGame
method then runs all the directives that create the game objects and initializes the game variables.
This is a bit of a technical hurdle to have to jump through. Yes, it's a full-blown drag! But it's unfortunately essential if any class that isn't the application class needs to refer to or use thestage
object.
Figure 8-5
illustrates how theADDED_TO_STAGE
listener jumpstarts the class's code. And luckily, this is all very routine code that you don't really waste any excess brainpower thinking too much about. Just blindly follow this example as-is to use it in your own games and don't fret about it too much.
Figure 8-5.
Wait until an object is on the stage before it's initialized.
TheADDED_TO_STAGE
listener has a companion calledREMOVED_FROM_STAGE
. TheREMOVED_FROM_STAGE
event listener fires when an object is removed from the stage. Very importantly,REMOVED_FROM_STAGE
allows you to remove any other event listeners that might be running on an object. This is particularly important forENTER_FRAME
or timer events. One of AS3.0's little quirks is that even after objects are taken off the stage usingremoveChild
, theirENTER_FRAME
events will still run silently in the background. If anENTER_FRAME
event is running and trying to reference objects that no longer exist, it will generate a torrent of error messages. To prevent this, you can use theREMOVED_FROM_STAGE
event to remove the object'senterFrameHandler
event listener when the object itself is removed, if your code doesn't remove it in any other way.
Here's the format for using aREMOVED_FROM_STAGE
event listener. First, add it in the class's constructor method:
public function ClassConstructorMethod()
{
//... any class initialization code
//or other listeners...
this.addEventListener
(Event.REMOVED_FROM_STAGE, removedFromStageHandler);
}
Next, use theremovedFromStageHandler
to run any code that should be run when the object is removed from the stage. Very importantly, you also need to remove theremovedFromStageHandler
itself.
private function removedFromStageHandler(event:Event):void
{
//... any directives you want to run when the
//object is removed from the stage
this.removeEventListener
(Event.ENTER_FRAME, enterFrameHandler);
this.removeEventListener
(Event.REMOVED_FROM_STAGE, removedFromStageHandler);
}
Although manually removingENTER_FRAME
events is required, removing other event listeners manually is optional. Flash still deletes objects even if they have listeners on them in most cases. However, just to make sure, it's considered best practice to manually remove them so you know with absolute certainty that that there won't be any lingering code running in the background after the object is gone.
All the game logic for the first level is in theLevelOne
class. It does what the application classes were doing in previous chapters. So, except for some of the new technical details that were just covered, much of the code will be very familiar. But the code is doing a lot of new things as well:
levelComplete
event when the level is finished so that the application class knows it has to load level two.You'll look at each of these new features in detail, one at a time. But just so that you have all the code in one place as a reference, here's the entire code listing forLevelOne
:
package
{
import flash.display.Sprite;
import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.events.TimerEvent;
import flash.ui.Keyboard;
import flash.utils.Timer;
public class LevelOne extends Sprite
{
//Declare the variables to hold
//the game objects
private var _character:Character;
private var _background:Background;
private var _gameOver:GameOver;
private var _monster1:Monster;
private var _monster2:Monster;
private var _star:Star;
private var _levelWinner:String;
//The timers
private var _monsterTimer:Timer;
private var _gameOverTimer:Timer;
//A variable to store the reference
//to the stage from the application class
private var _stage:Object;
public function LevelOne(stage:Object)
{
_stage = stage;
this.addEventListener
(Event.ADDED_TO_STAGE, addedToStageHandler);
}
private function addedToStageHandler(event:Event):void
{
startGame();
this.removeEventListener
(Event.ADDED_TO_STAGE, addedToStageHandler);
}
private function startGame():void
{
//Create the game objects
_character = new Character();
_star = new Star();
_background = new Background();
_monster1 = new Monster();
_monster2 = new Monster();
_gameOver = new GameOver();
//Add the game objects to the stage
addGameObjectToLevel(_background, 0, 0);
addGameObjectToLevel(_monster1, 400, 150);
addGameObjectToLevel(_monster2, 150, 150);
addGameObjectToLevel(_character, 250, 300);
addGameObjectToLevel(_star, 250, 300);
_star.visible = false;
addGameObjectToLevel(_gameOver, 140, 130);
//Initialize the monster timer
_monsterTimer = new Timer(1000);
_monsterTimer.addEventListener
(TimerEvent.TIMER, monsterTimerHandler);
_monsterTimer.start();
//Event listeners
_stage.addEventListener
(KeyboardEvent.KEY_DOWN, keyDownHandler);
_stage.addEventListener
(KeyboardEvent.KEY_UP, keyUpHandler);
this.addEventListener
(Event.ENTER_FRAME, enterFrameHandler);
}
private function enterFrameHandler(event:Event):void
{
//Move the game character and
//check its stage boundaries
_character.x += _character.vx;
_character.y += _character.vy;
checkStageBoundaries(_character);
//Move the monsters and
//check their stage boundaries
if(_monster1.visible)
{
_monster1.x += _monster1.vx;
_monster1.y += _monster1.vy;
checkStageBoundaries(_monster1);
}
if(_monster2.visible)
{
_monster2.x += _monster2.vx;
_monster2.y += _monster2.vy;
checkStageBoundaries(_monster2);
}
//If the star has been launched,
//move it, check its stage
//boundaries and collisions
//with the monsters
if(_star.launched)
{
//If it has been launched,
//make it visible
_star.visible = true;
//Move it
_star.y -= 3;
_star.rotation += 5;
//Check its stage boundaries
checkStarStageBoundaries(_star);
//Check for collisions with the monsters
starVsMonsterCollision(_star, _monster1);
starVsMonsterCollision(_star, _monster2);
}
else
{
_star.visible = false;
}
//Collision detection between the
//character and monsters
characterVsMonsterCollision(_character, _monster1);
characterVsMonsterCollision(_character, _monster2);
}
private function characterVsMonsterCollision
(character:Character, monster:Monster):void
{
if(monster.visible
&& character.hitTestObject(monster))
{
character.timesHit++;
checkGameOver();
}
}
private function starVsMonsterCollision
(star:Star, monster:Monster):void
{
if(monster.visible
&& star.hitTestObject(monster))
{
//Call the monster's openMouth
//method to make it open its mouth
monster.openMouth();
//Deactivate the star
star.launched = false;
//Add 1 to the monster's timesHit variable
monster.timesHit++;
//Has the monster been hit 3 times?
if(monster.timesHit == 3)
{
//call the killMonster
//method
killMonster(monster);
//Check to see if the
//game is over
checkGameOver();
}
}
}
private function killMonster(monster:Monster):void
{
//Make the monster invisible
monster.visible = false;
//Create a new explosion object
//and add it to the stage
var explosion:Explosion = new Explosion();
this.addChild(explosion);
//Center the explosion over
//the monster
explosion.x = monster.x -21;
explosion.y = monster.y -18;
//Call the explosion's
//explode method
explosion.explode();
}
private function checkGameOver():void
{
if(_monster1.timesHit == 3
&& _monster2.timesHit == 3)
{
_levelWinner = "character"
_gameOverTimer = new Timer(2000);
_gameOverTimer.addEventListener
(TimerEvent.TIMER, gameOverTimerHandler);
_gameOverTimer.start();
_monsterTimer.removeEventListener
(TimerEvent.TIMER, monsterTimerHandler);
this.removeEventListener
(Event.ENTER_FRAME, enterFrameHandler);
}
if(_character.timesHit == 1)
{
_levelWinner = "monsters"
_character.alpha = 0.5;
_gameOverTimer = new Timer(2000);
_gameOverTimer.addEventListener
(TimerEvent.TIMER, gameOverTimerHandler);
_gameOverTimer.start();
_monsterTimer.removeEventListener
(TimerEvent.TIMER, monsterTimerHandler);
this.removeEventListener
(Event.ENTER_FRAME, enterFrameHandler);
}
}
private function checkStageBoundaries(gameObject:Sprite):void
{
if (gameObject.x < 50)
{
gameObject.x = 50;
}
if (gameObject.y < 50)
{
gameObject.y = 50;
}
if (gameObject.x + gameObject.width > _stage.stageWidth - 50)
{
gameObject.x = _stage.stageWidth - gameObject.width - 50;
}
if (gameObject.y + gameObject.height
> _stage.stageHeight - 50)
{
gameObject.y = _stage.stageHeight - gameObject.height - 50;
}
}
private function checkStarStageBoundaries(star:Star):void
{
if (star.y < 50)
{
star.launched = false;
}
}
private function monsterTimerHandler(event:TimerEvent):void
{
changeMonsterDirection(_monster1);
changeMonsterDirection(_monster2);
}
private function changeMonsterDirection(monster:Monster):void
{
var randomNumber:int = Math.ceil(Math.random() * 4);
if(randomNumber == 1)
{
//Right
monster.vx = 1;
monster.vy = 0;
}
else if (randomNumber == 2)
{
//Left
monster.vx = -1;
monster.vy = 0;
}
else if(randomNumber == 3)
{
//Up
monster.vx = 0;
monster.vy = -1;
}
else
{
//Down
monster.vx = 0;
monster.vy = 1;
}
}
private function gameOverTimerHandler(event:TimerEvent):void
{
if(_levelWinner == "character")
{
if(_gameOverTimer.currentCount == 1)
{
_gameOver.levelComplete.visible = true;
}
if(_gameOverTimer.currentCount == 2)
{
_gameOverTimer.reset();
_gameOverTimer.removeEventListener
(TimerEvent.TIMER, gameOverTimerHandler);
dispatchEvent(new Event("levelOneComplete", true));
}
}
if(_levelWinner == "monsters")
{
_gameOver.youLost.visible = true;
_gameOverTimer.removeEventListener
(TimerEvent.TIMER, gameOverTimerHandler);
}
}
private function keyDownHandler(event:KeyboardEvent):void
{
if (event.keyCode == Keyboard.LEFT)
{
_character.vx = -5;
}
else if (event.keyCode == Keyboard.RIGHT)
{
_character.vx = 5;
}
else if (event.keyCode == Keyboard.UP)
{
_character.vy = -5;
}
else if (event.keyCode == Keyboard.DOWN)
{
_character.vy = 5;
}
if(event.keyCode == Keyboard.SPACE)
{
if(!_star.launched)
{
_star.x = _character.x + _character.width / 2;
_star.y = _character.y + _character.width / 2;
_star.launched = true;
}
}
}
private function keyUpHandler(event:KeyboardEvent):void
{
if (event.keyCode == Keyboard.LEFT
|| event.keyCode == Keyboard.RIGHT)
{
_character.vx = 0;
}
else if (event.keyCode == Keyboard.DOWN
|| event.keyCode == Keyboard.UP)
{
_character.vy = 0;
}
}
private function addGameObjectToLevel
(gameObject:Sprite, xPos:int, yPos:int):void
{
this.addChild(gameObject);
gameObject.x = xPos;
gameObject.y = yPos;
}
}
}