Build a Stage3D Shoot-'Em-Up: Terrain, Enemy AI, and Level Data
We're creating a high-performance 2D shoot-em-up using Flash's new hardware-accelerated Stage3D
rendering engine. In this part of the series we add new enemy movement modes, enemies that shoot back, and hand-crafted levels with background terrain.
Also available in this series:
- Build a Stage3D Shoot-’Em-Up: Sprite Test
- Build a Stage3D Shoot-’Em-Up: Interaction
- Build a Stage3D Shoot-’Em-Up: Explosions, Parallax, and Collisions
- Build a Stage3D Shoot-'Em-Up: Terrain, Enemy AI, and Level Data
- Build a Stage3D Shoot-’Em-Up: Score, Health, Lives, HUD and Transitions
- Build a Stage3D Shoot-’Em-Up: Full-Screen Boss Battles and Polish
Final Result Preview
Let's take a look at the final result we will be working towards: a hardware-accelerated shoot-em-up demo that includes everything from parts one to three of this series, plus new enemy movement modes, enemies that shoot back, and hand-crafted levels that include background terrain.
Introduction: Welcome to Level Four!
Let's continue to make a side-scrolling shooter inspired by retro arcade titles such as R-Type or Gradius in AS3.
In the first part of this series, we implemented a basic 2D sprite engine that achieves great performance through the use of Stage3D hardware rendering and as several optimizations.
In the first part, we implemented a title screen, the main menu, sound and music, and an input system so that the player can control their spaceship using the keyboard.
And in the third part, we added all the eye-candy: a particle system complete with sparks, flying debris, shockwaves, engine fire trails and tons of explosions.
In this part, we are going to upgrade several main components of our game engine. Firstly, we are going to add A.I. (artificial intelligence) to our enemies by creating several different behaviors and movement styles. They are finally going to start shooting back, and will no longer simply move in a straight line. Some won't even move at all, but will point at the the player: perfect for sentry guns.
Secondly, we are going to implement a level data parsing mechanism that will empower you to design vast game worlds using a level editor.
Thirdly, we are going to create a new spritesheet and rendering batch for a non-interactive set of terrain sprites that will be used as part of the background. This way, we will be flying over top of detailed space stations and asteroids rather than just empty space.
Step 1: Open Your Existing Project
We're going to be building on the source code written in the previous tutorials, much of which will not change. If you don't already have it, be sure to download the source code from the previous tutorial. Open the project file in FlashDevelop (info here) and get ready to upgrade your game! This source code will work in any other AS3 compiler, from CS6 to Flash Builder.
Step 2: Upgrade the Entity Class
We are first going to implement some new movement AI to our entity class. For this functionality we're going to require some more state data to be tracked for each entity. In particular we'll need some path information, and various timers that will let us know how often an enemy needs to shoot at the player. Open the existing Entity.as
file from last time and add the following new class variables as follows. To avoid confusion, the entire top section of the file is included here but only the AI variables at the top are new.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 4
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// Entity.as
|
6 |
// The Entity class will eventually hold all game-specific entity stats
|
7 |
// for the spaceships, bullets and effects in our game.
|
8 |
// It stores a reference to a gpu sprite and a few demo properties.
|
9 |
// This is where you would add hit points, weapons, ability scores, etc.
|
10 |
// This class handles any AI (artificial intelligence) for enemies as well.
|
11 |
|
12 |
package
|
13 |
{
|
14 |
import flash.geom.Point; |
15 |
import flash.geom.Rectangle; |
16 |
|
17 |
public class Entity |
18 |
{
|
19 |
// AI VARIABLES BEGIN
|
20 |
// if this is set, custom behaviors are run
|
21 |
public var aiFunction : Function; |
22 |
// the AI routines might want access to the entity manager
|
23 |
public var gfx:EntityManager; |
24 |
// AI needs to have access to time passing (in seconds)
|
25 |
public var age:Number = 0; |
26 |
public var fireTime:Number = 0; |
27 |
public var fireDelayMin:Number = 1; |
28 |
public var fireDelayMax:Number = 6; |
29 |
// an array of points defining a movement path for the AI
|
30 |
public var aiPathWaypoints:Array; |
31 |
// how fast we travel from one spline node to the next in seconds
|
32 |
public var pathNodeTime:Number = 1; |
33 |
// these offsets are added to the sprite location
|
34 |
// so that ships move around but eventually scroll offscreen
|
35 |
public var aiPathOffsetX:Number = 0; |
36 |
public var aiPathOffsetY:Number = 0; |
37 |
// how much big the path is (max)
|
38 |
public var aiPathSize:Number = 128; |
39 |
// how many different nodes in the path
|
40 |
public var aiPathWaypointCount:int = 8; |
41 |
// AI VARIABLES END
|
42 |
|
43 |
private var _speedX : Number; |
44 |
private var _speedY : Number; |
45 |
private var _sprite : LiteSprite; |
46 |
public var active : Boolean = true; |
47 |
|
48 |
// collision detection
|
49 |
public var isBullet:Boolean = false; // only these check collisions |
50 |
public var leavesTrail:Boolean = false; // creates particles as it moves |
51 |
public var collidemode:uint = 0; // 0=none, 1=sphere, 2=box, etc. |
52 |
public var collideradius:uint = 32; // used for sphere collision |
53 |
// box collision is not implemented (yet)
|
54 |
public var collidebox:Rectangle = new Rectangle(-16, -16, 32, 32); |
55 |
public var collidepoints:uint = 25; // score earned if destroyed |
56 |
public var touching:Entity; // what entity just hit us? |
57 |
|
58 |
public var owner:Entity; // so your own bullets don't hit you |
59 |
public var orbiting:Entity; // entities can orbit (circle) others |
60 |
public var orbitingDistance:Number; // how far in px from the orbit center |
61 |
|
62 |
// used for particle animation (in units per second)
|
63 |
public var fadeAnim:Number = 0; |
64 |
public var zoomAnim:Number = 0; |
65 |
public var rotationSpeed:Number = 0; |
66 |
|
67 |
// used to mark whether or not this entity was
|
68 |
// freshly created or reused from an inactive one
|
69 |
public var recycled:Boolean = false; |
Step 3: Don't Fire Immediately
We're going to keep track of the elapsed time since an enemy shot at the player so that bad guys don't shoot too often. We also need enemies to remember not to fire instantly right when they are first spawned, so we need to add a small amount of random time before they start shooting.
Continuing with Entity.as
, upgrade the class constructor function as follows, and simply take note of the unchanged getter and setter functions as well as the identical collision detection code from last time.
1 |
|
2 |
public function Entity(gs:LiteSprite, myManager:EntityManager) |
3 |
{
|
4 |
_sprite = gs; |
5 |
_speedX = 0.0; |
6 |
_speedY = 0.0; |
7 |
// we need a reference to the entity manager
|
8 |
gfx = myManager; |
9 |
// we don't want everyone shooting on the first frame
|
10 |
fireTime = (gfx.fastRandom() * (fireDelayMax - fireDelayMin)) + fireDelayMin; |
11 |
}
|
12 |
|
13 |
public function die() : void |
14 |
{
|
15 |
// allow this entity to be reused by the entitymanager
|
16 |
active = false; |
17 |
// skip all drawing and updating
|
18 |
sprite.visible = false; |
19 |
// reset some things that might affect future reuses:
|
20 |
leavesTrail = false; |
21 |
isBullet = false; |
22 |
touching = null; |
23 |
owner = null; |
24 |
age = 0; |
25 |
collidemode = 0; |
26 |
}
|
27 |
|
28 |
public function get speedX() : Number |
29 |
{
|
30 |
return _speedX; |
31 |
}
|
32 |
public function set speedX(sx:Number) : void |
33 |
{
|
34 |
_speedX = sx; |
35 |
}
|
36 |
public function get speedY() : Number |
37 |
{
|
38 |
return _speedY; |
39 |
}
|
40 |
public function set speedY(sy:Number) : void |
41 |
{
|
42 |
_speedY = sy; |
43 |
}
|
44 |
public function get sprite():LiteSprite |
45 |
{
|
46 |
return _sprite; |
47 |
}
|
48 |
public function set sprite(gs:LiteSprite):void |
49 |
{
|
50 |
_sprite = gs; |
51 |
}
|
52 |
|
53 |
// used for collision callback performed in GameActorpool
|
54 |
public function colliding(checkme:Entity):Entity |
55 |
{
|
56 |
if (collidemode == 1) // sphere |
57 |
{
|
58 |
if (isCollidingSphere(checkme)) |
59 |
return checkme; |
60 |
}
|
61 |
return null; |
62 |
}
|
63 |
|
64 |
// simple sphere to sphere collision
|
65 |
public function isCollidingSphere(checkme:Entity):Boolean |
66 |
{
|
67 |
// never collide with yourself
|
68 |
if (this == checkme) return false; |
69 |
// only check if these shapes are collidable
|
70 |
if (!collidemode || !checkme.collidemode) return false; |
71 |
// don't check your own bullets
|
72 |
if (checkme.owner == this) return false; |
73 |
// don't check things on the same "team"
|
74 |
if (checkme.owner == owner) return false; |
75 |
// don't check if no radius
|
76 |
if (collideradius == 0 || checkme.collideradius == 0) return false; |
77 |
|
78 |
// this is the simpler way to do it, but it runs really slow
|
79 |
// var dist:Number = Point.distance(sprite.position, checkme.sprite.position);
|
80 |
// if (dist <= (collideradius+checkme.collideradius))
|
81 |
|
82 |
// this looks wierd but is 6x faster than the above
|
83 |
// see: http://www.emanueleferonato.com/2010/10/13/as3-geom-point-vs-trigonometry/
|
84 |
if (((sprite.position.x - checkme.sprite.position.x) * |
85 |
(sprite.position.x - checkme.sprite.position.x) + |
86 |
(sprite.position.y - checkme.sprite.position.y) * |
87 |
(sprite.position.y - checkme.sprite.position.y)) |
88 |
<=
|
89 |
(collideradius+checkme.collideradius)*(collideradius+checkme.collideradius)) |
90 |
{
|
91 |
touching = checkme; // remember who hit us |
92 |
return true; |
93 |
}
|
94 |
|
95 |
// default: too far away
|
96 |
// trace("No collision. Dist = "+dist);
|
97 |
return false; |
98 |
|
99 |
}
|
Step 4: Spline Curve Movement
One of the new AI modes is going to be a curvy, random movement path. A great algorithm used in many games is the classic Catmull-Rom spline curve, which takes an array of points and interpolates a smooth path between them all. This path will loop around at the ends if need be.
The first new routine we need for our entity class is the spline calculation function. It takes three points and a "percentage" number t
that should go from zero to one over time. As t
approaches 1 the point returned will be at the very end of the curve segment, and conversely if it is zero the point returned is the spline's starting position.
For our current game demo, we're just going to generate a bunch of random points for each new entity that uses this kind of moment, but you could easily add your own predefined paths for all sorts of interesting enemy movement patterns, from a figure eight to a simple zig-zag pattern.
You can read more about Catmull-Rom splione curves in AS3 by checking out this demo and this tutorial.
1 |
|
2 |
// Calculates 2D cubic Catmull-Rom spline.
|
3 |
// See http://www.mvps.org/directx/articles/catmull/
|
4 |
public function spline (p0:Point, p1:Point, p2:Point, p3:Point, t:Number):Point |
5 |
{
|
6 |
return new Point ( |
7 |
0.5 * ((2 * p1.x) + |
8 |
t * (( -p0.x + p2.x) + |
9 |
t * ((2 * p0.x -5 * p1.x +4 * p2.x -p3.x) + |
10 |
t * ( -p0.x +3 * p1.x -3 * p2.x +p3.x)))), |
11 |
0.5 * ((2 * p1.y) + |
12 |
t * (( -p0.y + p2.y) + |
13 |
t * ((2 * p0.y -5 * p1.y +4 * p2.y -p3.y) + |
14 |
t * ( -p0.y +3 * p1.y -3 * p2.y +p3.y)))) |
15 |
); |
16 |
}
|
17 |
|
18 |
// generate a random path
|
19 |
public function generatePath():void |
20 |
{
|
21 |
trace("Generating AI path"); |
22 |
aiPathWaypoints = []; |
23 |
var N:int = aiPathWaypointCount; |
24 |
for (var i:int = 0; i < N; i++) |
25 |
{
|
26 |
aiPathWaypoints.push (new Point (aiPathSize * Math.random (), aiPathSize * Math.random ())); |
27 |
}
|
28 |
}
|
29 |
|
30 |
// find the point on a spline at ratio (0 to 1)
|
31 |
public function calculatePathPosition(ratio:Number = 0):Point |
32 |
{
|
33 |
var i:int = int(ratio); |
34 |
var pointratio:Number = ratio - i; |
35 |
//trace(ratio + ' ratio = path point ' + i + ' segment ratio ' + pointratio);
|
36 |
var p0:Point = aiPathWaypoints [(i -1 + aiPathWaypoints.length) % aiPathWaypoints.length]; |
37 |
var p1:Point = aiPathWaypoints [i % aiPathWaypoints.length]; |
38 |
var p2:Point = aiPathWaypoints [(i +1 + aiPathWaypoints.length) % aiPathWaypoints.length]; |
39 |
var p3:Point = aiPathWaypoints [(i +2 + aiPathWaypoints.length) % aiPathWaypoints.length]; |
40 |
// figure out current position
|
41 |
var q:Point = spline (p0, p1, p2, p3, pointratio); |
42 |
return q; |
43 |
}
|
Step 5: Decide When to Shoot
Every enemy type needs to shoot at the player (except for non-shooting asteroids that will simply spin and float in space). For now, we are going to simply keep track of the elapsed time since we last fired our weapon and add a random range of a couple seconds between shots.
1 |
|
2 |
// we could optionally implement many different
|
3 |
// versions of this routine with different randomness
|
4 |
public function maybeShoot(bulletNum:int = 1, |
5 |
delayMin:Number = NaN, |
6 |
delayMax:Number = NaN):void |
7 |
{
|
8 |
// is it time to shoot a bullet?
|
9 |
if (fireTime < age) |
10 |
{
|
11 |
// if delay parameters were not set, use class defaults
|
12 |
if (isNaN(delayMin)) delayMin = fireDelayMin; |
13 |
if (isNaN(delayMax)) delayMax = fireDelayMax; |
14 |
|
15 |
// shoot one from the current location
|
16 |
gfx.shootBullet(bulletNum, this); |
17 |
// randly choose the next time to shoot
|
18 |
fireTime = age + (gfx.fastRandom() * (delayMax - delayMin)) + delayMin; |
19 |
}
|
20 |
}
|
You could add more intelligence to this routine by only shooting when the player is within a certain distance, or only shooting once and self destructing if your game requires some sort of "time-bomb" effect.
Step 6: AI #1: Move in a Straight Line
The first AI "brain" function we are going to implement is the most simple: the straightforward movement along a line as seen in last week's tutorial. All we do here is point in the corrent direction and move at whatever trajectory was randomly assigned to us when we were first spawned. Nothing to it!
1 |
|
2 |
// moves forward and points at current destination based on speed
|
3 |
public function straightAI(seconds:Number):void |
4 |
{
|
5 |
age += seconds; |
6 |
maybeShoot(1); |
7 |
sprite.rotation = gfx.pointAtRad(speedX, speedY) |
8 |
- (90*gfx.DEGREES_TO_RADIANS); |
9 |
}
|
Step 7: AI #2: Sinusoidal Wobble
One of the most common movement patterns in any shoot-em-up is a sinusoidal "wave" motion. We're going to use a sine wave that will wobble up and down over time as the enemy moves in a mostly straight line toward the player. This pattern really looks nice and adds some pleasing movement to your enemies without too much chaos, which makes these kinds of enemies easy to aim at and destroy.
1 |
|
2 |
// a very simple up/down wobble movement
|
3 |
public function wobbleAI(seconds:Number):void |
4 |
{
|
5 |
age += seconds; |
6 |
maybeShoot(1); |
7 |
aiPathOffsetY = (Math.sin(age*2) / Math.PI) * 128; |
8 |
}
|
Step 8: AI #3: Sentry Guns
Another really handy and common style of enemy AI that most shoot-em-ups use is a motionless "sentry gun" or turret. This kind of enemy will just sit there and aim at the player. It can be scary for players to see the sentry guns following their every move and is sure to elicit some evasive maneuvers.
1 |
|
2 |
// simply point at the player: good for sentry guns
|
3 |
public function sentryAI(seconds:Number):void |
4 |
{
|
5 |
age += seconds; |
6 |
maybeShoot(3,3,6); |
7 |
if (gfx.thePlayer) |
8 |
sprite.rotation = gfx.pointAtRad( |
9 |
gfx.thePlayer.sprite.position.x - sprite.position.x, |
10 |
gfx.thePlayer.sprite.position.y - sprite.position.y) |
11 |
- (90*gfx.DEGREES_TO_RADIANS); |
12 |
}
|
Since we need access to the player entity's position, we do a quick check to ensure that it exists since AI functions might get run during the "attract mode" main menu before there is a player to aim at.
Step 9: AI #4: Spline Paths
The final style of AI movement is going to use the Catmull-Rom spline curve interpolation routines we programmed above. Enemies of this type will wobble and spin around in a choatic, frustrating fashion. It is best to only include a few enemies of this type in your game, unless you have upgraded the generatePath
function to use a manually-entered array of points that isn't so random.
To make things look nicer, once we figure out the new coordinates for our enemy, we orient the sprite to face the direction in which it is moving, so it spins while it loops around.
1 |
|
2 |
// move around on a random spline path
|
3 |
// in future versions, you could upgrade this function
|
4 |
// to (instead of random) follow a predefined array of points
|
5 |
// that were designed by hand in code (or even in the level editor!)
|
6 |
public function droneAI(seconds:Number):void |
7 |
{
|
8 |
//trace('droneAI');
|
9 |
|
10 |
age += seconds; |
11 |
maybeShoot(1); |
12 |
|
13 |
// movement style inspired by Galaga, R-Type, Centipede
|
14 |
// performed by easing through a catmull-rom spline curve
|
15 |
// defined by an array of points
|
16 |
if (aiPathWaypoints == null) |
17 |
generatePath(); |
18 |
|
19 |
// how many spline nodes have we passed? (loops around to beginning)
|
20 |
var pathProgress:Number = age / pathNodeTime; |
21 |
|
22 |
var newPos:Point = calculatePathPosition(pathProgress); |
23 |
|
24 |
// point in the correct direction
|
25 |
sprite.rotation = gfx.pointAtRad(newPos.x-aiPathOffsetX,newPos.y-aiPathOffsetY) |
26 |
- (90*gfx.DEGREES_TO_RADIANS); |
27 |
|
28 |
// change path offset location
|
29 |
// this is added to the sprite scrolling location
|
30 |
// so that ships eventually move offscreen
|
31 |
// sprite.position.x = newPos.x;
|
32 |
// sprite.position.y = newPos.y;
|
33 |
|
34 |
aiPathOffsetX = newPos.x; |
35 |
aiPathOffsetY = newPos.x; |
36 |
|
37 |
}
|
38 |
|
39 |
} // end class |
40 |
} // end package |
That's it for our newly upgraded entity class. We've added some fresh new movement code and finally our vast fleets of enemy ships are capable of firing back at the player!
Step 10: The Terrain Spritesheet
Before we move on, we are going to create a new spritesheet for use as the building blocks for our terrain graphics. We are again going to use the wonderful, legal and free "Tyrian" art by Daniel Cook (available at Lostgarden.com). As always, remember to use a square image that is a power-of-two in dimensions (128x128, 256x256, 512x512, etc) when creating your spritesheet. This is the spritesheet I created for this demo:



Step 11: Level Editor Time!
It is finally time to implement a way to parse the data that is generated by a proper level editor. Instead of a simplistic random, never-ending wave of enemies, we want to be able to hand-craft an interesting gameplay experience. To do so, we're going to create a simple class that parses the data output by a popular open source level editor called OGMO. You can read all about OGMO here.
You don't have to use OGMO: you could modify these routines to parse an XML as output by "Tiled" or "DAME", or a .CSV file as output by Excel, or even a .GIF or .PNG as output by Photoshop (by drawing individual pixels and spawning different kinds of enemy depending on what color each pixel is).
The parsing routine for any of kind of level data is trivial compared to our in-game functionality. For sheer simplicity and small download size, CSV (comma-separated values) is a great alternative to bloated and complex XML. More importantly, the linear nature of our shoot-em-up requires a long string of data that is convinient to be able to access column-by-column and row-by-row, as opposed to a "soup" of XML entities that could be in any order. CSV keeps our data flowing from left to right, just like our game. Since OGMO can save data in this format, it's a perfect fit.
Download the installer and optionally the source code. Once you've installed it, create two new projects - one for the terrain and one for the enemy sprites. Ensure that your level data will be saved in the compact and simplistic "trimmed CSV" format.



Be sure to save in CSV format
For the terrain project, we need to create a layer for the terrain that uses our newly photoshopped spritesheet above.



Now, simpy draw the map any way you like. You can click each sprite in your spritesheet in the tile palette and them draw or flood-fill your level as you see fit. Right-click the level to erase that tile, and hold down space while clicking and dragging to scroll around.



Now do the same thing for your enemy sprites. Create a layer that uses the spritesheet we made in previous tutorials as follows:



Finally, fill your level with all sorts of interesting squadrons of enemy ships as you see fit. For the current demo, I started with only a few baddies and literally filled every single space with asteroids towards the end of the level.



The OMGO source files for the levels used in the game demo are included in the /assets/
folder of the source code zip file.
Step 10: Embed the Level Data
We're going to simply embed the levels right into our SWF so that it continues to be a standalone game that doesn't require any external files to be downloaded. You can have the level editor open while programming your game. Any time you tweak your level, just save it and click FlashDevelop's RUN button to see the changes in action. It will notice the new timestamp on your level file and recompile the SWF accordingly.
Depending on which level is requested, we fill a two-dimensional array of integer values based on the level data output by the editor. During gameplay, our entity manager is going to periodically spawn another column of terrain or enemy sprites based on this data. Create a brand new file in your code project called GameLevels.as
and embed the level data as follows.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 4
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// GameLevels.as
|
6 |
// This class parses .CSV level data strings
|
7 |
// that define the locations of tiles from a spritesheet
|
8 |
// Example levels were created using the OGMO editor,
|
9 |
// but could be designed by hand or any number of other
|
10 |
// freeware game level editors that can output .csv
|
11 |
// This can be a .txt, .csv, .oel, .etc file
|
12 |
// - we will strip all xml/html tags (if any)
|
13 |
// - we only care about raw csv data
|
14 |
// Our game can access the current level with:
|
15 |
// spriteId = myLevel.data[x][y];
|
16 |
|
17 |
package
|
18 |
{
|
19 |
import flash.display3D.Context3DProgramType; |
20 |
public class GameLevels |
21 |
{
|
22 |
// the "demo" level seen during the title screen
|
23 |
[Embed(source = '../assets/level0.oel', mimeType = 'application/octet-stream')] |
24 |
private static const LEVEL0:Class; |
25 |
private var level0data:String = new LEVEL0; |
26 |
|
27 |
// the "demo" level background TERRAIN
|
28 |
[Embed(source = '../assets/terrain0.oel', mimeType = 'application/octet-stream')] |
29 |
private static const LEVEL0TERRAIN:Class; |
30 |
private var level0terrain:String = new LEVEL0TERRAIN; |
31 |
|
32 |
// the first level that the player actually experiences
|
33 |
[Embed(source = '../assets/level1.oel', mimeType = 'application/octet-stream')] |
34 |
private static const LEVEL1:Class; |
35 |
private var level1data:String = new LEVEL1; |
36 |
|
37 |
// the first level background TERRAIN
|
38 |
[Embed(source = '../assets/terrain1.oel', mimeType = 'application/octet-stream')] |
39 |
private static const LEVEL1TERRAIN:Class; |
40 |
private var level1terrain:String = new LEVEL1TERRAIN; |
41 |
|
42 |
// the currently loaded level data
|
43 |
public var data:Array = []; |
44 |
|
45 |
public function GameLevels() |
46 |
{
|
47 |
}
|
Step 11: Parse the Level Data
Our new level data parsing class is going to be incredibly simplistic: we simply strip any superfluous XML and gobble up the .CSV data by splitting each line by commas. This is enough for our purposes.
Continue with GameLevels.as
and implement the level data parsing function as follows:
1 |
|
2 |
private function stripTags(str:String):String |
3 |
{
|
4 |
var pattern:RegExp = /<\/?[a-zA-Z0-9]+.*?>/gim; |
5 |
return str.replace(pattern, ""); |
6 |
}
|
7 |
|
8 |
private function parseLevelData(lvl:String):Array |
9 |
{
|
10 |
var levelString:String; |
11 |
var temps:Array; |
12 |
var nextValue:int; |
13 |
var output:Array = []; |
14 |
var nextrow:int; |
15 |
switch (lvl) |
16 |
{
|
17 |
case "level0" : levelString = stripTags(level0data); break; |
18 |
case "terrain0" : levelString = stripTags(level0terrain); break; |
19 |
case "level1" : levelString = stripTags(level1data); break; |
20 |
case "terrain1" : levelString = stripTags(level1terrain); break; |
21 |
default: |
22 |
return output; |
23 |
}
|
24 |
//trace("Level " + num + " data:\n" + levelString);
|
25 |
var lines:Array = levelString.split(/\r\n|\n|\r/); |
26 |
for (var row:int = 0; row < lines.length; row++) |
27 |
{
|
28 |
// split the string by comma
|
29 |
temps = lines[row].split(","); |
30 |
if (temps.length > 1) |
31 |
{
|
32 |
nextrow = output.push([]) - 1; |
33 |
// turn the string values into integers
|
34 |
for (var col:int = 0; col < temps.length; col++) |
35 |
{
|
36 |
if (temps[col] == "") temps[col] = "-1"; |
37 |
nextValue = parseInt(temps[col]); |
38 |
if (nextValue < 0) nextValue = -1; // we still need blanks |
39 |
trace('row '+ nextrow + ' nextValue=' + nextValue); |
40 |
output[nextrow].push(nextValue); |
41 |
}
|
42 |
//trace('Level row '+nextrow+':\n' + String(output[nextrow]));
|
43 |
}
|
44 |
}
|
45 |
//trace('Level output data:\n' + String(output));
|
46 |
return output; |
47 |
}
|
48 |
|
49 |
public function loadLevel(lvl:String):void |
50 |
{
|
51 |
trace("Loading level " + lvl); |
52 |
data = parseLevelData(lvl); |
53 |
}
|
54 |
|
55 |
} // end class |
56 |
} // end package |
That's it for our level parsing class. Though it is simplistic, it takes up very little space in our SWF, runs quite quickly, and allows us to iterate our level designs with ease by having FlashDevelop and OGMO open at the same time. Two clicks are all it takes to try out a new version of your level, which means that the design-test-repeat cycle is mere seconds.
Just for fun, here are some screenshots of the style of levels we are going to be able to play in our game:












Step 12: Upgrade the Entity Manager
We need to take advantage of these cool new AI movement modes and the awesome new terrain system we just created. This will require some minor tweaks to the EntityManager.as
file from last time. To avoid confusion, the entire class is presented here, but only a few lines here and there have changed.
In particular, instead of forcing the entity manager to use a particular spritesheet image, we are going to allow it to be defined by our Main.as
so that we can have more than one. This is because we now have an enemy and a terrain entity manager running concurrently.
Other minor tweaks include a larger culling distance (the outside edges of the game world where sprite that go beyond it are recycled for reuse in our sprite pool), plus various new class variables that are needed for our level data parsing routine.
Begin by upgrading all the class variables at the top of the file as follows:
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 4
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// EntityManager.as
|
6 |
// The entity manager handles a list of all known game entities.
|
7 |
// This object pool will allow for reuse (respawning) of
|
8 |
// sprites: for example, when enemy ships are destroyed,
|
9 |
// they will be re-spawned when needed as an optimization
|
10 |
// that increases fps and decreases ram use.
|
11 |
|
12 |
package
|
13 |
{
|
14 |
import flash.display.Bitmap; |
15 |
import flash.display3D.*; |
16 |
import flash.geom.Point; |
17 |
import flash.geom.Rectangle; |
18 |
|
19 |
public class EntityManager |
20 |
{
|
21 |
// the level data parser
|
22 |
public var level:GameLevels; |
23 |
// the current level number
|
24 |
public var levelNum:int = 0; |
25 |
// where in the level we are in pixels
|
26 |
public var levelCurrentScrollX:Number = 0; |
27 |
// the last spawned column of level data
|
28 |
public var levelPrevCol:int = -1; |
29 |
// pixels we need to scroll before spawning the next col
|
30 |
public var levelTilesize:int = 48; |
31 |
// this is used to ensure all terrain tiles line up exactly
|
32 |
public var lastTerrainEntity:Entity; |
33 |
|
34 |
// we need to allow at least enough space for ship movement
|
35 |
// entities that move beyond the edges of the screen
|
36 |
// plus this amount are recycled (destroyed for reuse)
|
37 |
public var cullingDistance:Number = 200; |
38 |
|
39 |
// a particle system class that updates our sprites
|
40 |
public var particles:GameParticles; |
41 |
|
42 |
// so that explosions can be played
|
43 |
public var sfx:GameSound; |
44 |
|
45 |
// the sprite sheet image
|
46 |
public var spriteSheet : LiteSpriteSheet; |
47 |
public var SpritesPerRow:int = 8; |
48 |
public var SpritesPerCol:int = 8; |
49 |
// we no longer force a particular spritesheet here
|
50 |
//[Embed(source="../assets/sprites.png")]
|
51 |
//private var SourceImage : Class;
|
52 |
public var SourceImage : Class; |
53 |
|
54 |
// the general size of the player and enemies
|
55 |
public var defaultScale:Number = 1; |
56 |
// how fast the default scroll (enemy flying) speed is
|
57 |
public var defaultSpeed:Number = 128; |
58 |
// how fast player bullets go per second
|
59 |
public var bulletSpeed:Number = 250; |
60 |
|
61 |
// for framerate-independent timings
|
62 |
public var currentFrameSeconds:Number = 0; |
63 |
|
64 |
// sprite IDs (indexing the spritesheet)
|
65 |
public const spritenumFireball:uint = 63; |
66 |
public const spritenumFireburst:uint = 62; |
67 |
public const spritenumShockwave:uint = 61; |
68 |
public const spritenumDebris:uint = 60; |
69 |
public const spritenumSpark:uint = 59; |
70 |
public const spritenumBullet3:uint = 58; |
71 |
public const spritenumBullet2:uint = 57; |
72 |
public const spritenumBullet1:uint = 56; |
73 |
public const spritenumPlayer:uint = 10; |
74 |
public const spritenumOrb:uint = 17; |
75 |
|
76 |
// reused for calculation speed
|
77 |
public const DEGREES_TO_RADIANS:Number = Math.PI / 180; |
78 |
public const RADIANS_TO_DEGREES:Number = 180 / Math.PI; |
79 |
|
80 |
// the player entity - a special case
|
81 |
public var thePlayer:Entity; |
82 |
// a "power orb" that orbits the player
|
83 |
public var theOrb:Entity; |
84 |
|
85 |
// a reusable pool of entities
|
86 |
// this contains every known Entity
|
87 |
// including the contents of the lists below
|
88 |
public var entityPool : Vector.<Entity>; |
89 |
// these pools contain only certain types
|
90 |
// of entity as an optimization for smaller loops
|
91 |
public var allBullets : Vector.<Entity>; |
92 |
public var allEnemies : Vector.<Entity>; |
93 |
|
94 |
// all the polygons that make up the scene
|
95 |
public var batch : LiteSpriteBatch; |
96 |
|
97 |
// for statistics
|
98 |
public var numCreated : int = 0; |
99 |
public var numReused : int = 0; |
100 |
|
101 |
public var maxX:int; |
102 |
public var minX:int; |
103 |
public var maxY:int; |
104 |
public var minY:int; |
105 |
public var midpoint:int; |
Step 13: Upgrade the Inits
We need to upgrade our entity manager's class constructor to create an instance of the game level data parser class we wrote above. Additionally, we want to store the midpoint of the screen for use as the starting playing position and extend the minimum and maximum sprite locations whenever the game is resized. Continuing with EntityManager.as
, upgrade the following.
1 |
|
2 |
public function EntityManager(view:Rectangle) |
3 |
{
|
4 |
entityPool = new Vector.<Entity>(); |
5 |
allBullets = new Vector.<Entity>(); |
6 |
allEnemies = new Vector.<Entity>(); |
7 |
particles = new GameParticles(this); |
8 |
setPosition(view); |
9 |
level = new GameLevels(); |
10 |
}
|
11 |
|
12 |
public function setPosition(view:Rectangle):void |
13 |
{
|
14 |
// allow moving fully offscreen before
|
15 |
// automatically being culled (and reused)
|
16 |
maxX = view.width + cullingDistance; |
17 |
minX = view.x - cullingDistance; |
18 |
maxY = view.height + cullingDistance; |
19 |
minY = view.y - cullingDistance; |
20 |
midpoint = view.height / 2; |
21 |
}
|
22 |
|
23 |
// this XOR based fast random number generator runs 4x faster
|
24 |
// than Math.random() and also returns a number from 0 to 1
|
25 |
// see http://www.calypso88.com/?cat=7
|
26 |
private const FASTRANDOMTOFLOAT:Number = 1 / uint.MAX_VALUE; |
27 |
private var fastrandomseed:uint = Math.random() * uint.MAX_VALUE; |
28 |
public function fastRandom():Number |
29 |
{
|
30 |
fastrandomseed ^= (fastrandomseed << 21); |
31 |
fastrandomseed ^= (fastrandomseed >>> 35); |
32 |
fastrandomseed ^= (fastrandomseed << 4); |
33 |
return (fastrandomseed * FASTRANDOMTOFLOAT); |
34 |
}
|
Step 14: Account for UV Padding
There is one special tweak that is required for our createBatch function. It turns out that the terrain spritesheet, which used tiled sprites that are right next to each other and doesn't include any empty space between tiles, can produce visual glitches on our game if used as-is. This is due to the way your video card's GPU samples each texture in the sprite batch when it renders all the sprites. Here's an example of our terrain being rendered using the routines from last week:



What is happening in example #1 is that the edge pixels of tiles can "bleed through" to adjascent tiles due to bilinear interpolation of the RGB values. To account for this, we need to allow for a small texture coordinate (UV) offset value, which will "zoom in" each tile by just a minuscule amount. Without this change, the game would have artifacts as seen above.
1 |
|
2 |
public function createBatch(context3D:Context3D, uvPadding:Number=0) : LiteSpriteBatch |
3 |
{
|
4 |
var sourceBitmap:Bitmap = new SourceImage(); |
5 |
|
6 |
// create a spritesheet with 8x8 (64) sprites on it
|
7 |
spriteSheet = new LiteSpriteSheet(sourceBitmap.bitmapData, SpritesPerRow, SpritesPerCol, uvPadding); |
8 |
|
9 |
// Create new render batch
|
10 |
batch = new LiteSpriteBatch(context3D, spriteSheet); |
11 |
|
12 |
return batch; |
13 |
}
|
Step 15: Spawning Routines
The following routines are virtually identical to last week's apart from the use of the midpoint value in the player spawning and some size differences. They are included here for completeness.
Finally, the addRandomEntity
function as defined below is what in previous versions was the addEntities
function. It is not used in this demo and could be deleted, since we are changing our game to no longer use randomly-spawned enemies and are instead switching to hand-crafted levels. This function might come handy in your testing to account for time when there is no remaining level data. You can simply copy and paste this code over top of your original routines and move on without taking a deeper look.
1 |
|
2 |
|
3 |
// search the entity pool for unused entities and reuse one
|
4 |
// if they are all in use, create a brand new one
|
5 |
public function respawn(sprID:uint=0):Entity |
6 |
{
|
7 |
var currentEntityCount:int = entityPool.length; |
8 |
var anEntity:Entity; |
9 |
var i:int = 0; |
10 |
// search for an inactive entity
|
11 |
for (i = 0; i < currentEntityCount; i++ ) |
12 |
{
|
13 |
anEntity = entityPool[i]; |
14 |
if (!anEntity.active && (anEntity.sprite.spriteId == sprID)) |
15 |
{
|
16 |
//trace('Reusing Entity #' + i);
|
17 |
anEntity.active = true; |
18 |
anEntity.sprite.visible = true; |
19 |
anEntity.recycled = true; |
20 |
numReused++; |
21 |
return anEntity; |
22 |
}
|
23 |
}
|
24 |
// none were found so we need to make a new one
|
25 |
//trace('Need to create a new Entity #' + i);
|
26 |
var sprite:LiteSprite; |
27 |
sprite = batch.createChild(sprID); |
28 |
anEntity = new Entity(sprite, this); |
29 |
entityPool.push(anEntity); |
30 |
numCreated++; |
31 |
return anEntity; |
32 |
}
|
33 |
|
34 |
// this entity is the PLAYER
|
35 |
public function addPlayer(playerController:Function):Entity |
36 |
{
|
37 |
trace("Adding Player Entity"); |
38 |
thePlayer = respawn(spritenumPlayer); |
39 |
thePlayer.sprite.position.x = 64; |
40 |
thePlayer.sprite.position.y = midpoint; |
41 |
thePlayer.sprite.rotation = 180 * DEGREES_TO_RADIANS; |
42 |
thePlayer.sprite.scaleX = thePlayer.sprite.scaleY = defaultScale; |
43 |
thePlayer.speedX = 0; |
44 |
thePlayer.speedY = 0; |
45 |
thePlayer.active = true; |
46 |
thePlayer.collidemode = 1; |
47 |
thePlayer.collideradius = 10; |
48 |
thePlayer.owner = thePlayer; // collisions require this |
49 |
thePlayer.aiFunction = playerController; |
50 |
|
51 |
// just for fun, spawn an orbiting "power orb"
|
52 |
theOrb = respawn(spritenumOrb); |
53 |
theOrb.rotationSpeed = 720 * DEGREES_TO_RADIANS; |
54 |
theOrb.sprite.scaleX = theOrb.sprite.scaleY = defaultScale / 2; |
55 |
theOrb.leavesTrail = true; |
56 |
theOrb.collidemode = 1; |
57 |
theOrb.collideradius = 12; |
58 |
theOrb.isBullet = true; |
59 |
theOrb.owner = thePlayer; |
60 |
theOrb.orbiting = thePlayer; |
61 |
theOrb.orbitingDistance = 180; |
62 |
|
63 |
return thePlayer; |
64 |
}
|
65 |
|
66 |
// shoot a bullet
|
67 |
public function shootBullet(powa:uint=1, shooter:Entity = null):Entity |
68 |
{
|
69 |
// just in case the AI is running during the main menu
|
70 |
// and we've not yet created the player entity
|
71 |
if (thePlayer == null) return null; |
72 |
|
73 |
var theBullet:Entity; |
74 |
// assume the player shot it
|
75 |
// otherwise maybe an enemy did
|
76 |
if (shooter == null) |
77 |
shooter = thePlayer; |
78 |
|
79 |
// three possible bullets, progressively larger
|
80 |
if (powa == 1) |
81 |
theBullet = respawn(spritenumBullet1); |
82 |
else if (powa == 2) |
83 |
theBullet = respawn(spritenumBullet2); |
84 |
else
|
85 |
theBullet = respawn(spritenumBullet3); |
86 |
theBullet.sprite.position.x = shooter.sprite.position.x + 8; |
87 |
theBullet.sprite.position.y = shooter.sprite.position.y + 2; |
88 |
theBullet.sprite.rotation = 180 * DEGREES_TO_RADIANS; |
89 |
theBullet.sprite.scaleX = theBullet.sprite.scaleY = 1; |
90 |
if (shooter == thePlayer) |
91 |
{
|
92 |
theBullet.speedX = bulletSpeed; |
93 |
theBullet.speedY = 0; |
94 |
}
|
95 |
else // enemy bullets move slower and towards the player |
96 |
{
|
97 |
theBullet.sprite.rotation = |
98 |
pointAtRad(theBullet.sprite.position.x - thePlayer.sprite.position.x, |
99 |
theBullet.sprite.position.y-thePlayer.sprite.position.y) - (90*DEGREES_TO_RADIANS); |
100 |
|
101 |
// move in the direction we're facing
|
102 |
theBullet.speedX = defaultSpeed*1.5*Math.cos(theBullet.sprite.rotation); |
103 |
theBullet.speedY = defaultSpeed*1.5*Math.sin(theBullet.sprite.rotation); |
104 |
|
105 |
// optionally, we could just fire straight ahead in the direction we're heading:
|
106 |
// theBullet.speedX = shooter.speedX * 1.5;
|
107 |
// theBullet.speedY = shooter.speedY * 1.5;
|
108 |
// and we could point where we're going like this:
|
109 |
// pointAtRad(theBullet.speedX,theBullet.speedY) - (90*DEGREES_TO_RADIANS);
|
110 |
}
|
111 |
theBullet.owner = shooter; |
112 |
theBullet.collideradius = 10; |
113 |
theBullet.collidemode = 1; |
114 |
theBullet.isBullet = true; |
115 |
if (!theBullet.recycled) |
116 |
allBullets.push(theBullet); |
117 |
return theBullet; |
118 |
}
|
119 |
|
120 |
// Unused: this was "addEntities()" in the previous tutorials.
|
121 |
// It spawns random enemies that move in a straight line
|
122 |
public function addRandomEntity():void |
123 |
{
|
124 |
var anEntity:Entity; |
125 |
var sprID:int; |
126 |
sprID = Math.floor(fastRandom() * 55); |
127 |
// try to reuse an inactive entity (or create a new one)
|
128 |
anEntity = respawn(sprID); |
129 |
// give it a new position and velocity
|
130 |
anEntity.sprite.position.x = maxX; |
131 |
anEntity.sprite.position.y = fastRandom() * maxY; |
132 |
anEntity.speedX = 15 * ((-1 * fastRandom() * 10) - 2); |
133 |
anEntity.speedY = 15 * ((fastRandom() * 5) - 2.5); |
134 |
anEntity.sprite.scaleX = defaultScale; |
135 |
anEntity.sprite.scaleY = defaultScale; |
136 |
anEntity.sprite.rotation = pointAtRad(anEntity.speedX,anEntity.speedY) - (90*DEGREES_TO_RADIANS); |
137 |
anEntity.collidemode = 1; |
138 |
anEntity.collideradius = 16; |
139 |
if (!anEntity.recycled) |
140 |
allEnemies.push(anEntity); |
141 |
}
|
142 |
|
143 |
// returns the angle in radians of two points
|
144 |
public function pointAngle(point1:Point, point2:Point):Number |
145 |
{
|
146 |
var dx:Number = point2.x - point1.x; |
147 |
var dy:Number = point2.y - point1.y; |
148 |
return -Math.atan2(dx,dy); |
149 |
}
|
150 |
|
151 |
// returns the angle in degrees of 0,0 to x,y
|
152 |
public function pointAtDeg(x:Number, y:Number):Number |
153 |
{
|
154 |
return -Math.atan2(x,y) * RADIANS_TO_DEGREES; |
155 |
}
|
156 |
|
157 |
// returns the angle in radians of 0,0 to x,y
|
158 |
public function pointAtRad(x:Number, y:Number):Number |
159 |
{
|
160 |
return -Math.atan2(x,y); |
161 |
}
|
162 |
|
163 |
// as an optimization to saver millions of checks, only
|
164 |
// the player's bullets check for collisions with all enemy ships
|
165 |
// (enemy bullets only check to hit the player)
|
166 |
public function checkCollisions(checkMe:Entity):Entity |
167 |
{
|
168 |
var anEntity:Entity; |
169 |
var collided:Boolean = false; |
170 |
if (checkMe.owner != thePlayer) |
171 |
{ // quick check ONLY to see if we have hit the player |
172 |
anEntity = thePlayer; |
173 |
if (checkMe.colliding(anEntity)) |
174 |
{
|
175 |
trace("Player was HIT!"); |
176 |
collided = true; |
177 |
}
|
178 |
}
|
179 |
else // check all active enemies |
180 |
{
|
181 |
for(var i:int=0; i< allEnemies.length;i++) |
182 |
{
|
183 |
anEntity = allEnemies[i]; |
184 |
if (anEntity.active && anEntity.collidemode) |
185 |
{
|
186 |
if (checkMe.colliding(anEntity)) |
187 |
{
|
188 |
collided = true; |
189 |
break; |
190 |
}
|
191 |
}
|
192 |
}
|
193 |
}
|
194 |
if (collided) |
195 |
{
|
196 |
//trace('Collision!');
|
197 |
if (sfx) sfx.playExplosion(int(fastRandom() * 2 + 1.5)); |
198 |
particles.addExplosion(checkMe.sprite.position); |
199 |
if ((checkMe != theOrb) && (checkMe != thePlayer)) |
200 |
checkMe.die(); // the bullet |
201 |
if ((anEntity != theOrb) && ((anEntity != thePlayer))) |
202 |
anEntity.die(); // the victim |
203 |
return anEntity; |
204 |
}
|
205 |
return null; |
206 |
}
|
Step 16: Upgrade the Render Loop
The update()
function is run every single frame, just as before. Several modifications have been made to account for out new enemy AI functionality that we added to the entity class above, as well as our new level data parsing functionality.
For example, last week we only ran the entity simulation update step if there wasn't an aiFunction
defined. Now, we will be calling this function on almost every game entity that moves, so we run it and then continue with the standard animation by checking the speeds of various entity parameters. We also used to only check for collisions by bullets, but now an enemy ship can collide with the player as well.
Continuing with EntityManager.as
, implement these changes as follows.
1 |
|
2 |
|
3 |
// called every frame: used to update the simulation
|
4 |
// this is where you would perform AI, physics, etc.
|
5 |
// in this version, currentTime is seconds since the previous frame
|
6 |
public function update(currentTime:Number) : void |
7 |
{
|
8 |
var anEntity:Entity; |
9 |
var i:int; |
10 |
var max:int; |
11 |
|
12 |
// what portion of a full second has passed since the previous update?
|
13 |
currentFrameSeconds = currentTime / 1000; |
14 |
|
15 |
// handle all other entities
|
16 |
max = entityPool.length; |
17 |
for (i = 0; i < max; i++) |
18 |
{
|
19 |
anEntity = entityPool[i]; |
20 |
if (anEntity.active) |
21 |
{
|
22 |
// subtract the previous aiPathOffset
|
23 |
anEntity.sprite.position.x -= anEntity.aiPathOffsetX; |
24 |
anEntity.sprite.position.y -= anEntity.aiPathOffsetY; |
25 |
|
26 |
// calculate location on screen with scrolling
|
27 |
anEntity.sprite.position.x += anEntity.speedX * currentFrameSeconds; |
28 |
anEntity.sprite.position.y += anEntity.speedY * currentFrameSeconds; |
29 |
|
30 |
// is a custom AI specified? if so, run it now
|
31 |
if (anEntity.aiFunction != null) |
32 |
{
|
33 |
anEntity.aiFunction(currentFrameSeconds); |
34 |
}
|
35 |
|
36 |
// add the new aiPathOffset
|
37 |
anEntity.sprite.position.x += anEntity.aiPathOffsetX; |
38 |
anEntity.sprite.position.y += anEntity.aiPathOffsetY; |
39 |
|
40 |
// collision detection
|
41 |
if (anEntity.collidemode) |
42 |
{
|
43 |
checkCollisions(anEntity); |
44 |
}
|
45 |
|
46 |
// entities can orbit other entities
|
47 |
// (uses their rotation as the position)
|
48 |
if (anEntity.orbiting != null) |
49 |
{
|
50 |
anEntity.sprite.position.x = anEntity.orbiting.sprite.position.x + |
51 |
((Math.sin(anEntity.sprite.rotation/4)/Math.PI) * anEntity.orbitingDistance); |
52 |
anEntity.sprite.position.y = anEntity.orbiting.sprite.position.y - |
53 |
((Math.cos(anEntity.sprite.rotation/4)/Math.PI) * anEntity.orbitingDistance); |
54 |
}
|
55 |
|
56 |
// entities can leave an engine emitter trail
|
57 |
if (anEntity.leavesTrail) |
58 |
{
|
59 |
// leave a trail of particles
|
60 |
if (anEntity == theOrb) |
61 |
particles.addParticle(63, |
62 |
anEntity.sprite.position.x, anEntity.sprite.position.y, |
63 |
0.25, 0, 0, 0.6, NaN, NaN, -1.5, -1); |
64 |
else // other enemies |
65 |
particles.addParticle(63, anEntity.sprite.position.x + 12, |
66 |
anEntity.sprite.position.y + 2, |
67 |
0.5, 3, 0, 0.6, NaN, NaN, -1.5, -1); |
68 |
|
69 |
}
|
70 |
|
71 |
if ((anEntity.sprite.position.x > maxX) || |
72 |
(anEntity.sprite.position.x < minX) || |
73 |
(anEntity.sprite.position.y > maxY) || |
74 |
(anEntity.sprite.position.y < minY)) |
75 |
{
|
76 |
// if we go past any edge, become inactive
|
77 |
// so the sprite can be respawned
|
78 |
if ((anEntity != thePlayer) && (anEntity != theOrb)) |
79 |
anEntity.die(); |
80 |
}
|
81 |
|
82 |
if (anEntity.rotationSpeed != 0) |
83 |
anEntity.sprite.rotation += anEntity.rotationSpeed * currentFrameSeconds; |
84 |
|
85 |
if (anEntity.fadeAnim != 0) |
86 |
{
|
87 |
anEntity.sprite.alpha += anEntity.fadeAnim * currentFrameSeconds; |
88 |
if (anEntity.sprite.alpha <= 0.001) |
89 |
{
|
90 |
anEntity.die(); |
91 |
}
|
92 |
else if (anEntity.sprite.alpha > 1) |
93 |
{
|
94 |
anEntity.sprite.alpha = 1; |
95 |
}
|
96 |
}
|
97 |
if (anEntity.zoomAnim != 0) |
98 |
{
|
99 |
anEntity.sprite.scaleX += anEntity.zoomAnim * currentFrameSeconds; |
100 |
anEntity.sprite.scaleY += anEntity.zoomAnim * currentFrameSeconds; |
101 |
if (anEntity.sprite.scaleX < 0 || anEntity.sprite.scaleY < 0) |
102 |
anEntity.die(); |
103 |
}
|
104 |
}
|
105 |
}
|
106 |
}
|
Step 17: Switching Levels
The remaining functions in EntityManager.as
are brand new. We need a mechanism to instantly destroy all known entities in an entire game world. This will happen whenever the player goes to the next level. It also occurs immediately when the game leaves the attract mode "main menu" so that sprites that were there don't pollute the player's actual game world. When the game starts, we will parse the next set of level data as well.
1 |
|
2 |
// kill (recycle) all known entities
|
3 |
// this is run when we change levels
|
4 |
public function killEmAll():void |
5 |
{
|
6 |
//trace('Killing all entities...');
|
7 |
var anEntity:Entity; |
8 |
var i:int; |
9 |
var max:int; |
10 |
max = entityPool.length; |
11 |
for (i = 0; i < max; i++) |
12 |
{
|
13 |
anEntity = entityPool[i]; |
14 |
if ((anEntity != thePlayer) && (anEntity != theOrb)) |
15 |
anEntity.die(); |
16 |
}
|
17 |
}
|
18 |
|
19 |
// load a new level for entity generation
|
20 |
public function changeLevels(lvl:String):void |
21 |
{
|
22 |
killEmAll(); |
23 |
level.loadLevel(lvl); |
24 |
levelCurrentScrollX = 0; |
25 |
levelPrevCol = -1; |
26 |
}
|
Step 18: Streaming the Level
The final function we need to add to our entity manager is the one that spawns new entities based on the level data. It measures the distance we have travelled, and when the next set of level tiles is requires it spawns another column of entities as specified by the level data. If the entity manager running this routine is in charge of the terrain, nothing more needs to be done, but if we are spawning enemy ships, asteroids, and sentry guns, we need to decide which kind of AI routine to give each entity.
One important consideration relates to the terrain glitches as illustrated in the image above that showed "seams" between tiles.



In the first few versions of this function, we simply measured the distance travelled based on time elapsed each frame and incremented a counter variable, spawning the next row of tiles when needed. The problem with this approach is that floating point numbers (anything with a decimal point) are not 100% accurate. Since we can only store so much information in a Number
type, some really small amounts are rounded off.
This is imperceptable in most situations, but over time the slight discrepancies add up until eventually the terrain tiles are off by a pixel. Therefore, we keep track of the previous column's terrain tiles and force the next to be exactly the correct distance from it. We simply can't assume that the terrain has scrolled exactly 48 pixels since the last time we spawned tiles. It may have moved 48.00000000001 pixels.
You can read more about the many problems that floating-point accumulators can produce in games in this very interesting article.
1 |
|
2 |
// check to see if another row from the level data should be spawned
|
3 |
public function streamLevelEntities(theseAreEnemies:Boolean = false):void |
4 |
{
|
5 |
var anEntity:Entity; |
6 |
var sprID:int; |
7 |
// time-based with overflow remembering (increment and floor)
|
8 |
levelCurrentScrollX += defaultSpeed * currentFrameSeconds; |
9 |
// is it time to spawn the next col from our level data?
|
10 |
if (levelCurrentScrollX >= levelTilesize) |
11 |
{
|
12 |
levelCurrentScrollX = 0; |
13 |
levelPrevCol++; |
14 |
|
15 |
// this prevents small "seams" due to floating point inaccuracies over time
|
16 |
var currentLevelXCoord:Number; |
17 |
if (lastTerrainEntity && !theseAreEnemies) |
18 |
currentLevelXCoord = lastTerrainEntity.sprite.position.x + levelTilesize; |
19 |
else
|
20 |
currentLevelXCoord = maxX; |
21 |
|
22 |
var rows:int = level.data.length; |
23 |
//trace('levelCurrentScrollX = ' + levelCurrentScrollX +
|
24 |
//' - spawning next level column ' + levelPrevCol + ' row count: ' + rows);
|
25 |
|
26 |
if (level.data && level.data.length) |
27 |
{
|
28 |
for (var row:int = 0; row < rows; row++) |
29 |
{
|
30 |
if (level.data[row].length > levelPrevCol) // data exists? NOP? |
31 |
{
|
32 |
//trace('Next row data: ' + String(level.data[row]));
|
33 |
sprID = level.data[row][levelPrevCol]; |
34 |
if (sprID > -1) // zero is a valid number, -1 means blank |
35 |
{
|
36 |
anEntity = respawn(sprID); |
37 |
anEntity.sprite.position.x = currentLevelXCoord; |
38 |
anEntity.sprite.position.y = (row * levelTilesize) + (levelTilesize/2); |
39 |
trace('Spawning a level sprite ID ' + sprID + ' at ' |
40 |
+ anEntity.sprite.position.x + ',' + anEntity.sprite.position.y); |
41 |
anEntity.speedX = -defaultSpeed; |
42 |
anEntity.speedY = 0; |
43 |
anEntity.sprite.scaleX = defaultScale; |
44 |
anEntity.sprite.scaleY = defaultScale; |
Step 19: Give the Enemies Brains
Continuing with the streamLevelEntities
function, we simply need to choose which kind of AI to use for each newly spawned enemy (if any). For reference, this is the spritesheet we are using:

The enemy spritesheet has been split into rows. The first row of sprites simply moves forward in a straight line at a random angle. The second row uses our newly created sinusoidal "wave like" movement which ponting in a straight line. We account for the two sentry gun tiles and three asteroid images as a special case. The next rows move at a random angle with a wobble, and finally, all the remaining sprites will use our random Catmull-Rom spline curve movement.
1 |
|
2 |
if (theseAreEnemies) |
3 |
{
|
4 |
// which AI should we give this enemy?
|
5 |
switch (sprID) |
6 |
{
|
7 |
case 1: |
8 |
case 2: |
9 |
case 3: |
10 |
case 4: |
11 |
case 5: |
12 |
case 6: |
13 |
case 7: |
14 |
// move forward at a random angle
|
15 |
anEntity.speedX = 15 * ((-1 * fastRandom() * 10) - 2); |
16 |
anEntity.speedY = 15 * ((fastRandom() * 5) - 2.5); |
17 |
anEntity.aiFunction = anEntity.straightAI; |
18 |
break; |
19 |
case 8: |
20 |
case 9: |
21 |
case 10: |
22 |
case 11: |
23 |
case 12: |
24 |
case 13: |
25 |
case 14: |
26 |
case 15: |
27 |
// move straight with a wobble
|
28 |
anEntity.aiFunction = anEntity.wobbleAI; |
29 |
break
|
30 |
case 16: |
31 |
case 24: // sentry guns don't move and always look at the player |
32 |
anEntity.aiFunction = anEntity.sentryAI; |
33 |
anEntity.speedX = -90; // same speed as background |
34 |
break; |
35 |
case 17: |
36 |
case 18: |
37 |
case 19: |
38 |
case 20: |
39 |
case 21: |
40 |
case 22: |
41 |
case 23: |
42 |
// move at a random angle with a wobble
|
43 |
anEntity.speedX = 15 * ((-1 * fastRandom() * 10) - 2); |
44 |
anEntity.speedY = 15 * ((fastRandom() * 5) - 2.5); |
45 |
anEntity.aiFunction = anEntity.wobbleAI; |
46 |
break; |
47 |
case 32: |
48 |
case 40: |
49 |
case 48: // asteroids don't move or shoot but they do spin and drift |
50 |
anEntity.aiFunction = null; |
51 |
anEntity.rotationSpeed = fastRandom() * 8 - 4 |
52 |
anEntity.speedY = fastRandom() * 64 - 32; |
53 |
break; |
54 |
default: // follow a complex random spline curve path |
55 |
anEntity.aiFunction = anEntity.droneAI; |
56 |
break; |
57 |
}
|
58 |
|
59 |
anEntity.sprite.rotation = pointAtRad(anEntity.speedX, anEntity.speedY) |
60 |
- (90*DEGREES_TO_RADIANS); |
61 |
anEntity.collidemode = 1; |
62 |
anEntity.collideradius = 16; |
63 |
if (!anEntity.recycled) |
64 |
allEnemies.push(anEntity); |
65 |
} // end if these were enemies |
66 |
}// end loop for level data rows |
67 |
}
|
68 |
}
|
69 |
}
|
70 |
// remember the last created terrain entity
|
71 |
// (might be null if the level data was blank for this column)
|
72 |
// to avoid slight seams due to terrain scrolling speed over time
|
73 |
if (!theseAreEnemies) lastTerrainEntity = anEntity; |
74 |
}
|
75 |
}
|
76 |
} // end class |
77 |
} // end package |
That's it for our newly upgraded entity manager class. It now takes advantage of our "streaming" terrain, gives enemies the appropriate AI, and no longer simply spawns infinite random streams of baddies.
Step 20: Account for UV Padding
In our upgrades above, we avoided small graphical glitches by accounting for two things: floating-point imprecision and texture sampling interpolation, which causes bleeding of edge pixels into adjascent terrain tiles. The latter required that we "zoom in" the terrain tiles just a tiny bit to ensure that the edges look right. We need to upgrade our existing LiteSpriteSheet.as
class to account for this small offset when creating all the UV texture coordinates for each sprite.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 4
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
|
5 |
// LiteSpriteSheet.as
|
6 |
// An optimization used to improve performance, all sprites used
|
7 |
// in the game are packed onto a single texture so that
|
8 |
// they can be rendered in a single pass rather than individually.
|
9 |
// This also avoids the performance penalty of 3d stage changes.
|
10 |
// Based on example code by Chris Nuuja which is a port
|
11 |
// of the haXe+NME bunnymark demo by Philippe Elsass
|
12 |
// which is itself a port of Iain Lobb's original work.
|
13 |
// Also includes code from the Starling framework.
|
14 |
// Grateful acknowledgements to all involved.
|
15 |
|
16 |
package
|
17 |
{
|
18 |
import flash.display.Bitmap; |
19 |
import flash.display.BitmapData; |
20 |
import flash.display.Stage; |
21 |
import flash.display3D.Context3D; |
22 |
import flash.display3D.Context3DTextureFormat; |
23 |
import flash.display3D.IndexBuffer3D; |
24 |
import flash.display3D.textures.Texture; |
25 |
import flash.geom.Point; |
26 |
import flash.geom.Rectangle; |
27 |
import flash.geom.Matrix; |
28 |
|
29 |
public class LiteSpriteSheet |
30 |
{
|
31 |
internal var _texture : Texture; |
32 |
|
33 |
protected var _spriteSheet : BitmapData; |
34 |
protected var _uvCoords : Vector.<Number>; |
35 |
protected var _rects : Vector.<Rectangle>; |
36 |
|
37 |
// because the edge pixels of some sprites are bleeding through,
|
38 |
// we zoom in the texture just the slightest bit for terrain tiles
|
39 |
public var uvPadding:Number = 0; // 0.01; |
40 |
|
41 |
public function LiteSpriteSheet(SpriteSheetBitmapData:BitmapData, numSpritesW:int = 8, numSpritesH:int = 8, uvPad:Number = 0) |
42 |
{
|
43 |
_uvCoords = new Vector.<Number>(); |
44 |
_rects = new Vector.<Rectangle>(); |
45 |
_spriteSheet = SpriteSheetBitmapData; |
46 |
uvPadding = uvPad; |
47 |
createUVs(numSpritesW, numSpritesH); |
48 |
}
|
49 |
|
50 |
// generate a list of uv coordinates for a grid of sprites
|
51 |
// on the spritesheet texture for later reference by ID number
|
52 |
// sprite ID numbers go from left to right then down
|
53 |
public function createUVs(numSpritesW:int, numSpritesH:int) : void |
54 |
{
|
55 |
trace('creating a '+_spriteSheet.width+'x'+_spriteSheet.height+ |
56 |
' spritesheet texture with '+numSpritesW+'x'+ numSpritesH+' sprites.'); |
57 |
|
58 |
var destRect : Rectangle; |
59 |
|
60 |
for (var y:int = 0; y < numSpritesH; y++) |
61 |
{
|
62 |
for (var x:int = 0; x < numSpritesW; x++) |
63 |
{
|
64 |
_uvCoords.push( |
65 |
// bl, tl, tr, br
|
66 |
(x / numSpritesW) + uvPadding, ((y+1) / numSpritesH) - uvPadding, |
67 |
(x / numSpritesW) + uvPadding, (y / numSpritesH) + uvPadding, |
68 |
((x+1) / numSpritesW) - uvPadding, (y / numSpritesH) + uvPadding, |
69 |
((x + 1) / numSpritesW) - uvPadding, ((y + 1) / numSpritesH) - uvPadding); |
70 |
|
71 |
destRect = new Rectangle(); |
72 |
destRect.left = 0; |
73 |
destRect.top = 0; |
74 |
destRect.right = _spriteSheet.width / numSpritesW; |
75 |
destRect.bottom = _spriteSheet.height / numSpritesH; |
76 |
_rects.push(destRect); |
77 |
}
|
78 |
}
|
79 |
}
|
80 |
|
81 |
// when the automated grid isn't what we want
|
82 |
// we can define any rectangle and return a new sprite ID
|
83 |
public function defineSprite(x:uint, y:uint, w:uint, h:uint) : uint |
84 |
{
|
85 |
var destRect:Rectangle = new Rectangle(); |
86 |
destRect.left = x; |
87 |
destRect.top = y; |
88 |
destRect.right = x + w; |
89 |
destRect.bottom = y + h; |
90 |
_rects.push(destRect); |
91 |
|
92 |
_uvCoords.push( |
93 |
destRect.x/_spriteSheet.width, destRect.y/_spriteSheet.height + destRect.height/_spriteSheet.height, |
94 |
destRect.x/_spriteSheet.width, destRect.y/_spriteSheet.height, |
95 |
destRect.x/_spriteSheet.width + destRect.width/_spriteSheet.width, destRect.y/_spriteSheet.height, |
96 |
destRect.x/_spriteSheet.width + destRect.width/_spriteSheet.width, destRect.y/_spriteSheet.height + destRect.height/_spriteSheet.height); |
97 |
|
98 |
return _rects.length - 1; |
99 |
}
|
100 |
|
101 |
public function removeSprite(spriteId:uint) : void |
102 |
{
|
103 |
if ( spriteId < _uvCoords.length ) { |
104 |
_uvCoords = _uvCoords.splice(spriteId * 8, 8); |
105 |
_rects.splice(spriteId, 1); |
106 |
}
|
107 |
}
|
108 |
|
109 |
public function get numSprites() : uint |
110 |
{
|
111 |
return _rects.length; |
112 |
}
|
113 |
|
114 |
public function getRect(spriteId:uint) : Rectangle |
115 |
{
|
116 |
return _rects[spriteId]; |
117 |
}
|
118 |
|
119 |
public function getUVCoords(spriteId:uint) : Vector.<Number> |
120 |
{
|
121 |
var startIdx:uint = spriteId * 8; |
122 |
return _uvCoords.slice(startIdx, startIdx + 8); |
123 |
}
|
124 |
|
125 |
public function uploadTexture(context3D:Context3D) : void |
126 |
{
|
127 |
if ( _texture == null ) { |
128 |
_texture = context3D.createTexture(_spriteSheet.width, _spriteSheet.height, Context3DTextureFormat.BGRA, false); |
129 |
}
|
130 |
|
131 |
_texture.uploadFromBitmapData(_spriteSheet); |
132 |
|
133 |
// generate mipmaps
|
134 |
var currentWidth:int = _spriteSheet.width >> 1; |
135 |
var currentHeight:int = _spriteSheet.height >> 1; |
136 |
var level:int = 1; |
137 |
var canvas:BitmapData = new BitmapData(currentWidth, currentHeight, true, 0); |
138 |
var transform:Matrix = new Matrix(.5, 0, 0, .5); |
139 |
|
140 |
while ( currentWidth >= 1 || currentHeight >= 1 ) { |
141 |
canvas.fillRect(new Rectangle(0, 0, Math.max(currentWidth,1), Math.max(currentHeight,1)), 0); |
142 |
canvas.draw(_spriteSheet, transform, null, null, null, true); |
143 |
_texture.uploadFromBitmapData(canvas, level++); |
144 |
transform.scale(0.5, 0.5); |
145 |
currentWidth = currentWidth >> 1; |
146 |
currentHeight = currentHeight >> 1; |
147 |
}
|
148 |
}
|
149 |
} // end class |
150 |
} // end package |
As you can see, the only changes above are related to the uvPadding
parameter in the class constructor for each spritesheet.
Step 21: Final Upgrades!
We're nearly done! All we need to do now is upgrade the existing Main.as
in our project to account for the various minor changes to the way we create new instances of our entity manager and their spritesheets. The changes to this file are trivial, but it is included here in full to avoid confusion.
The primary differences include the fact that we are now embedding the two main spritesheets here rather than in the entity manager, the addition of a terrain layer, dealing with two different scrolling speeds (since we want the terrain to move slower than the enemies in the foreground, and triggering the new levels to be loaded.
1 |
|
2 |
// Stage3D Shoot-em-up Tutorial Part 4
|
3 |
// by Christer Kaitila - www.mcfunkypants.com
|
4 |
// Created for active.tutsplus.com
|
5 |
|
6 |
package
|
7 |
{
|
8 |
[SWF(width = "600", height = "400", frameRate = "60", backgroundColor = "#000000")] |
9 |
|
10 |
import flash.display3D.*; |
11 |
import flash.display.Sprite; |
12 |
import flash.display.StageAlign; |
13 |
import flash.display.StageQuality; |
14 |
import flash.display.StageScaleMode; |
15 |
import flash.events.Event; |
16 |
import flash.events.ErrorEvent; |
17 |
import flash.events.MouseEvent; |
18 |
import flash.geom.Rectangle; |
19 |
import flash.utils.getTimer; |
20 |
|
21 |
public class Main extends Sprite |
22 |
{
|
23 |
// the entity spritesheet (ships, particles)
|
24 |
[Embed(source="../assets/sprites.png")] |
25 |
private var EntitySourceImage : Class; |
26 |
|
27 |
// the terrain spritesheet
|
28 |
[Embed(source="../assets/terrain.png")] |
29 |
private var TerrainSourceImage : Class; |
30 |
|
31 |
// the keyboard control system
|
32 |
private var _controls : GameControls; |
33 |
// don't update the menu too fast
|
34 |
private var nothingPressedLastFrame:Boolean = false; |
35 |
// timestamp of the current frame
|
36 |
public var currentTime:int; |
37 |
// for framerate independent speeds
|
38 |
public var currentFrameMs:int; |
39 |
public var previousFrameTime:int; |
40 |
|
41 |
// player one's entity
|
42 |
public var thePlayer:Entity; |
43 |
// movement speed in pixels per second
|
44 |
public var playerSpeed:Number = 128; |
45 |
// timestamp when next shot can be fired
|
46 |
private var nextFireTime:uint = 0; |
47 |
// how many ms between shots
|
48 |
private var fireDelay:uint = 200; |
49 |
|
50 |
// main menu = 0 or current level number
|
51 |
private var _state : int = 0; |
52 |
// the title screen batch
|
53 |
private var _mainmenu : GameMenu; |
54 |
// the sound system
|
55 |
private var _sfx : GameSound; |
56 |
// the background stars
|
57 |
private var _bg : GameBackground; |
58 |
|
59 |
private var _terrain : EntityManager; |
60 |
private var _entities : EntityManager; |
61 |
private var _spriteStage : LiteSpriteStage; |
62 |
private var _gui : GameGUI; |
63 |
private var _width : Number = 600; |
64 |
private var _height : Number = 400; |
65 |
public var context3D : Context3D; |
66 |
|
67 |
// constructor function for our game
|
68 |
public function Main():void |
69 |
{
|
70 |
if (stage) init(); |
71 |
else addEventListener(Event.ADDED_TO_STAGE, init); |
72 |
}
|
73 |
|
74 |
// called once flash is ready
|
75 |
private function init(e:Event = null):void |
76 |
{
|
77 |
_controls = new GameControls(stage); |
78 |
removeEventListener(Event.ADDED_TO_STAGE, init); |
79 |
stage.quality = StageQuality.LOW; |
80 |
stage.align = StageAlign.TOP_LEFT; |
81 |
stage.scaleMode = StageScaleMode.NO_SCALE; |
82 |
stage.addEventListener(Event.RESIZE, onResizeEvent); |
83 |
trace("Init Stage3D..."); |
84 |
_gui = new GameGUI("Stage3D Shoot-em-up Tutorial Part 3"); |
85 |
addChild(_gui); |
86 |
stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, onContext3DCreate); |
87 |
stage.stage3Ds[0].addEventListener(ErrorEvent.ERROR, errorHandler); |
88 |
stage.stage3Ds[0].requestContext3D(Context3DRenderMode.AUTO); |
89 |
trace("Stage3D requested..."); |
90 |
_sfx = new GameSound(); |
91 |
}
|
92 |
|
93 |
// this is called when the 3d card has been set up
|
94 |
// and is ready for rendering using stage3d
|
95 |
private function onContext3DCreate(e:Event):void |
96 |
{
|
97 |
trace("Stage3D context created! Init sprite engine..."); |
98 |
context3D = stage.stage3Ds[0].context3D; |
99 |
initSpriteEngine(); |
100 |
}
|
101 |
|
102 |
// this can be called when using an old version of flash
|
103 |
// or if the html does not include wmode=direct
|
104 |
private function errorHandler(e:ErrorEvent):void |
105 |
{
|
106 |
trace("Error while setting up Stage3D: "+e.errorID+" - " +e.text); |
107 |
}
|
108 |
|
109 |
protected function onResizeEvent(event:Event) : void |
110 |
{
|
111 |
trace("resize event..."); |
112 |
|
113 |
// Set correct dimensions if we resize
|
114 |
_width = stage.stageWidth; |
115 |
_height = stage.stageHeight; |
116 |
|
117 |
// Resize Stage3D to continue to fit screen
|
118 |
var view:Rectangle = new Rectangle(0, 0, _width, _height); |
119 |
if ( _spriteStage != null ) { |
120 |
_spriteStage.position = view; |
121 |
}
|
122 |
if(_terrain != null) { |
123 |
_terrain.setPosition(view); |
124 |
}
|
125 |
if(_entities != null) { |
126 |
_entities.setPosition(view); |
127 |
}
|
128 |
if(_mainmenu != null) { |
129 |
_mainmenu.setPosition(view); |
130 |
}
|
131 |
}
|
132 |
|
133 |
private function initSpriteEngine():void |
134 |
{
|
135 |
// init a gpu sprite system
|
136 |
//var view:Rectangle = new Rectangle(0,0,_width,_height)
|
137 |
var stageRect:Rectangle = new Rectangle(0, 0, _width, _height); |
138 |
_spriteStage = new LiteSpriteStage(stage.stage3Ds[0], context3D, stageRect); |
139 |
_spriteStage.configureBackBuffer(_width,_height); |
140 |
|
141 |
// create the background stars
|
142 |
trace("Init background..."); |
143 |
_bg = new GameBackground(stageRect); |
144 |
_bg.createBatch(context3D); |
145 |
_spriteStage.addBatch(_bg.batch); |
146 |
_bg.initBackground(); |
147 |
|
148 |
// create the terrain spritesheet and batch
|
149 |
trace("Init Terrain..."); |
150 |
_terrain = new EntityManager(stageRect); |
151 |
_terrain.SourceImage = TerrainSourceImage; |
152 |
_terrain.SpritesPerRow = 16; |
153 |
_terrain.SpritesPerCol = 16; |
154 |
_terrain.defaultSpeed = 90; |
155 |
_terrain.defaultScale = 1.5; |
156 |
_terrain.levelTilesize = 48; |
157 |
_terrain.createBatch(context3D, 0.001); // a little UV padding required |
158 |
_spriteStage.addBatch(_terrain.batch); |
159 |
_terrain.level.loadLevel('terrain0'); // demo level NOW |
160 |
|
161 |
// create a single rendering batch
|
162 |
// which will draw all sprites in one pass
|
163 |
trace("Init Entities..."); |
164 |
_entities = new EntityManager(stageRect); |
165 |
_entities.SourceImage = EntitySourceImage; |
166 |
_entities.defaultScale = 1.5; // 1 |
167 |
_entities.levelTilesize = 48; |
168 |
_entities.createBatch(context3D); |
169 |
_entities.sfx = _sfx; |
170 |
_spriteStage.addBatch(_entities.batch); |
171 |
_entities.level.loadLevel('level0'); // demo level NOW |
172 |
_entities.streamLevelEntities(true); // spawn first row of the level immediately |
173 |
|
174 |
// create the logo/titlescreen main menu
|
175 |
_mainmenu = new GameMenu(stageRect); |
176 |
_mainmenu.createBatch(context3D); |
177 |
_spriteStage.addBatch(_mainmenu.batch); |
178 |
|
179 |
// tell the gui where to grab statistics from
|
180 |
_gui.statsTarget = _entities; |
181 |
|
182 |
// start the render loop
|
183 |
stage.addEventListener(Event.ENTER_FRAME,onEnterFrame); |
184 |
|
185 |
// only used for the menu
|
186 |
stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDown); |
187 |
stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMove); |
188 |
}
|
189 |
|
190 |
public function playerLogic(seconds:Number):void |
191 |
{
|
192 |
var me:Entity = _entities.thePlayer; |
193 |
me.speedY = me.speedX = 0; |
194 |
if (_controls.pressing.up) |
195 |
me.speedY = -playerSpeed; |
196 |
if (_controls.pressing.down) |
197 |
me.speedY = playerSpeed; |
198 |
if (_controls.pressing.left) |
199 |
me.speedX = -playerSpeed; |
200 |
if (_controls.pressing.right) |
201 |
me.speedX = playerSpeed; |
202 |
|
203 |
// keep on screen
|
204 |
if (me.sprite.position.x < 0) |
205 |
me.sprite.position.x = 0; |
206 |
if (me.sprite.position.x > _width) |
207 |
me.sprite.position.x = _width; |
208 |
if (me.sprite.position.y < 0) |
209 |
me.sprite.position.y = 0; |
210 |
if (me.sprite.position.y > _height) |
211 |
me.sprite.position.y = _height; |
212 |
|
213 |
//
|
214 |
// leave a trail of particles
|
215 |
_entities.particles.addParticle(63, |
216 |
me.sprite.position.x - 12, |
217 |
me.sprite.position.y + 2, |
218 |
0.75, -200, 0, 0.4, NaN, NaN, -1, -1.5); |
219 |
}
|
220 |
|
221 |
private function mouseDown(e:MouseEvent):void |
222 |
{
|
223 |
trace('mouseDown at '+e.stageX+','+e.stageY); |
224 |
if (_state == 0) // are we at the main menu? |
225 |
{
|
226 |
if (_mainmenu && _mainmenu.activateCurrentMenuItem(getTimer())) |
227 |
{ // if the above returns true we should start the game |
228 |
startGame(); |
229 |
}
|
230 |
}
|
231 |
}
|
232 |
|
233 |
private function mouseMove(e:MouseEvent):void |
234 |
{
|
235 |
if (_state == 0) // are we at the main menu? |
236 |
{
|
237 |
// select menu items via mouse
|
238 |
if (_mainmenu) _mainmenu.mouseHighlight(e.stageX, e.stageY); |
239 |
}
|
240 |
}
|
241 |
|
242 |
// handle any player input
|
243 |
private function processInput():void |
244 |
{
|
245 |
if (_state == 0) // are we at the main menu? |
246 |
{
|
247 |
// select menu items via keyboard
|
248 |
if (_controls.pressing.down || _controls.pressing.right) |
249 |
{
|
250 |
if (nothingPressedLastFrame) |
251 |
{
|
252 |
_sfx.playGun(1); |
253 |
_mainmenu.nextMenuItem(); |
254 |
nothingPressedLastFrame = false; |
255 |
}
|
256 |
}
|
257 |
else if (_controls.pressing.up || _controls.pressing.left) |
258 |
{
|
259 |
if (nothingPressedLastFrame) |
260 |
{
|
261 |
_sfx.playGun(1); |
262 |
_mainmenu.prevMenuItem(); |
263 |
nothingPressedLastFrame = false; |
264 |
}
|
265 |
}
|
266 |
else if (_controls.pressing.fire) |
267 |
{
|
268 |
if (_mainmenu.activateCurrentMenuItem(getTimer())) |
269 |
{ // if the above returns true we should start the game |
270 |
startGame(); |
271 |
}
|
272 |
}
|
273 |
else
|
274 |
{
|
275 |
// this ensures the menu doesn't change too fast
|
276 |
nothingPressedLastFrame = true; |
277 |
}
|
278 |
}
|
279 |
else
|
280 |
{
|
281 |
// we are NOT at the main menu: we are actually playing the game
|
282 |
// in future versions we will add projectile
|
283 |
// spawning functinality here to fire bullets
|
284 |
if (_controls.pressing.fire) |
285 |
{
|
286 |
// is it time to fire again?
|
287 |
if (currentTime >= nextFireTime) |
288 |
{
|
289 |
//trace("Fire!");
|
290 |
nextFireTime = currentTime + fireDelay; |
291 |
_sfx.playGun(1); |
292 |
_entities.shootBullet(3); |
293 |
}
|
294 |
}
|
295 |
}
|
296 |
}
|
297 |
|
298 |
private function startGame():void |
299 |
{
|
300 |
trace("Starting game!"); |
301 |
_state = 1; |
302 |
_spriteStage.removeBatch(_mainmenu.batch); |
303 |
_sfx.playMusic(); |
304 |
// add the player entity to the game!
|
305 |
thePlayer = _entities.addPlayer(playerLogic); |
306 |
// load level one (and clear demo entities)
|
307 |
_entities.changeLevels('level1'); |
308 |
_terrain.changeLevels('terrain1'); |
309 |
}
|
310 |
|
311 |
// this function draws the scene every frame
|
312 |
private function onEnterFrame(e:Event):void |
313 |
{
|
314 |
try
|
315 |
{
|
316 |
// grab timestamp of current frame
|
317 |
currentTime = getTimer(); |
318 |
currentFrameMs = currentTime - previousFrameTime; |
319 |
previousFrameTime = currentTime; |
320 |
|
321 |
// erase the previous frame
|
322 |
context3D.clear(0, 0, 0, 1); |
323 |
|
324 |
// for debugging the input manager, update the gui
|
325 |
_gui.titleText = _controls.textDescription(); |
326 |
|
327 |
// process any player input
|
328 |
processInput(); |
329 |
|
330 |
// scroll the background
|
331 |
if (_entities.thePlayer) _bg.yParallax(_entities.thePlayer.sprite.position.y / _height); |
332 |
_bg.update(currentTime); |
333 |
|
334 |
// update the main menu titlescreen
|
335 |
if (_state == 0) |
336 |
_mainmenu.update(currentTime); |
337 |
|
338 |
// move/animate all entities
|
339 |
_terrain.update(currentFrameMs); |
340 |
_entities.update(currentFrameMs); |
341 |
|
342 |
// keep adding more sprites - IF we need to
|
343 |
_terrain.streamLevelEntities(false); |
344 |
_entities.streamLevelEntities(true); |
345 |
|
346 |
// draw all entities
|
347 |
_spriteStage.render(); |
348 |
|
349 |
// update the screen
|
350 |
context3D.present(); |
351 |
}
|
352 |
catch (e:Error) |
353 |
{
|
354 |
// this can happen if the computer goes to sleep and
|
355 |
// then re-awakens, requiring reinitialization of stage3D
|
356 |
// (the onContext3DCreate will fire again)
|
357 |
}
|
358 |
}
|
359 |
} // end class |
360 |
} // end package |
We're done! Compile your project, fix any typos, and run the game. If you're having trouble with the code you typed in or just want the instant gratification of everything in one place, remember that you can download the full source code here.
Here are a few tips for if you experience problems:
- If you do use FlashBuilder, be sure to include "
-default-frame-rate 60
" in your compiler options to ensure you get the best performance. - If you are using Linux or a Mac, you can compile this from the command-line (or in a makefile) using something similar to "
mxmlc -load-config+=obj\shmup_tutorial_part4Conf