Foundation Game Design with ActionScript 3.0, Second Edition (79 page)

BOOK: Foundation Game Design with ActionScript 3.0, Second Edition
4.65Mb size Format: txt, pdf, ePub

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 the
MonsterMayhem
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 the
LevelOne
and
LevelTwo
classes.

Programming the game

These are the classes that
MonsterMayhem
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
and
LevelTwo
contain all the game programming. All the other classes are game object classes. You'll first take a look at how
LevelOne
is added to the stage and then at how all the specific features of the game are programmed.

Giving LevelOne access to the stage

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 the
stage
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 the
LevelOne
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 the
stage
. The
_levelOne
object then stores that reference in its own variable called
_stage
. This means that whenever the code in the
LevelOne
class uses the variable
_stage
, it can access the main stage in the
MonsterMayhem
application class.
Figure 8-4
illustrates how the reference to the stage gets passed from the
MonsterMayhem
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 the
keyDownHandler
and
KeyUpHandler
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.

Making sure that LevelOne is actually on the stage

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 the
new
keyword and that object tries to access the stage before it's been added to the stage by the application class with the
stage.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 the
stage
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 called
ADDED_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.

The
LevelOne
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 the
ADDED_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 a
LevelOne
object is created by the application class, the constructor method sets up the
ADDED_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 the
addedToStageHandler
. This has the job of calling the
startGame
method and also of removing the listener, like this:

private function addedToStageHandler(event:Event):void
{
  startGame();
  this.removeEventListener
    (Event.ADDED_TO_STAGE, addedToStageHandler);
}

The
startGame
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 the
stage
object.
Figure 8-5
illustrates how the
ADDED_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.

The
ADDED_TO_STAGE
listener has a companion called
REMOVED_FROM_STAGE
. The
REMOVED_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 for
ENTER_FRAME
or timer events. One of AS3.0's little quirks is that even after objects are taken off the stage using
removeChild
, their
ENTER_FRAME
events will still run silently in the background. If an
ENTER_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 the
REMOVED_FROM_STAGE
event to remove the object's
enterFrameHandler
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 a
REMOVED_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 the
removedFromStageHandler
to run any code that should be run when the object is removed from the stage. Very importantly, you also need to remove the
removedFromStageHandler
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 removing
ENTER_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.

The LevelOne class

All the game logic for the first level is in the
LevelOne
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:

  • Letting the character fire star projectiles.
  • Making the monsters open their mouths when they're hit.
  • Adding explosions to the stage when the monsters are killed.
  • Displaying the “Level Complete” or “Game Over, You Lost” messages at the end of the level.
  • Generating a
    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 for
LevelOne
:

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;
    }
  }
}

Other books

Foxglove Summer by Ben Aaronovitch
Borden (Borden #1) by R. J. Lewis
Tempting Eden by Celia Aaron
A World of Other People by Steven Carroll
Murder by the Slice by Livia J. Washburn
A Witch's Path by N. E. Conneely
Atlantis by Robert Doherty
The Watson Brothers by Lori Foster