Create from Scratch a Away3D Shoot’em’Up Game: Part 3
by 15 July, 2009 12:50 pm1
After you have created the 3D level, in the part 3 of this Away3D series, we will show you how to add the player spacecraft and enemies.
Result
Requirements
Pre-Requisites
You should read the previous articles in this series.
Adding a player
As before the ApplicationManager will be used to create both the player and the enemies that the player will face. This is done in the same fashion as the BackgroundPlane and BackgroundBuildings that were added in the last article.
ApplicationManager.as
package { import away3d.primitives.Cube; import mx.core.Application; public class ApplicationManager extends BaseObject { protected static const TIME_BETWEEN_BUILDINGS:Number = 2; protected static const TIME_BETWEEN_ENEMIES:Number = 1; protected var timeToNextBuilding:Number = 0; protected var timeToNextEnemy:Number = TIME_BETWEEN_ENEMIES; public function ApplicationManager() { super(); } public function startupApplicationManager(engineManager:EngineManager):ApplicationManager { this.startupBaseObject(engineManager); return this; } public function startLevel1():void { timeToNextBuilding = 0; new BackgroundPlane().startupBackgroundPlane(engineManager); new Player().startupPlayer(engineManager); // prepopulate the city for (var i:int = 0; i < 5; ++i) { var building:BackgroundBuilding = new BackgroundBuilding(); building.startupBackgroundBuilding(engineManager); building.enterFrame((i + 1) * 2); } } public override function shutdown():void { super.shutdown(); } public override function enterFrame(dt:Number):void { timeToNextBuilding -= dt; if (timeToNextBuilding <= 0) { timeToNextBuilding = TIME_BETWEEN_BUILDINGS; new BackgroundBuilding().startupBackgroundBuilding(engineManager); } timeToNextEnemy -= dt; if (timeToNextEnemy <= 0) { timeToNextEnemy = TIME_BETWEEN_ENEMIES; new Enemy().startupBasicEnemy(engineManager); } } } }
As you can see the player is represented by the new Player class, a new instance of which is created in the startLevel function.
The enemies are represented by the new Enemy class. Instances of the Enemy class are created at regular intervals, just like the BackgroundBuildings, in the enterFrame function.
Player.as
package { import away3d.core.math.Number3D; import away3d.events.MouseEvent3D; import away3d.primitives.*; import flash.events.*; import flash.geom.*; import flash.media.*; import mx.core.*; public class Player extends MeshObject { protected static const PLAYER_X_LIMIT:Number = 52; protected static const PLAYER_Y_LIMIT:Number = 38; protected static const PLAYER_WIDTH:Number = 16; protected static const PLAYER_HEIGHT:Number = 5; protected static const COLLISION_PLANE_WIDTH:Number = 150; protected static const COLLISION_PLANE_HEIGHT:Number = 100; protected var collisionPlane:MeshObject = null; public function Player() { super(); } public function startupPlayer(engineManager:EngineManager):void { var plane:Plane = new Plane( {material:ResourceManager.Player_Tex, width:PLAYER_WIDTH, height:PLAYER_HEIGHT, yUp:false}); super.startupMeshObject(engineManager, plane); var collisionPlaneMesh:Plane = new Plane( {material:new NullMaterial(), width:COLLISION_PLANE_WIDTH, height:COLLISION_PLANE_HEIGHT, yUp:false}); collisionPlane = new MeshObject().startupMeshObject(engineManager, collisionPlaneMesh); collisionPlaneMesh.addEventListener(MouseEvent3D.MOUSE_MOVE, this.mouseMove3D); } override public function shutdown():void { collisionPlane.model.removeEventListener(MouseEvent3D.MOUSE_MOVE, this.mouseMove3D); collisionPlane.shutdown(); collisionPlane = null; super.shutdown(); } override public function enterFrame(dt:Number):void { super.enterFrame(dt); if (this.model.x > PLAYER_X_LIMIT) this.model.x = PLAYER_X_LIMIT; if (this.model.x < -PLAYER_X_LIMIT) this.model.x = -PLAYER_X_LIMIT; if (this.model.y > PLAYER_Y_LIMIT) this.model.y = PLAYER_Y_LIMIT; if (this.model.y < -PLAYER_Y_LIMIT) this.model.y = -PLAYER_Y_LIMIT; } public function mouseMove3D(event:MouseEvent3D):void { if (this.model) { this.model.x = event.sceneX; this.model.y = event.sceneY; } } } }
The Player class extends MeshObject, because it will display a 3D model in the scene. In this case though the 3D model is just a textured plane. This is because of the performance limitations of the Flash runtime. In order to maintain a decent framerate I choose to implement the player (and the enemies) essentially as 2D objects. While you might get away with using 3D models that have only a few hundred triangles, even these models would soon slow the game down if you have a few on the screen at once.
public function startupPlayer(engineManager:EngineManager):void { var plane:Plane = new Plane( {material:ResourceManager.Player_Tex, width:PLAYER_WIDTH, height:PLAYER_HEIGHT, yUp:false}); super.startupMeshObject(engineManager, plane); var collisionPlaneMesh:Plane = new Plane( {material:new NullMaterial(), width:COLLISION_PLANE_WIDTH, height:COLLISION_PLANE_HEIGHT, yUp:false}); collisionPlane = new MeshObject().startupMeshObject(engineManager, collisionPlaneMesh); collisionPlaneMesh.addEventListener(MouseEvent3D.MOUSE_MOVE, this.mouseMove3D); }
The startupPlayer function is where the player is constructed. We create a new Plane and then pass this to the MeshObject startupMeshObject function. The only special thing we do here is specify the yUp property of the Plane to be false, which causes the Plane to be constructed in the X/Y axis (and therefore facing the camera).
The Player will be moved around on the screen so it is always under the mouse pointer. This sort of control scheme will be immediately obvious to anyone playing the game, but it does raise the question: how do you find the position of the mouse cursor in a 3D world? There are two ways. The hard way is to calculate and project a ray into the world and then find a collision with a plane. You can find this procedure detailed here, but I warn you now it’s not pretty. The easy way is to use the mouse events built into Away3D.
Basically the procedure is this: you create a Plane, texture it with an invisible texture, attach a function to the MouseEvent3D.MOUSE_MOVE event, and then read the scene coordinates from the supplied MouseEvent3D object passed to the event listener.
var collisionPlaneMesh:Plane = new Plane( {material:new NullMaterial(), width:COLLISION_PLANE_WIDTH, height:COLLISION_PLANE_HEIGHT, yUp:false});
The first three of these steps is done here in the startupPlayer function. We create a new Plane instance and set it’s material to a new NullMaterial instance. The Nullmaterial is a class that extends the ITriangleMaterial interface (as we will see later), and it’s only purpose is to provide a material that doesn’t draw anything to the screen. This is necessary because mouse events don’t appear to be triggered on models whose textures have an alpha of 0 (or set to transparent).
collisionPlane = new MeshObject().startupMeshObject(engineManager, collisionPlaneMesh);
Once the Plane has been created we assign it to a new MeshObject instance so it will be placed in the scene.
collisionPlaneMesh.addEventListener(MouseEvent3D.MOUSE_MOVE, this.mouseMove3D);
We then attach the mouseMove3D function to the MouseEvent3D.MOUSE_MOVE event. This causes the mouseMove3D function to be called when the mouse moves over the Plane.
public function mouseMove3D(event:MouseEvent3D):void { if (this.model) { this.model.x = event.sceneX; this.model.y = event.sceneY; } }
Now in the mouseMove3D function we take the sceneX and sceneY properties of the supplied MouseEvent3D object and assign these to the position of the Plane that represents the player.
override public function enterFrame(dt:Number):void { super.enterFrame(dt); if (this.model.x > PLAYER_X_LIMIT) this.model.x = PLAYER_X_LIMIT; if (this.model.x < -PLAYER_X_LIMIT) this.model.x = -PLAYER_X_LIMIT; if (this.model.y > PLAYER_Y_LIMIT) this.model.y = PLAYER_Y_LIMIT; if (this.model.y < -PLAYER_Y_LIMIT) this.model.y = -PLAYER_Y_LIMIT; }
The last step we need to do is to make sure the player never leaves the screen. The screen limits are defined in the PLAYER_X_LIMIT and PLAYER_Y_LIMIT
constants. These values were manually picked through trial and error rather than being calculated.
Enemy.as
package { import away3d.materials.BitmapMaterial; import away3d.primitives.Plane; public class Enemy extends MeshObject { protected static const ENEMY_X_LIMIT:Number = 70; protected static const ENEMY_Y_LIMIT:Number = 60; protected static const ENEMY_X_START:Number = 65; protected static const ENEMY_Y_START:Number = 55; protected static const ENEMY_WIDTH:Number = 16; protected static const ENEMY_HEIGHT:Number = 5; protected static const BASIC_ENEMY_SPEED:Number = 50; protected var logic:Function = null; public function Enemy() { super(); } public override function shutdown():void { super.shutdown(); this.logic = null; } protected function startupEnemy(engineManager:EngineManager, material:BitmapMaterial):Enemy { var plane:Plane = new Plane( {material:material, width:ENEMY_WIDTH, height:ENEMY_HEIGHT, yUp:false}); super.startupMeshObject(engineManager, plane); return this; } public function startupBasicEnemy(engineManager:EngineManager):Enemy { this.startupEnemy(engineManager, ResourceManager.Enemy_Tex); this.logic = basicEnemyLogic; this.model.x = ENEMY_X_LIMIT; this.model.y = MathUtils.randRange(-ENEMY_Y_START, ENEMY_Y_START); return this; } public override function enterFrame(dt:Number):void { if (logic != null) logic(dt); if (this.model.x > ENEMY_X_LIMIT || this.model.x < -ENEMY_X_LIMIT || this.model.y > ENEMY_Y_LIMIT || this.model.y < -ENEMY_Y_LIMIT) this.shutdown(); } protected function basicEnemyLogic(dt:Number):void { this.model.x -= BASIC_ENEMY_SPEED * dt; } } }
Like the Player class, the Enemy class also extends MeshObject. And like the Player, the Enemies are also represented by a Plane rather than a full 3D model.
The Enemy class has two startup functions. This is because there will eventually be a number of different types of enemies, each with different materials and behaviours. The startupEnemy function provides the common functionality required to construct the Enemy class, but it is the other startup functions, startupBasicEnemy in this case, that define the type the enemy.
The behaviour of the Enemy class is defined by a function referenced by the logic property. The startupBasicEnemy function has assigned the basicEnemyLogic function to the logic property. Then in the enterFrame function the function referenced by the logic property is called. You can think of the logic property as being the Enemy’s brain – it defines what the Enemy will do during the render loop. In this the basicEnemyLogic function simply moves the enemy across the screen in a straight line, but nearly any kind of behaviour could be implemented here.
The last thing we need to do is remove the Enemy from the game when it has moved off the screen. Just like the Player some screen limits have been defined (the ENEMY_X_LIMIT and ENEMY_Y_LIMIT constants), and when the Enemy moves beyond these limits the shutdown function is called.
NullMaterial.as
package { import away3d.core.base.Object3D; import away3d.containers.View3D; import away3d.core.draw.DrawTriangle; import away3d.materials.ITriangleMaterial; public class NullMaterial implements ITriangleMaterial { public function NullMaterial() { } public function get visible():Boolean { return true; } public function renderTriangle(tri:DrawTriangle):void { } public function updateMaterial(source:Object3D, view:View3D):void { } public function addOnMaterialUpdate(listener:Function):void { } public function removeOnMaterialUpdate(listener:Function):void { } } }
As mentioned earlier the Nullmaterial class allows a model to be added to the scene, not be visible, but still receive mouse events. It is basically an empty implementation of the ITriangleMaterial interface.
So we now have a player that can be controlled by the mouse and a stream of enemies. Currently though there is no interaction between the two: the player can fly straight through an enemy. We will see how to add collision detection in the next article.