package
{
import flash.display.*;
import flash.events.*;
import flash.utils.*;
import mx.collections.*;
import mx.core.*;
public class GameObjectManager
{
// double buffer
public var backBuffer:BitmapData;
// colour to use to clear backbuffer with
public var clearColor:uint = 0xFF0043AB;
// static instance
protected static var instance:GameObjectManager = null;
// the last frame time
protected var lastFrame:Date;
// a collection of the GameObjects
protected var gameObjects:ArrayCollection = new ArrayCollection();
// a collection where new GameObjects are placed, to avoid adding items
// to gameObjects while in the gameObjects collection while it is in a loop
protected var newGameObjects:ArrayCollection = new ArrayCollection();
// a collection where removed GameObjects are placed, to avoid removing items
// to gameObjects while in the gameObjects collection while it is in a loop
protected var removedGameObjects:ArrayCollection = new ArrayCollection();
protected var collisionMap:Dictionary = new Dictionary();
static public function get Instance():GameObjectManager
{
if ( instance == null )
instance = new GameObjectManager();
return instance;
}
public function GameObjectManager()
{
if ( instance != null )
throw new Error( "Only one Singleton instance should be instantiated" );
backBuffer = new BitmapData(Application.application.width, Application.application.height, false);
}
public function startup():void
{
lastFrame = new Date();
}
public function shutdown():void
{
shutdownAll();
}
public function enterFrame():void
{
// Calculate the time since the last frame
var thisFrame:Date = new Date();
var seconds:Number = (thisFrame.getTime() - lastFrame.getTime())/1000.0;
lastFrame = thisFrame;
removeDeletedGameObjects();
insertNewGameObjects();
Level.Instance.enterFrame(seconds);
checkCollisions();
// now allow objects to update themselves
for each (var gameObject:GameObject in gameObjects)
{
if (gameObject.inuse)
gameObject.enterFrame(seconds);
}
drawObjects();
}
public function click(event:MouseEvent):void
{
for each (var gameObject:GameObject in gameObjects)
{
if (gameObject.inuse) gameObject.click(event);
}
}
public function mouseDown(event:MouseEvent):void
{
for each (var gameObject:GameObject in gameObjects)
{
if (gameObject.inuse) gameObject.mouseDown(event);
}
}
public function mouseUp(event:MouseEvent):void
{
for each (var gameObject:GameObject in gameObjects)
{
if (gameObject.inuse) gameObject.mouseUp(event);
}
}
public function mouseMove(event:MouseEvent):void
{
for each (var gameObject:GameObject in gameObjects)
{
if (gameObject.inuse) gameObject.mouseMove(event);
}
}
protected function drawObjects():void
{
backBuffer.fillRect(backBuffer.rect, clearColor);
// draw the objects
for each (var gameObject:GameObject in gameObjects)
{
if (gameObject.inuse)
gameObject.copyToBackBuffer(backBuffer);
}
}
public function addGameObject(gameObject:GameObject):void
{
newGameObjects.addItem(gameObject);
}
public function removeGameObject(gameObject:GameObject):void
{
removedGameObjects.addItem(gameObject);
}
protected function shutdownAll():void
{
// don't dispose objects twice
for each (var gameObject:GameObject in gameObjects)
{
var found:Boolean = false;
for each (var removedObject:GameObject in removedGameObjects)
{
if (removedObject == gameObject)
{
found = true;
break;
}
}
if (!found)
gameObject.shutdown();
}
}
protected function insertNewGameObjects():void
{
for each (var gameObject:GameObject in newGameObjects)
{
for (var i:int = 0; i < gameObjects.length; ++i)
{
if (gameObjects.getItemAt(i).zOrder > gameObject.zOrder ||
gameObjects.getItemAt(i).zOrder == -1)
break;
}
gameObjects.addItemAt(gameObject, i);
}
newGameObjects.removeAll();
}
protected function removeDeletedGameObjects():void
{
// insert the object acording to it's z position
for each (var removedObject:GameObject in removedGameObjects)
{
var i:int = 0;
for (i = 0; i < gameObjects.length; ++i)
{
if (gameObjects.getItemAt(i) == removedObject)
{
gameObjects.removeItemAt(i);
break;
}
}
}
removedGameObjects.removeAll();
}
public function addCollidingPair(collider1:String, collider2:String):void
{
if (collisionMap[collider1] == null)
collisionMap[collider1] = new Array();
if (collisionMap[collider2] == null)
collisionMap[collider2] = new Array();
collisionMap[collider1].push(collider2);
collisionMap[collider2].push(collider1);
}
protected function checkCollisions():void
{
for (var i:int = 0; i < gameObjects.length; ++i)
{
var gameObjectI:GameObject = gameObjects.getItemAt(i) as GameObject;
for (var j:int = i + 1; j < gameObjects.length; ++j)
{
var gameObjectJ:GameObject = gameObjects.getItemAt(j) as GameObject;
// early out for non-colliders
var collisionNameNotNothing:Boolean = gameObjectI.collisionName != CollisionIdentifiers.NONE;
// objects can still exist in the gameObjects collection after being disposed, so check
var bothInUse:Boolean = gameObjectI.inuse && gameObjectJ.inuse;
// make sure we have an entry in the collisionMap
var collisionMapEntryExists:Boolean = collisionMap[gameObjectI.collisionName] != null;
// make sure the two objects are set to collide
var testForCollision:Boolean = collisionMapEntryExists && collisionMap[gameObjectI.collisionName]. indexOf(gameObjectJ.collisionName) != -1
if ( collisionNameNotNothing &&
bothInUse &&
collisionMapEntryExists &&
testForCollision)
{
if (gameObjectI.CollisionArea. intersects(gameObjectJ.CollisionArea))
{
gameObjectI.collision(gameObjectJ);
gameObjectJ.collision(gameObjectI);
}
}
}
}
}
}
}
We have added one property to GameObjectManager: collisionMap. This is a dictionary where the key is the collision name of a GameObject, and the value is an array of the collision names of all the other GameObjects that it will collide with. Once populated it will look something like this:
Key: "Player" Value: {"Enemy", "EnemyWeapon", "Powerup"}
Key: "Enemy" Value: {"Player", "PlayerWeapon"}
Key: "PlayerWeapon" Value: {"Enemy"}
Key: "Powerup" Value: {"Player"}
and so on.
The addCollidingPair function is used to populate the collisionMap dictionary. We will call this from in the main.mxml file in the creationComplete function.
The checkCollision function is where the collisions are actually detected and the corresponding GameObjects notified. It looks complicated, but is quite simple.
It starts by looping through the gameObjects collection (which contains all the active GameObjects) twice, and is structured in such as way as to compare each GameObject to every other GameObject once. It then does a number of checks:
- Is the collisionName of either GameObject "None"? Both GameObjects need a collisionName that is not "None" to participate in a collision.
- Are both of the GameObjects inuse (i.e. are they active in the game). This should always be the case, but it doesn't hurt to check.
- Are the collisionNames of the GameObjects registered as being colliders in the collisionMap? The purpose of the collisionMap is to determine which GameObject will collide.
If these few checks are true then we use the intersects function of a rectangle to see if the GameObjects are actually colliding. If they are then they are notified through their collision function.
As I mentioned earlier in the article collision detection is a subject that has entire books devoted to it. There are many clever ways to optimize a collision detection system which we have ignored. What we have here is a simple, brute force method of checking for collisions. It may not be a shining example of an opimized collision detection system, but it works because we will only have maybe two dozen GameObjects on the screen at any one point.
In order for any collisions to be detected at all we need to make a few calls to the addCollidingPair function. These will be made in the creationComplete function of our Application object. Lets look at those changes now.
main.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute"
width="600"
height="400"
frameRate="100"
creationComplete="creationComplete()"
enterFrame="enterFrame(event)"
click="click(event)"
mouseDown="mouseDown(event)"
mouseUp="mouseUp(event)"
mouseMove="mouseMove(event)"
currentState="MainMenu">
<mx:states>
<mx:State
name="Game"
enterState="enterGame(event)"
exitState="exitGame(event)">
</mx:State>
<mx:State name="MainMenu">
<mx:AddChild relativeTo="{myCanvas}" position="lastChild">
<mx:Button x="525" y="368" label="Start" id="btnStart" click="startGameClicked(event)"/>
</mx:AddChild>
</mx:State>
</mx:states>
<mx:Canvas x="0" y="0" width="100%" height="100%" id="myCanvas"/>
<mx:Script>
<![CDATA[
protected var inGame:Boolean = false;
public function creationComplete():void
{
GameObjectManager.Instance. addCollidingPair(CollisionIdentifiers.PLAYER, CollisionIdentifiers.ENEMY);
GameObjectManager.Instance. addCollidingPair(CollisionIdentifiers.ENEMY, CollisionIdentifiers.PLAYERWEAPON);
GameObjectManager.Instance. addCollidingPair(CollisionIdentifiers.PLAYER, CollisionIdentifiers.ENEMYWEAPON);
}
public function enterFrame(event:Event):void
{
if (inGame)
{
GameObjectManager.Instance.enterFrame();
myCanvas.graphics.clear();
myCanvas.graphics. beginBitmapFill(GameObjectManager.Instance.backBuffer, null, false, false);
myCanvas.graphics.drawRect(0, 0, this.width, this.height);
myCanvas.graphics.endFill();
}
}
private function click(event:MouseEvent):void
{
GameObjectManager.Instance.click(event);
}
private function mouseDown(event:MouseEvent):void
{
GameObjectManager.Instance.mouseDown(event);
}
private function mouseUp(event:MouseEvent):void
{
GameObjectManager.Instance.mouseUp(event);
}
private function mouseMove(event:MouseEvent):void
{
GameObjectManager.Instance.mouseMove(event);
}
protected function startGameClicked(event:Event):void
{
currentState = "Game"
}
protected function enterGame(event:Event):void
{
Mouse.hide();
GameObjectManager.Instance.startup();
Level.Instance.startup();
inGame = true;
}
protected function exitGame(event:Event):void
{
Mouse.show();
Level.Instance.shutdown();
GameObjectManager.Instance.shutdown();
inGame = false;
}
]]>
</mx:Script>
</mx:Application>
As you can see specifying that two GameObjects will collide with each other only requires one call to the GameObjectManager addCollidingPair function. Here we have specified that the player will collide with an enemy, the enemies will collide with the players weapons, and that the player will collide with the enemies weapons.
So now that we have collisions being detected we need to update the code for the Player, Weapon and Enemy classes to set their collisionName and to react when a detection has been found. Lets look at the Player class now.
Player.as
package
{
import flash.events.*;
import flash.geom.*;
import mx.core.*;
public class Player extends GameObject
{
protected static const TimeBetweenShots:Number = 0.25;
protected var shooting:Boolean = false;
protected var timeToNextShot:Number = 0;
public function Player()
{
}
public function startupPlayer():void
{
startupGameObject(ResourceManager.BrownPlaneGraphics, new Point(Application.application.width / 2, Application.application.height / 2), ZOrders.PlayerZOrder);
shooting = false;
timeToNextShot = 0;
this.collisionName = CollisionIdentifiers.PLAYER;
}
override public function shutdown():void
{
super.shutdown();
}
override public function enterFrame(dt:Number):void
{
super.enterFrame(dt);
timeToNextShot -= dt;
if (timeToNextShot <= 0 && shooting)
{
timeToNextShot = TimeBetweenShots;
var weapon:Weapon = Weapon.pool.ItemFromPool as Weapon;
weapon.startupBasicWeapon(
ResourceManager.TwoBulletsGraphics,
new Point(
position.x + graphics.bitmap.width / 2 - ResourceManager.TwoBulletsGraphics.bitmap.width / 2,
position.y - graphics.bitmap.height + ResourceManager.TwoBulletsGraphics.bitmap.height * 2),
150,
true);
}
}
override public function mouseMove(event:MouseEvent):void
{
// move player to mouse position
position.x = event.stageX;
position.y = event.stageY;
// keep player on the screen
if (position.x < 0)
position.x = 0;
if (position.x > Application.application.width - graphics.bitmap.width)
position.x = Application.application.width - graphics.bitmap.width;
if (position.y < 0)
position.y = 0;
if (position.y > Application.application.height - graphics.bitmap.height )
position.y = Application.application.height - graphics.bitmap.height ;
}
override public function mouseDown(event:MouseEvent):void
{
shooting = true;
}
override public function mouseUp(event:MouseEvent):void
{
shooting = false;
}
override public function collision(other:GameObject):void
{
Level.Instance.levelEnd = true;
this.shutdown();
}
}
}
As you can see there are two modifications to the Player class to accomodate the new collision detection system. The first is during the startup function where we set the collisionName. The second is the addition of the collision function, which will be called by the GameObjectManager when a collision has been found. Here we notify the Level that the level should end by setting the levelEnd to true (because the player has been killed), and we call shutdown to remove the Player from the game.
The changes to the Enemy and Weapon classes are exactly the same except for modifying the Levels levelEnd property in the collision function. For the sake of brevity I won't show the Enemy or Weapons classes - you can download the source at the end of the article and check out the changes for yourself.
The final change we have to make is to the Level class. Lets look at that now.
Level.as
package
{
import flash.events.*;
import flash.geom.*;
import flash.media.*;
import flash.net.*;
import flash.utils.*;
import mx.collections.ArrayCollection;
import mx.core.*;
public class Level
{
protected static var instance:Level = null;
protected static const TimeBetweenLevelElements:Number = 2;
protected static const TimeBetweenEnemies:Number = 3;
protected static const TimeBetweenClouds:Number = 2.5;
protected static const TimeToLevelEnd:Number = 2;
protected var timeToNextLevelElement:Number = 0;
protected var levelElementGraphics:ArrayCollection = new ArrayCollection();
protected var timeToNextEnemy:Number = 0;
protected var enemyElementGraphics:ArrayCollection = new ArrayCollection();
protected var timeToNextCloud:Number = 0;
protected var timeToLevelEnd:Number = 0;
public var levelEnd:Boolean = false;
static public function get Instance():Level
{
if ( instance == null )
instance = new Level();
return instance;
}
public function Level(caller:Function = null )
{
if ( Level.instance != null )
throw new Error( "Only one Singleton instance should be instantiated" );
levelElementGraphics. addItem(ResourceManager.SmallIslandGraphics);
levelElementGraphics. addItem(ResourceManager.BigIslandGraphics);
levelElementGraphics. addItem(ResourceManager.VolcanoIslandGraphics);
enemyElementGraphics. addItem(ResourceManager.SmallBluePlaneGraphics);
enemyElementGraphics. addItem(ResourceManager.SmallGreenPlaneGraphics);
enemyElementGraphics. addItem(ResourceManager.SmallWhitePlaneGraphics);
}
public function startup():void
{
timeToNextLevelElement = 0;
new Player().startupPlayer();
timeToLevelEnd = TimeToLevelEnd;
levelEnd = false;
}
public function shutdown():void
{
}
public function enterFrame(dt:Number):void
{
// add a background element
timeToNextLevelElement -= dt;
if (timeToNextLevelElement <= 0)
{
timeToNextLevelElement = TimeBetweenLevelElements;
var graphics:GraphicsResource = levelElementGraphics.getItemAt(MathUtils.randomInteger(0, levelElementGraphics.length)) as GraphicsResource;
var backgroundLevelElement:BackgroundLevelElement = BackgroundLevelElement.pool.ItemFromPool as BackgroundLevelElement;
backgroundLevelElement.startupBackgroundLevelElement(
graphics,
new Point(Math.random() * Application.application.width, -graphics.bitmap.height),
ZOrders.BackgoundZOrder,
50);
}
// add an emeny
timeToNextEnemy -= dt;
if (timeToNextEnemy <= 0)
{
timeToNextEnemy = TimeBetweenEnemies;
var enemygraphics:GraphicsResource = enemyElementGraphics.getItemAt(MathUtils.randomInteger(0, enemyElementGraphics.length)) as GraphicsResource;
var enemy:Enemy = Enemy.pool.ItemFromPool as Enemy;
enemy.startupBasicEnemy(
enemygraphics,
new Point(Math.random() * Application.application.width, -enemygraphics.bitmap.height),
55);
}
// add cloud
timeToNextCloud -= dt;
if (timeToNextCloud <= dt)
{
timeToNextCloud = TimeBetweenClouds;
var cloudBackgroundLevelElement:BackgroundLevelElement = BackgroundLevelElement.pool.ItemFromPool as BackgroundLevelElement;
cloudBackgroundLevelElement. startupBackgroundLevelElement(
ResourceManager.CloudGraphics,
new Point(Math.random() * Application.application.width, -ResourceManager.CloudGraphics.bitmap.height),
ZOrders.CloudsBelowZOrder,
75);
}
if (levelEnd)
timeToLevelEnd -= dt;
if (timeToLevelEnd <= 0)
Application.application.currentState = "MainMenu";
}
}
}
The changes to the Level class simply allow it to be notified when the Player dies through the levelEnd property. When set to true a count down beings in enterFrame using the timeToLevelEnd property, and when timeToLevelEnd reaches 0 the state is changed back to MainMenu which drops us back to the main menu screen.
Collision detection is essential in any action game. In this article we have implemented a simple, but effective, collision detection system which allows the player to now interact with other elements in the game, like being able to shoot the enemies. Unfortunately at the moment actualy destroying an enemy is quite unsatisfying because they just disappear. In part 7 of the series we will add some animations to the game, and with that some nice explosions.