Create from Scratch a Away3D Shoot’em’Up Game: Part 2
by 8 July, 2009 1:45 pm6
In the part 1 of these tutorial series articles we created a framework that we could build off to start making the actual game. Now it’s time to create a 3D level for the player to fly through.
Result
Requirements
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 var timeToNextBuilding:Number = 0; public function ApplicationManager() { super(); } public function startupApplicationManager(engineManager:EngineManager):ApplicationManager { this.startupBaseObject(engineManager); return this; } public function startLevel1():void { new BackgroundPlane().startupBackgroundPlane(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 enterFrame(dt:Number):void { timeToNextBuilding -= dt; if (timeToNextBuilding <= 0) { timeToNextBuilding = TIME_BETWEEN_BUILDINGS; new BackgroundBuilding().startupBackgroundBuilding(engineManager); } } } }
The level that we will create is a 3D city that the player will fly over. A city is a good choice because buildings can be represented with simple rectangles (all of 12 triangles – 2 per side), and you want to keep your polygon count low in order to assure good performance on modest PC’s. Even though Flash 10 includes built in support for 3D effects, you still have to be
mindful of the polygon count in your scene. I found on my own PC that things started to slow down with just a few thousand triangles. That’s a pretty low number considering most PC’s can measure their ability to render triangles in the millions per second, but that is the downside to using Flash as a 3D platform.
In addition to the buildings that the player will be flying over, we will also create a panoramic background that will slowly scroll from right to left to give the illusion of movement. In our game the player won’t actually move further than a screen width – the appearance of movement will come from the buildings moving underneath the player and the scrolling of the background.
Last time the ApplicationManager didn’t do a whole lot – it just created a cube. The purpose of the ApplicationManager class is to create the objects that will appear in the final game, and this now includes the buildings that make up the level and the background image.
new BackgroundPlane().startupBackgroundPlane(engineManager);
The background image is represented by the new BackgroundPlane class. So we create and start up a new instance of the BackgroundPlane.
for (var i:int = 0; i < 5; ++i) { var building:BackgroundBuilding = new BackgroundBuilding(); building.startupBackgroundBuilding(engineManager); building.enterFrame((i + 1) * 2); }
The buildings are represented by the new BackgroundBuilding class. First we have to add a few initial buildings to the scene. After these initial buildings are created a new building is created every 2 seconds off to the right of the screen, and the BackgroundBuilding then slides itself across the screen until it is removed when it disappears off to the left. But if we don’t add some buildings initially the player will fly over empty space until the first building, created on the right hand side, slides across. In order to “trick” these initial backgroundBuildings into moving across the screen to fill up that initial void we call the enterFrame function with values between 2 and 10 seconds. This has the effect of causing the building move between 2 – 10 seconds across the screen – 10 seconds is about how long it takes for a building to move completely across from one side of the screen to the other.
public override function enterFrame(dt:Number):void { timeToNextBuilding -= dt; if (timeToNextBuilding <= 0) { timeToNextBuilding = TIME_BETWEEN_BUILDINGS; new BackgroundBuilding().startupBackgroundBuilding(engineManager); } }
Once the initial few buildings have been created we then need to create new buildings at regular intervals. The timeToNextBuilding property is a measure of the time since the last building has been created. This value decreases with every frame, until it reaches 0, at which point a new building is created and the timeToNextBuilding value is reset. In this way we get the impression that we are flying over an endless city.
BackgroundPlane.as
package { import away3d.materials.TransformBitmapMaterial; import away3d.primitives.Plane; public class BackgroundPlane extends MeshObject { protected static const ZPOS:Number = 800; protected static const WIDTH:Number = 2000; protected static const HEIGHT:Number = 400; protected static const SPEED:Number = 5; protected var material:TransformBitmapMaterial = null; public function BackgroundPlane() { super(); } public function startupBackgroundPlane(engineManager:EngineManager):BackgroundPlane { material = ResourceManager.Background1_Tex; super.startupMeshObject( engineManager, new Plane( {material:material, width:WIDTH, height:HEIGHT})); this.model.rotationX = 90; this.model.z = ZPOS; return this; } public override function shutdown():void { this.material = null; super.shutdown(); } public override function enterFrame(dt:Number):void { material.offsetX -= SPEED * dt; } } }
The BackgroundPlane simply holds a Plane that is rotated to face the camera, which is textured with a TransformBitmapMaterial. The reason why we use a TransformBitmapMaterial to texture the plane is because it has the ability to offset the texture. We use this offsetting ability to slide the texture across the plane, which gives the illusion of movement. The plane itself never moves, it’s only the texture which moves across the plane. You can see the xOffset property of the material being modified in the enterFrame function.
BackgroundBuilding.as
package { import away3d.materials.BitmapMaterial; import away3d.primitives.Cube; import away3d.primitives.data.CubeMaterialsData; public class BackgroundBuilding extends MeshObject { protected static const SPEED:Number = 50; protected static const XLIMIT:Number = 350; protected static const YPOSITION:Number = -120; protected static const MAXHEIGHT:Number = 300; protected static const MINHEIGHT:Number = 100; protected static const MINWIDTH:Number = 50; protected static const MAXWIDTH:Number = 75; protected static const MINDEPTH:Number = 200; protected static const MAXDEPTH:Number = 750; public function BackgroundBuilding() { super(); } public function startupBackgroundBuilding(engineManager:EngineManager):void { var material:BitmapMaterial = null; switch (MathUtils.randomInteger(0, 3)) { case 0: material = ResourceManager.Building1_Tex; break; case 1: material = ResourceManager.Building2_Tex; break; case 2: material = ResourceManager.Building3_Tex; break; } // the cube needs to have 6 textures defined var buildingMaterial:CubeMaterialsData = new CubeMaterialsData( {left:material, right:material, front:material, back:material, top:ResourceManager.Roof1_Tex}); super.startupMeshObject( engineManager, new Cube( {faces:buildingMaterial, width:MathUtils.randRange(MINWIDTH, MAXWIDTH), depth:MathUtils.randRange(MINWIDTH, MAXWIDTH), height:MathUtils.randRange(MINHEIGHT, MAXHEIGHT)})); this.model.z = MathUtils.randRange(MINDEPTH, MAXDEPTH); this.model.x = XLIMIT; this.model.y = YPOSITION; } public override function enterFrame(dt:Number):void { this.model.x -= SPEED * dt; if (this.model.x <= -XLIMIT) this.shutdown(); } } }
The BackgroundBuilding class holds a Cube which has been textured with 1 of 3 possible textures, randomly scaled (through the width, height and depth Cube properties), placed at a random distance from the camera (through the Cubes z property), and then positioned off to the right of the screen (through the Cubes x property).
Randomly scaling the cube mesh allows for some subtle variation between the buildings, and placing them at a random distance from the camera gives a sense of depth.
The building slides across the screen in the enterFrame function, where its position on the x axis is modified slightly every frame. When the building is no longer visible it simply calls its shutdown function, which removes it from the scene.
ResourceManage.as
package { import away3d.materials.BitmapMaterial; import away3d.materials.TransformBitmapMaterial; import mx.core.BitmapAsset; public class ResourceManager { [Embed (source="../media/building1.png")] public static const Building1:Class; public static const Building1_BitmapAsset:BitmapAsset = new Building1(); public static const Building1_Tex:BitmapMaterial = new BitmapMaterial(Building1_BitmapAsset.bitmapData); [Embed (source="../media/building2.png")] public static const Building2:Class; public static const Building2_BitmapAsset:BitmapAsset = new Building2(); public static const Building2_Tex:BitmapMaterial = new BitmapMaterial(Building2_BitmapAsset.bitmapData); [Embed (source="../media/building3.png")] public static const Building3:Class; public static const Building3_BitmapAsset:BitmapAsset = new Building3(); public static const Building3_Tex:BitmapMaterial = new BitmapMaterial(Building3_BitmapAsset.bitmapData); [Embed (source="../media/roof1.png")] public static const Roof1:Class; public static const Roof1_BitmapAsset:BitmapAsset = new Roof1(); public static const Roof1_Tex:BitmapMaterial = new BitmapMaterial(Roof1_BitmapAsset.bitmapData); [Embed (source="../media/background1.png")] public static const Background1:Class; public static const Background1_BitmapAsset:BitmapAsset = new Background1(); public static const Background1_Tex:TransformBitmapMaterial = new TransformBitmapMaterial(Background1_BitmapAsset.bitmapData); } }
Here we have used the ResourceManager to Embed a number of images, and then create the corresponding Away3D material classes from them. The buildings use the standard BitmapMaterial class, while the background texture applied by the BackgroundPlane class is the TransformBitmapMaterial class (because we need the xOffset property). You should also note that the repeat property of the TransformBitmapMaterial class needs to be set to true, which enables the texture to wrap around as the offset is increased.
If you have not seen the Embed keyword before, it is a neat feature of Flex that allows you to embed any file directly in the final SWF file. This means that your application or game can be made up of just one SWF file, as opposed to several resource files that have to be manually loaded.
As you can see, with the addition of two fairly simple classes (BackgroundPlane and BackgroundBuilding) and some minor modification of two existing classes (ResourceManager and
ApplicationManager) we have created a nice 3D city that our player will eventually fly across. The work we did in the last article to setup the framework now allows us to quite easily start adding these new features.
In the next article we will add the player and some enemies.