Pro Evolution Soccer 2008 for the Wii
Soccer video games have not changed very much since the the first 3D version of FIFA appeared on the 3DO almost 15 years ago. The graphics and models have gotten much better, and the licensed player names have improved, but the actual game-play has stayed (relatively) the same. The controls of nearly all modern soccer games go something like this:
- 1 button passes
- 1 button lob passes
- 1 button shoots
- 1 button is used to switch control to player closest to the ball
- 1 button is designed to be mashed as quickly as possible to make the above player run as fast as possible to or with the ball
- 1 of the above buttons is used to volley the ball from a pass
- Defense consists of the above running and switching of players, plus an abundance of slide-tackling
Of course there are other controls, many of which use shoulder-buttons and combos that are nearly impossible to remember lest actually execute in heat of the game. After the initial learning curve of each new game is completed, play usually falls into a rut where you use one or two "super-man" players who do nearly everything on the field, taking shots at the goal from a set of standard positions that you know have a high probability of getting you a tick on scoreboard. While this can be fun for a while, it's really not soccer at all, no matter what the Cockney accented color commentator would have you believe. The real problem is that the interface to the game (the gamepad) does not allow for the complex interactions that make soccer an interesting sport to watch and play. The games simply capture a shadow of what really makes a soccer match a great contest: the immersive nuances in the run of play.
Because of these limitations designers have created games that eshued nuance altogether. They place games in dark alleys with power-ups and weapons, or on mini-fields where the proper combo can launch 8 balls at an unsuspecting goalie. They make rarities in an actual game (like a bicycle-kick) into sought-after power-ups and special moves that replace tactics with gimmicks. Sure, they make the game fun to play, but they also pave over the actual game of soccer in the process. Sadly however, without a new way to implement the basics of the game, it seemed like soccer games had gotten just about as good as they could possibly get. The game could not get any more immersive as long the control scheme stayed the same.
On a tip from the weekly IGN Wii Podcast I picked-up Pro Evolution Soccer 2008 for the Wii last week. The podcast (and subsequent reviews) told a very intriguing story about a new control scheme for a soccer game that could only be accomplished on the Wii with a Wiimote. To me, this seemed like it could be the answer to immersion problem. The reviewers described the game as using a "John Madden tele-strator"-like interface. Even though these descriptions made it sound like the game would be played in slow motion, I was intrigued enough to buy the game and see for myself.
When Pro Evolution Soccer 2008 for the Wii starts for the first-time, you are thrust into a tutorial about the controls. This is appropriate because the controls are like nothing I have ever experienced before in any kind of sports game. All action on the screen is directed using the Wiimote and the Nunchuck, but not in any garden-variety way. Like other soccer games, you have direct control of one-player at a time. By pressing down the (A) button, an arrow appears on the screen. By controlling the length and direction of that arrow, you control the player. It might sound weird at first, but after a couple tries is appears to work almost flawlessly. Instead of mashing a button to run, pressing a shoulder-button for a step-over, and controlling the player movement with an analog stick, you (almost) effortlessly glide the controlled player through the defensive-line and into scoring position. All the way you are weaving, dribbling, stepping-over, etc, but these tactics come from intuitive flicks of wrist instead of multiple button combos. Shooting the ball at the goal comes from a flick of the Nunchuck. This itself is significant, as it actually separates shooting from passing and dribbling: something that most other soccer games get completely wrong. By separating shooting to it's own unique action, it becomes much harder to make mistakes in-front of the net. This should be welcome news to anyone who has played a soccer game in the past and has furiously yelled at the TV to "shoot shoot shoot damn it" only to realize they have been shttoing at all, but repeatedly telling the game to "lob" the ball back to the wing.
While improving the control of single player is welcome, in and of itself it is not enough of an improvement to warrant calling this game "revolutionary". You might be thinking: "Sure, you can shoot easier, but how does that offer more immersion and nuances than a gamepad? In fact, it seems like the gamepad might be more flexible and nuance than the Wiimote." If the improvements in single-player control were all that Pro Evolution Soccer 2008 had to offer, then these thoughts would be correct. however, it is the passing game truly sets this game above all that have come before it. In most other soccer games, passing the ball is relatively "magical" process. Since you can only control one player, you must rely on A.I. to direct the other players on where to stand and when to run for a pass. While some games offer a modicum of control of the player that will be passed the ball, going beyond single passes, one-twos or pass-volleys is nearly impossible. Those games take a full-field game of soccer, and crunch it down to a series of one-on-ones and one-on- two and match-ups. It's like a mini game of one on one basketball on a giant green field. However soccer is not basketball, and the makers Pro Evolution Soccer 2008 for the Wii figured out a way to take the essential but seemingly simple tactic of passing the ball in soccer revolutionize it.
By pressing the (B) button on the Wiimote an second arrow appears. By clicking on another player, while pressing (B) you will pass them the ball. simple right? How is that revolutionary? Well, here comes the best part. Before you pass the ball, you can press the (B) button over more players. This does not cancel-out your first pass, it adds to it. Very quickly you will find yourself lining-up 3, 4 and 5 pass plays that result in shots on goal. If you press both (A) and (B) at the same time, you can direct players into one-two plays around defenders. You can even direct players to run for an open space to receive a leading pass. As far as I know, this has been essentially impossible, or at least improbable with other soccer games. I might have accomplished these feats a few times with all other games combined in my lifetime, but with Pro Evolution Soccer 2008 for the Wii I can make them happen on every play. Furthermore set-piece passes can be set-up in much the same way. With other soccer games, a corner-kick was most likely a "prayer" pass while mashing the "shoot" button for a hopeful volley into the net. While you can still do that with this game, a bit more careful planning will lead you to directing a corner kick volley as pass to a 3rd player and possibly a 4th before swinging the Nunchuck for a shot on goal. The results are truly astonishing. All of a sudden you will find yourself using the entire field to play a soccer video game.
While offense is modeled amazingly well in Pro Evolution Soccer 2008, defense, while still good, doesn't offer the same significant level improvements. You can mark specific players, direct players to intercept passes, call an off sides trap, direct the goalie to come off his line, and call for slide-tackles. It's all fine, but simply not as immersive or enjoyable as offense. Some people might argue such is the nature of defense, and I'd tend to agree, if it was not for the nagging want to gain control a single players and go after those any bastard that tries to attack my goal! Still, defense if certainly not a deal breaker, and as far as staying with the intended game design, I could not think of a better implementation.
The game offers a slew of play options and modes. A wi-fi online mode is available, but the most enjoyable mode to me is called "Champions Road". This option allows you to select a team, and play in a series of tournaments of increasing difficulty. After every game your players increase in their abilities, and if you win, you get the chance to pinch the best players from the other team. In this way, you get to mold and form your team as the you play the game and immerse yourself in the details of team management. The significant immersion and nuance of on-field play, added to this addictive and interesting tournament mode, make this one of the best soccer games available today. If you even think you might like to play a soccer game, but have been put-off or frustrated by the controls of earlier soccer games, be sure to check this one out. Pro Evolution Soccer 2008 for the Wii offers the type of innovative controls and game play that I expected from the Wii in 2006, but slogged through 2007 without finding. I'm happy to see that in 2008 developers are finally finding ways to move Wii's unique control scheme away from hand flipping mini-game gimmicks, and towards new and innovative methods to control and immerse the player into games that I once (mistakenly) thought had reached their apex.
Flash Game Development Inter-web mash up : Mar. 30, 2008
The latest in Blog entries and articles that might interest Flash game developers.
Forrest's AS3 / Flash Blog has an entry that is right up my alley. It is entitled What is the fastest way to draw pixels in AS3? He found that the fastest method was to call setPixel once per pixel using y as the outer loop and x for the inner loop. He also locks the output BitmapData object before the loops and unlocks afterward. He was able to get 20FPS writing out an 800x600 bitmap on every frame tick. Take a look at the entire article, as there is a lot of good work involved.
A brand spankin' new site has launched, and by god its not another game portal that won't accept my games. It is great new site called Game Poetry, and its first entry is devoted to making games for FREE on the Flash platform - primarily using Flash Develop and Flex.
http://www.jesshansen.com/ returns to this round-up with a very nice tutorial on creating a shattered glass effect in AS3. He has the class available for YOU to use right now, so go check it out.
I finished the 4th and final part of my Atari 7800 Asteroids Tutorial. In the final part, we build in pixel perfect collisions, and a simple particle emitter and object pool to draw particles from.
Midcoregamer.com Midcore RSS Aggregation Features
Midcoregamer.com has been scouring the web for the past few weeks to locate blogs that are relevant to the Midcore audience. We found an increasing number of homegrown bloggers that have taken the concept of the "midcore gamer" (as well has other like designations) to heart and have decided to collect their respective RSS feeds on our home page. Now you can find out the current state of the "Midcore World" with one look at http://www.midcoregamer.com. Some of the blogs we are now featuring include:
- Seanbajuice - Mid-Core Gamer Reviews and Tech News
- Hardcore Casual
- Hardcasual
- CasualHardcore
As well, we have added feeds for some other like-minded sites that are not necessarily "mid-core"
- Gaming With Children
- GAMEparent
- Armchair Arcade
You can read all the latest blog entries from this growing segment of the blogosphere by visiting http://www.midcoregamer.com.
Also, if you have know of another like-minded blog, or would like us to review your own blog to see if it would be appropriate for the home page of midcoregamer.com, please leave a message below, or email us at info@8bitrocket.com
Tutorial: Using Flash CS3 and Actionscript 3 to create Atari 7800 Asteroids Part 4
Tutorial: Using Flash CS3 and Actionscript 3 to create Atari 7800 Asteroids Part 4
In this 4th and final part we will cover 2 main topics. First, we will use pixel perfect BitmapData collision detection to detect missile and ship collisions with the rocks. And second, we will create a very simple particle engine and use a pre-created farm/pool of particles to draw from. This will allow us to have some nice particle effects but also keep optimization in mind.
In Part 3 we covered firing missiles from the player ship. We used a png sprite sheet for the missile animation and demonstrated how to blit the missiles to the BitmapData canvas.
In Part 2 we covered adding the asteroids to the screen. The asteroid animation was cached into an array in a different manner then the ship rotation. With the asteroids, we took the frames of a timeline animation and drew each one into a BitmapData image that was placed into an array.
In Part 1 we covered creating an array of pre-calculated vector values for animating our player ship. We also covered caching the rotated frames of the ship in an array of BitmapData objects. We also added the ship to the screen and move it with the keyboard.
First, here is the the final game we will create. It isn't fully fleshed out game-wise, but it contains all of the basic elements necessary for you to apply to your own games. There is only 1 level, and the ship will not die. It will collide with rocks and provide a particle explosion though. In the upper part of the screen you will see Active Graph showing the game's current memory usage. When you click to start a new game, I don't explicitly null out all of the created objects, so you will see a slight increase in memory use on 2nd and subsequent plays. If this were a real game, I would set every object to null and call dispose() on all BitmapData objects. You will also see a frameRate counter next to the Active Graph. It is included in the zip file below.
Arrow keys rotate left and right and the up thrusts forward. The [z] key fires. The the window is embeded with a wmode=window. It will work at about 5 more FPS if I could use use the transparent or opaque settings. I don't because some browsers will not work with with it yet.. The below example starts with 20 rocks and each use up 40 particles for each explosion. Even using the BitmapData collision detection (which uses more resources than math-based) we can still keep a pretty constant 30 FPS frame rate. The .fla and all class files are provide so you can play with it and change things around if you like.
The source files are here: 7800 Asteroids Tutorial Part 4 source files
In the first 3 parts of this tutorial I have exhaustively gone through all of the code for each section of the game. I am not going to do that in part 4. My reasoning is simple. I don't think the tutorials layout has been as effective as they can be up to this point. The code box has been easy to read, but the explanations have not. I am currently fiddling with the layout and content of this an future tutorials, so I will do this one a little different. I will explain in detail as much as I feel is sufficient, but I will focus on a little more of the details behind these two theories. I definitely will show the code here, but you will need to unzip the files and play with them to see absolutely everything in action. I have added in some optimizations in this version that I will also explain at the end.
BitmapData pixel perfect collision detection
The theory about collision detection up to this point (especially in Flash) has been that math based detection is the best manner in which to detect. I agree with this, but I have seen some pretty un optimized math based collisions in my time. This is especially true if you are going to use the new Point() class and check the radius of each object against the other objects to see if they overlap. While this is an excellent way to do collisions, most new AS3 programmers make the mistake of in-line creating new Point objects on every frame tick. This wastes a lot of execution time because Object creation is very time consuming in Actionscript. Now, I'm not saying that checking each non-transparent pixel on a BitmapData object is faster, but as you will see it works fine and maintains a decent frame rate. So, since I wanted to try and make a game with pixel perfect collision detection I went ahead with this later route. What I am saying here really is no matter what type of collision detection you choose, if you write un-optimized code, it can perform worse than you might have imagined.
BitmapData objects are basically a collection of colored and transparent pixels. When two BitmapData objects overlap, if any non-transparent pixels are in the overlapped portion, a hit will be detected. This is very different from the standard Bounding Box to Bounding Box and Bounding Box to single screen point that is available with standard display objects. Now, you can also test pixel perfect collisions between a BitmapData object and a Sprite or MovieClip, but we will keep it simple as check to BitmapData objects against one another.
References: A Paradox with BitmapData.hitTest?
Keep in mid that when you create a BitmapData object from another BitmapData object through the assignment operation (=), you have just created a reference to the original BitmapData object. You don't physically have two different pieces of BitmapData, but two references to the same object. This is important because our game is made up of a lot of objects that all share the same array of BitmapData objects for display. If we were to modify one of the frames of animation for an asteroid on the fly somehow (by setting a pixel color for instance), all of the asteroids would get the change because they all share the same original BitmapData objects for each animation frame. Since all of the asteroids share the same array of BitmapData objects, detecting collisions between objects based on the BitmapData of each object would seem impossible, but it isn't. You see, if you remember from earlier parts of this tutorial, on each frame tick, an object might animate by changing to the next frame of BitmapData in the shared array of a animation frames. Each asteroid holds just a index int that represents the location in the array of asteroid frames to copyPixels from for display to the canvas. There is no way we can use the same index and reference for collision detection. So, we simply need to make sure that each asteroid also contains another variable called bitmapData. This variable will hold a reference to the current BitmapData object that is displayed by the asteroid object. It doesn't sound logical, but it works. If you have any experience with the BitmapData object, you might think that you need to use the clone() method to create a new object for the hitTest, but you do not. I was surprised by this myself, and will keep exploring how this might be beneficial in the future.
This new checkCollisions() method is below. I will not be discussing each line of the code but the most significant functionality will be discussed in detail.
[cc lang="javascript" width="550"]
private function checkCollisions():void {
missileLen=aMissile.length-1;
rockLen=aRock.length-1;
rocks: for (rockCtr=rockLen;rockCtr>=0;rockCtr--) {
tempRock=aRock[rockCtr];
rockPoint.x=tempRock.x;
rockPoint.y=tempRock.y;
missiles: for (missileCtr=missileLen;missileCtr>=0;missileCtr--) {
tempMissile=aMissile[missileCtr];
missilePoint.x=tempMissile.x;
missilePoint.y=tempMissile.y;
//trace("1");
try{
if (tempMissile.bitmapData.hitTest(missilePoint,255,tempRock.bitmapData,rockPoint,255)) {
// trace("hit!");
createExplode(tempRock.x+18,tempRock.y+18);
tempMissile=null;
tempRock=null;
aMissile.splice(missileCtr,1);
aRock.splice(rockCtr,1);
break rocks;
break missiles;
}
}catch(e:Error) {
trace("error in missle test");
}
}
//trace("2");
playerPoint.x=playerObject.x;
playerPoint.y=playerObject.y;
try{
if (tempRock.bitmapData.hitTest(rockPoint,255,playerObject.bitmapData,playerPoint,255)){
//trace("ship hit");
createExplode(tempRock.x+18,tempRock.y+18);
tempRock=null;
aRock.splice(rockCtr,1);
}
}catch(e:Error) {
trace("error in ship test");
}
}
//trace("3");
}
[/cc]
Labels
We need to loop through all of the rocks and all of the missiles to see if they hit another. We don't care if the missiles hit other missiles, and we don't care if rocks hit other rocks, but we do care of rocks hit the playerShip. For that reason, we loop through the rocks in our outer loop and the missiles in our inner loop. If we looped through the missiles as our outer loop, then for each missile we would need to check each rock AND also check the ship against each rock. That would be a waste of time, so we loop through each rock in the outer loop - first looking to see if a rock has hit a missile, and if it does NOT, then check the rock against the ship. We do this be setting up two labels called:
rocks:
missiles:
You can use labels in nested loops to help discern the loop you want to break out of. We do that here when a rock and a missile collide. The break rocks; and break missiles; lines tell the run-time to stop looking at this rock and this missile and start with the next in each loop.
Looping Backward
Why the heck does Jeff loop backward through his arrays? Is he one of the THOSE people who can use ++i and i++ correctly? Sadly, I cannot, but that is beside the point. We loop backward through the rocks and the missiles because we need to splice them out of their respective arrays when a collision is detected. If we looped forward through the array, and we had to splice say the 10th element in the missile array, something unexpected would happen. We would actually SKIP checking the 11th element in our array. That is because if the missileCtr is on 10 and we splice the element at index 10 in our array, right away, the 11th element shifts into the 10th spot (and all other elements after 11 shift one spot also). The missileCtr would then increase from 10 to 11, and the former element at position 11 (now at 10) is never checked. By looping backward through the array, when we splice, we don't cause this same problem. If we splice the 10th element in the array, 11 certainly moves in to fill its place, but the next one we check is the element in position 9 (we are looping backward), so we never skip an element.
The BitmpData.hittest method.
[cc lang="javascript" width="550"]if (tempMissile.bitmapData.hitTest(missilePoint,255,tempRock.bitmapData,rockPoint,255))
[/cc]
The hitTest() method of the BitmapData object is a powerful one. When called on a BitmpData object, it looks at all of the pixels on the object to see if they are overlapping with pixels on the other object. If those pixels are not transparent (or don't fall over the opacity threshold value) and hit is detected. As an optimization, I created generic point objects called rockPoint and missilePoint as global class variables. By doing so, I use a little extra memory up front, but don't have to instantiate the point objects on each hitTest. The missilePoint and rockPoint must be the upper left-hand corner of each object on the scene in global coordinates. The 255 basically tells the hitTest that NO alpha areas are to be considered opaque for the rock. Setting this number lower we change areas with alpha values above it to be considered opaque for this test.
The tempMissle.bitmapData and tempRock.bitmpaData hold the current objects referenced to the bitmapData needed for the hitTest. It is essential that this be changed each time the BitmapData of the object changes.
For example, in the code below, I have modified the previous drawMissiles methods from Part 3 to include one more line at the bottom. (see the //added in part 4 section).
[cc lang="javascript" width="550"]
private function drawMissiles():void {
missileLen=aMissile.length-1;
for each (tempMissile in aMissile) {
//trace("tempRock.animationIndex=" + tempRock.animationIndex);
//trace("aAsteroidAnimation[tempRock.animationIndex]=" + aAsteroidAnimation[tempRock.animationIndex].width);
missilePoint.x=tempMissile.x;
missilePoint.y=tempMissile.y;
canvasBD.copyPixels(aMissileAnimation[tempMissile.animationIndex],missileRect, missilePoint);
tempMissile.animationIndex++;
if (tempMissile.animationIndex > missileArrayLength-1) {
tempMissile.animationIndex = 0;
}
//added in part 4
tempMissile.bitmapData=aMissileAnimation[tempMissile.animationIndex];
}
}[/cc]
//added in part 4
tempMissile.bitmapData=aMissileAnimation[tempMissile.animationIndex
As we update the missile on the screen we also change the piece of BitmapData that used to display it on the screen. (it animates from red to yellow). When this change is made, we store a reference to the new BitmapData object representing the animation in our tempMissile.bitmapData variable.
We have added similar lines to the the drawRocks() method (see the bottom).
[cc lang="javascript" width="550"]
private function drawRocks() {
rockLen=aRock.length-1;
for each (tempRock in aRock) {
//trace("tempRock.animationIndex=" + tempRock.animationIndex);
//trace("aAsteroidAnimation[tempRock.animationIndex]=" + aAsteroidAnimation[tempRock.animationIndex].width);
rockPoint.x=tempRock.x;
rockPoint.y=tempRock.y;
canvasBD.copyPixels(aAsteroidAnimation[tempRock.animationIndex],rockRect, rockPoint);
tempRock.frameCount++;
if (tempRock.frameCount > tempRock.frameDelay){
tempRock.animationIndex++;
if (tempRock.animationIndex > asteroidFrames-1) {
tempRock.animationIndex = 0;
}
tempRock.frameCount=0;
//added in part 4
tempRock.bitmapData=aAsteroidAnimation[tempRock.animationIndex];
}
}
}
[/cc]
For the playerObject. we needed to add the code only when the frame of animation changed by turning the ship:
[cc lang="javascript" width="550"]
private function checkKeys():void {
if (aKeyPress[38]){
//trace("up pressed");
playerObject.dx=aRotation[playerObject.arrayIndex].dx;
playerObject.dy=aRotation[playerObject.arrayIndex].dy;
var mxn:Number=playerObject.movex+playerObject.acceleration*(playerObject.dx);
var myn:Number=playerObject.movey+playerObject.acceleration*(playerObject.dy);
var currentSpeed:Number = Math.sqrt ((mxn*mxn) + (myn*myn));
if (currentSpeed < playerObject.maxVelocity) {
playerObject.movex=mxn;
playerObject.movey=myn;
} // end speed check
}
if (aKeyPress[37]){
playerObject.arrayIndex--;
if (playerObject.arrayIndex <0) playerObject.arrayIndex=shipAnimationArrayLength-1;
//added in part 4
playerObject.bitmapData=aShipAnimation[playerObject.arrayIndex];
}
if (aKeyPress[39]){
playerObject.arrayIndex++;
if (playerObject.arrayIndex ==shipAnimationArrayLength) playerObject.arrayIndex=0;
//added in part 4
playerObject.bitmapData=aShipAnimation[playerObject.arrayIndex];
}
//*** added for part 3
if (aKeyPress[90]){
fireMissile();
}
}
[/cc]
You can se in the above code we simply added
playerObject.bitmapData=aShipAnimation[playerObject.arrayIndex]
when a change is made to the animationIndex.
So, that's basically pixel perfect collision detection between bitmapData objects in a nut shell. You need 4 pieces of data:
1. A reference to the bitmapData object for the first object you want to check.
2. A reference to the bitmapData object for the second object you want to check.
3. A point object representing the current upper left corner x and y values of the first object.
4. A point object representing the current upper left corner x and y values for the second object.
I usually leave the Alpha threshold values at 255, but I can see a need for them with more complicated objects. So, here is what the code looks like for the hitTest between a rock and the playerObject:
[cc lang="javascript" width="550"]
playerPoint.x=playerObject.x;
playerPoint.y=playerObject.y;
try{
if (tempRock.bitmapData.hitTest(rockPoint,255,playerObject.bitmapData,playerPoint,255)){
//trace("ship hit");
createExplode(tempRock.x+18,tempRock.y+18);
tempRock=null;
aRock.splice(rockCtr,1);
}
}catch(e:Error) {
trace("error in ship test");
}
[/cc]
This is very similar to the check between the rocks and the missiles. When a rock is hit by either the ship or a missile, we set the tempRock to be null, splice it from our array of rocks (we do this with the missile and the missile array when a rock - missile collision is detected also), and then we create an explosion.
createExplode(tempRock.x+18,tempRock.y+18);
The new createExplode function takes two parameter and x and y value. Here, since the x and y values for the rock are the upper left hand corner, and the rock is a 36x36 bitmap, we add 18 to the x and y before we pass them in. That will put the explosion roughly in the middle of the rock.
The Particle Explosion and Particle Pool/Farm
Since the particles that make up our explosion are merely for decoration, I have decided to limit the number available for display. I have done this through the implementation of a farm or pool or particles. This is a set of pre-created particle objects in an array called aFarmParticle. A the beginning of the game, I pre-create 500 particles objects and place them in this array. The code is below:
[cc lang="javascript" width="550"]
private function createFarmParticles() {
var particleCtr:int;
for (particleCtr=0;particleCtr<maxParticles;particleCtr++) {
var tempPart:Object={};
tempPart.lifeCount=0;
tempPart.life=0;
tempPart.x=0;
tempPart.y=0;
tempPart.dx=0;
tempPart.dy=0;
tempPart.speed=0;
tempPart.bitmapData=null;
aFarmParticle.push(tempPart);
}
}
[/cc]
I have a maxParticles variable set to 500 in my variable definition section. This code merely loops 500 times, creates some dummy particles and places them in the array for later use. This way, I don't have to create particles on fly, thus saving valuable execution time.
When a particle explosion is needed, we call the createExplode function. This function will start a loop and try to move maxPartsPerExplode (currently set to 40) from the aFarmParticle to the aActiveParticle array.
[cc lang="javascript" width="550"]
private function createExplode(xval:Number,yval:Number):void {
//trace("aFarmParticle.length=" + aFarmParticle.length);
//trace("aActiveParticle.length=" + aActiveParticle.length);
for (explodeCtr=0;explodeCtr<maxPartsPerExplode;explodeCtr++) {
farmLen=aFarmParticle.length-1;
if (farmLen >0){
tempPart=aFarmParticle[farmLen];
aFarmParticle.splice(farmLen,1);
tempPart.lifeCount=0;
tempPart.life=int(Math.random()*partMaxLife)+partMinLife;;
tempPart.x=xval;
tempPart.y=yval;
tempPart.speed=(Math.random()*partMaxSpeed)+1;
randIntFrame=int(Math.random()*10);
tempPart.bitmapData=aMissileAnimation[randIntFrame];
randIntVector=int(Math.random()*36);
tempPart.dx=aRotation[randIntVector].dx;
tempPart.dy=aRotation[randIntVector].dy;
aActiveParticle.push(tempPart);
}
}
}
[/cc]
On each iteration through the the loop we first check to make sure our farm/pool length is not 0. If it is above 0, there are particles that can be moved from the farm to the active array. We do this by creating a tempPart (temporary particle) from the last element in the aFarmParticle array. We then splice that particle from the farm, set random properties for the tempPart and add it to the aActiveParticle array.
We set random properties for the movement vector (dx and dy) values by randomly picking a number between 0-36 and them using that as the index for our pre-calculated aRotation array. We also set a random speed, and a random frame from our missile tile sheet (array of BitmapData) aMissileAnimation - because we have those already and they are easy to use. We also set a random life between partMaxLife (currently 50) and partMinLife (currently 10). We do all of this to make the particles have some minor differences and seem more organic. There are many more things we could do, but this is our simple particle emitter.
Now, once we have active particles, we need to update them on each frame. This is very similar to updating the rocks or the missiles. We loop through them, and copy the current bitmapData of the particle to the canvasBD.
[cc lang="javascript" width="550"]
private function drawParts():void {
activeLen=aActiveParticle.length-1;
for (partCtr=activeLen;partCtr>=0;partCtr--) {
removePart=false;
tempPart=aActiveParticle[partCtr];
tempPart.x+=tempPart.dx*tempPart.speed;;
tempPart.y+=tempPart.dy*tempPart.speed;
if ((tempPart.x > stage.width) || (tempPart.x < 0)) {
removePart=true;
}
if ((tempPart.y > stage.height) || (tempPart.y < 0)){
removePart=true;
}
tempPart.lifeCount++;
if (tempPart.lifeCount > tempPart.life) {
}
if (removePart) {
aFarmParticle.push(tempPart);
aActiveParticle.splice(partCtr,1);
}else{
//trace("tempRock.animationIndex=" + tempRock.animationIndex);
//trace("aAsteroidAnimation[tempRock.animationIndex]=" + aAsteroidAnimation[tempRock.animationIndex].width);
partPoint.x=tempPart.x;
partPoint.y=tempPart.y;
canvasBD.copyPixels(tempPart.bitmapData,partRect, partPoint);
}
}
}
[/cc]
A particle is removed from the screen is its life count is greater than its life value or it leaves the screen boundaries. We have 500 particles and only a hand full of asteroids on the screen. We will never use all 500 at one time, but this just sets us up for higher level of complex space battles and explosions later in the game. When a particle is removed, it is added back to the aFarmParticle array and spliced from the the aActiveParticle array:
aFarmParticle.push(tempPart);
aActiveParticle.splice(partCtr,1);
So, we have now completed all of the features for this Asteroids game. Our new game loop looks like this:
[cc lang="javascript" width="550"]
private function runGame(e:TimerEvent) {
checkKeys();
updatePlayer();
updateRocks();
updateMissiles();
checkCollisions();
drawBackground();
drawPlayer();
drawRocks();
drawMissiles();
drawParts();
frameTimer.countFrames();
frameTimer.render();
if (aRock.length ==0 && aActiveParticle.length==0) stopRunningGame();
}
[/cc]
I have added a simple frameRate counter and the class is in the .zip above. It is easy to use, you just need to make sure that you call its countFrames() and render() methods on each frame tick.
Below is the entire code listing, including the Main class, StartBox class, and FrameTimer class.
[cc lang="javascript" width="550"]
/**
* ...
* @author Default
* @version 0.1
*/
package {
import flash.display.Bitmap;
import flash.display.MovieClip;
import flash.events.Event;
import flash.events.TimerEvent;
import flash.utils.Timer;
import flash.display.BitmapData;
import flash.geom.*;
import flash.events.*;
import ActiveGraph.*;
//** added for part 3
import flash.utils.getTimer;
//*** added for part 4
import FrameTimer;
public class Main extends MovieClip {
var aRotation:Array=[];
var aShipAnimation:Array=[];
var shipAnimationArrayLength:int;
var ship:Ship;
var shipHolder:MovieClip;
var animationTimer:Timer;
var animationCounter:int;
var playerObject:Object;
var canvasBD:BitmapData;
var canvasBitmap:Bitmap;
var backgroundBD:BitmapData;
var backgroundSource:Background;
var gameTimer:Timer;
var backgroundRect:Rectangle;
var backgroundPoint:Point;
var playerRect:Rectangle;
var playerPoint:Point;
var aKeyPress:Array=[];
var spriteHeight:int=20;
var spriteWidth:int=20;
//** part 2 variables
var aAsteroidAnimation:Array=[];
var asteroidAnimationTimer:Timer;
var asteroidHolder:RockToCache;
var asteroidFrames:int=12;
var aRock:Array=[];
var level:int=1;
var rockPoint:Point=new Point(0,0);
var rockRect:Rectangle=new Rectangle(0,0,36,36);
var levelRocksCreated:Boolean=false;
//*** part 3 variables
var missleTileSheet:BitmapData;
var aMissileAnimation:Array;
var aMissile:Array; //holds missile objects fires
var missileSpeed:int=4;
var missileWidth:int=4;
var missileHeight:int=4;
var missileArrayLength=10;
var missilePoint:Point=new Point(0,0);
var missileRect:Rectangle=new Rectangle(0,0,4,4);
var missileMaxLife:int=50;
var missileFireDelay:Number=100;
var lastMissileShot:Number=getTimer();
//*** part 4 variables
var missileLen:int;
var missileCtr:int;
var tempMissile:Object;
var tempRock:Object;
var rockLen:int;
var rockCtr:int;
var aFarmParticle:Array=[];
var aActiveParticle:Array=[];
var maxParticles:int=500;
var maxPartsPerExplode:int=40;
var explodeCtr:int;
var tempPart:Object;
var farmLen:int;
var randIntVector:int;
var randIntFrame:int;
var activeLen:int;
var partCtr:int;
var partMaxSpeed:int=2;
var partMaxLife:int=50;
var partMinLife:int=10;
var removePart:Boolean=false;
var partPoint:Point=new Point(0,0);
var partRect:Rectangle=new Rectangle(0,0,4,4);
//part 4 optimizations
var randInt:int;
var randInt2:int;
var ctr:int;
var frameTimer:FrameTimer;
var startBox:StartBox;
var ag:ActiveGraph;
public function Main() {
trace("main");
createObjects();
createRotationArray();
createShipAnimation();
startBox=new StartBox(this);
ag=new ActiveGraph(0,false,true,1);
}
private function startBoxOn():void{
startBox.startit();
}
private function createObjects():void {
//create generic object for the player
playerObject=new Object();
playerObject.arrayIndex=0;
playerObject.x=200;
playerObject.y=200;
playerObject.dx=0;
playerObject.dy=0;
playerObject.movex=0;
playerObject.movey=0;
playerObject.acceleration=.3;
playerObject.maxVelocity=8;
playerObject.friction=.01;
playerObject.centerx=playerObject.x+spriteWidth;
playerObject.centery=playerObject.y+spriteHeight;
playerRect=new Rectangle(0,0,spriteWidth*2,spriteHeight*2);
playerPoint=new Point(playerObject.x,playerObject.y);
//init canvas and display bitmap for canvas
canvasBD=new BitmapData(400,400,false,0x000000);
canvasBitmap=new Bitmap(canvasBD);
//init background
backgroundSource=new Background();
backgroundBD=new BitmapData(400,400,false,0x000000);
backgroundBD.draw(backgroundSource,new Matrix());
backgroundRect=new Rectangle(0,0,400,400);
backgroundPoint=new Point(0,0);
//part 3 init tilesheet for missiles
aMissile=[];
aMissileAnimation=[];
missleTileSheet=new missile_sheet(40,4);
var tilesPerRow:int=10;
var tileSize:int=4;
for (var tileNum=0;tileNum<10;tileNum++) {
var sourceX:int=(tileNum % tilesPerRow)*tileSize;
var sourceY:int=(int(tileNum/tilesPerRow))*tileSize;
var tileBitmapData:BitmapData=new BitmapData(tileSize,tileSize,true,0x00000000);
tileBitmapData.copyPixels(missleTileSheet,
new Rectangle(sourceX,sourceY,tileSize,tileSize),new Point(0,0));
aMissileAnimation.push(tileBitmapData);
}
//added in part 4
createFarmParticles();
frameTimer=new FrameTimer(this,140,0,canvasBD);
}
private function createFarmParticles() {
var particleCtr:int;
for (particleCtr=0;particleCtr< playerObject.maxVelocity) {
playerObject.movex=mxn;
playerObject.movey=myn;
} // end speed check
}
if (aKeyPress[37]){
playerObject.arrayIndex--;
if (playerObject.arrayIndex <0) playerObject.arrayIndex=shipAnimationArrayLength-1;
//added in part 4
playerObject.bitmapData=aShipAnimation[playerObject.arrayIndex];
}
if (aKeyPress[39]){
playerObject.arrayIndex++;
if (playerObject.arrayIndex ==shipAnimationArrayLength) playerObject.arrayIndex=0;
//added in part 4
playerObject.bitmapData=aShipAnimation[playerObject.arrayIndex];
}
//*** added for part 3
if (aKeyPress[90]){
fireMissile();
}
}
private function updatePlayer():void {
//add friction
if (playerObject.movex > 0) {
playerObject.movex-=playerObject.friction;
}else if (playerObject.movex < 0) {
playerObject.movex+=playerObject.friction;
}
if (playerObject.movey > 0) {
playerObject.movey-=playerObject.friction;
}else if (playerObject.movey < 0) {
playerObject.movey+=playerObject.friction;
}
playerObject.x+=(playerObject.movex);
playerObject.y+=(playerObject.movey);
playerObject.centerx=playerObject.x+spriteWidth;
playerObject.centery=playerObject.y+spriteHeight;
if (playerObject.centerx > stage.width) {
playerObject.x=-spriteWidth;
playerObject.centerx=playerObject.x+spriteWidth;
}else if (playerObject.centerx < 0) {
playerObject.x=stage.width - spriteWidth;
playerObject.centerx=playerObject.x+spriteWidth;
}
if (playerObject.centery > stage.height) {
playerObject.y=-spriteHeight;
playerObject.centery=playerObject.y+spriteHeight;
}else if (playerObject.centery < 0) {
playerObject.y=stage.height-spriteHeight;
playerObject.centery=playerObject.y+spriteHeight;
}
//trace("centerx=" + playerObject.centerx);
}
private function updateRocks():void {
if (!levelRocksCreated) {
for (ctr=0;ctr<level+5;ctr++) {
//create a rock;
tempRock=new Object();
randInt=int(Math.random()*36);
randInt2=int(Math.random()*asteroidFrames);
tempRock.dx=aRotation[randInt].dx;
tempRock.dy=aRotation[randInt].dy;
tempRock.x=20;
tempRock.y=20;
tempRock.animationIndex=randInt2;
//added for part 4
tempRock.bitmapData=aAsteroidAnimation[tempRock.animationIndex];
tempRock.frameDelay=3;
tempRock.frameCount=0;
tempRock.speed = (Math.random()*level)+1;
//trace("tempRock.speed=" + tempRock.speed);
aRock.push(tempRock);
}
levelRocksCreated=true;
}
for each (tempRock in aRock) {
tempRock.x+=tempRock.dx*tempRock.speed;;
tempRock.y+=tempRock.dy*tempRock.speed;
if (tempRock.x > stage.width) {
tempRock.x=0;
}else if (tempRock.x < 0) {
tempRock.x=stage.width;
}
if (tempRock.y > stage.height) {
tempRock.y=0;
}else if (tempRock.y < 0) {
tempRock.y=stage.height
}
}
}
private function drawBackground():void {
canvasBD.copyPixels(backgroundBD,backgroundRect, backgroundPoint);
}
private function drawPlayer():void {
playerPoint.x=playerObject.x;
playerPoint.y=playerObject.y;
canvasBD.copyPixels(aShipAnimation[playerObject.arrayIndex],
playerRect, playerPoint);
}
private function drawRocks() {
rockLen=aRock.length-1;
for each (tempRock in aRock) {
rockPoint.x=tempRock.x;
rockPoint.y=tempRock.y;
canvasBD.copyPixels(aAsteroidAnimation[tempRock.animationIndex],
rockRect, rockPoint);
tempRock.frameCount++;
if (tempRock.frameCount > tempRock.frameDelay){
tempRock.animationIndex++;
if (tempRock.animationIndex > asteroidFrames-1) {
tempRock.animationIndex = 0;
}
tempRock.frameCount=0;
//added in part 4
tempRock.bitmapData=aAsteroidAnimation[tempRock.animationIndex];
}
}
}
private function fireMissile():void {
if (getTimer() > lastMissileShot + missileFireDelay) {
tempMissile=new Object();
tempMissile.x=playerObject.centerx;
tempMissile.y=playerObject.centery;
tempMissile.dx=aRotation[playerObject.arrayIndex].dx;
tempMissile.dy=aRotation[playerObject.arrayIndex].dy;
tempMissile.speed=missileSpeed;
tempMissile.life=50;
tempMissile.lifeCount=0;
tempMissile.animationIndex=0;
tempMissile.bitmapData=aMissileAnimation[tempMissile.animationIndex];
aMissile.push(tempMissile);
lastMissileShot=getTimer();
}
}
private function updateMissiles():void {
missileLen=aMissile.length-1;
for (ctr=missileLen;ctr>=0;ctr--) {
tempMissile=aMissile[ctr];
tempMissile.x+=tempMissile.dx*tempMissile.speed;;
tempMissile.y+=tempMissile.dy*tempMissile.speed;
if (tempMissile.x > stage.width) {
tempMissile.x=0;
}else if (tempMissile.x < 0) {
tempMissile.x=stage.width;
}
if (tempMissile.y > stage.height) {
tempMissile.y=0;
}else if (tempMissile.y < 0) {
tempMissile.y=stage.height
}
tempMissile.lifeCount++;
if (tempMissile.lifeCount > tempMissile.life) {
aMissile.splice(ctr,1);
tempMissile=null;
}
}
}
private function drawMissiles():void {
missileLen=aMissile.length-1;
for each (tempMissile in aMissile) {
missilePoint.x=tempMissile.x;
missilePoint.y=tempMissile.y;
canvasBD.copyPixels(aMissileAnimation[tempMissile.animationIndex],
missileRect, missilePoint);
tempMissile.animationIndex++;
if (tempMissile.animationIndex > missileArrayLength-1) {
tempMissile.animationIndex = 0;
}
//added in part 4
tempMissile.bitmapData=aMissileAnimation[tempMissile.animationIndex];
}
}
//added in part 4
private function checkCollisions():void {
missileLen=aMissile.length-1;
rockLen=aRock.length-1;
rocks: for (rockCtr=rockLen;rockCtr>=0;rockCtr--) {
tempRock=aRock[rockCtr];
rockPoint.x=tempRock.x;
rockPoint.y=tempRock.y;
missiles: for (missileCtr=missileLen;missileCtr>=0;missileCtr--) {
tempMissile=aMissile[missileCtr];
missilePoint.x=tempMissile.x;
missilePoint.y=tempMissile.y;
//trace("1");
try{
if (tempMissile.bitmapData.hitTest(missilePoint,255,
tempRock.bitmapData,rockPoint,255)) {
// trace("hit!");
createExplode(tempRock.x+18,tempRock.y+18);
tempMissile=null;
tempRock=null;
aMissile.splice(missileCtr,1);
aRock.splice(rockCtr,1);
break rocks;
break missiles;
}
}catch(e:Error) {
trace("error in missle test");
}
}
//trace("2");
playerPoint.x=playerObject.x;
playerPoint.y=playerObject.y;
try{
if (tempRock.bitmapData.hitTest(rockPoint,255,
playerObject.bitmapData,playerPoint,255)){
//trace("ship hit");
createExplode(tempRock.x+18,tempRock.y+18);
tempRock=null;
aRock.splice(rockCtr,1);
}
}catch(e:Error) {
trace("error in ship test");
}
}
//trace("3");
}
private function createExplode(xval:Number,yval:Number):void {
//trace("aFarmParticle.length=" + aFarmParticle.length);
//trace("aActiveParticle.length=" + aActiveParticle.length);
for (explodeCtr=0;explodeCtr<maxPartsPerExplode;explodeCtr++) {
farmLen=aFarmParticle.length-1;
if (farmLen >0){
tempPart=aFarmParticle[farmLen];
aFarmParticle.splice(farmLen,1);
tempPart.lifeCount=0;
tempPart.life=int(Math.random()*partMaxLife)+partMinLife;;
tempPart.x=xval;
tempPart.y=yval;
tempPart.speed=(Math.random()*partMaxSpeed)+1;
randIntFrame=int(Math.random()*10);
tempPart.bitmapData=aMissileAnimation[randIntFrame];
randIntVector=int(Math.random()*36);
tempPart.dx=aRotation[randIntVector].dx;
tempPart.dy=aRotation[randIntVector].dy;
aActiveParticle.push(tempPart);
}
}
}
private function drawParts():void {
activeLen=aActiveParticle.length-1;
for (partCtr=activeLen;partCtr>=0;partCtr--) {
removePart=false;
tempPart=aActiveParticle[partCtr];
tempPart.x+=tempPart.dx*tempPart.speed;;
tempPart.y+=tempPart.dy*tempPart.speed;
if ((tempPart.x > stage.width) || (tempPart.x < 0)) {
removePart=true;
}
if ((tempPart.y > stage.height) || (tempPart.y < 0)){
removePart=true;
}
tempPart.lifeCount++;
if (tempPart.lifeCount > tempPart.life) {
}
if (removePart) {
aFarmParticle.push(tempPart);
aActiveParticle.splice(partCtr,1);
}else{
partPoint.x=tempPart.x;
partPoint.y=tempPart.y;
canvasBD.copyPixels(tempPart.bitmapData,partRect, partPoint);
}
}
}
}
}
[/cc]
[cc lang="javascript" width="550"]
package {
import flash.display.MovieClip;
import flash.events.*;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.utils.Timer;
import flash.text.TextField;
import flash.text.TextFormat;
import flash.display.BitmapData;
public class FrameTimer {
private var format:TextFormat=new TextFormat();
private var messageText:String;
private var messageBitmapData:BitmapData;
private var messageTextField:TextField = new TextField();
public var frameTimer:Timer;
public var framesCounted:int=0;
public var parent:MovieClip;
public var x:int;
public var y:int;
public var canvasBD:BitmapData;
public var messagePoint:Point;
public var messageRect:Rectangle;
public function FrameTimer(parentVal:MovieClip,xval:int,yval:int,canvasval:BitmapData):void {
x=xval;
y=yval;
canvasBD=canvasval;
format.size=12;
format.font="Arial";
format.color="0xffffff";
format.bold=true;
messageText="0";
messageTextField.text=messageText;
messageTextField.setTextFormat(format);
//messageTextField.width=(messageText.length+2)*int(format.size);
messageTextField.width=30;
//messageTextField.height=int(format.size)*2;
messageTextField.height=20;
messageBitmapData=new BitmapData(messageTextField.width,
messageTextField.height,true,0xffff0000);
parent=parentVal;
frameTimer= new Timer(1000,0);
frameTimer.addEventListener(TimerEvent.TIMER,frameCounter,false,0,true);
frameTimer.start();
messagePoint=new Point(x,y);
messageRect=new Rectangle(0,0,messageTextField.width,messageTextField.height);
}
function frameCounter(e:TimerEvent):void {
messageText=framesCounted.toString();
messageTextField.text=messageText;
//trace("frameRate:" + framesCounted.toString());
framesCounted=0;
}
function countFrames():void {
framesCounted++;
}
function render():void {
format.size=12;
format.font="Arial";
format.color="0xffffff";
format.bold=true;
messageTextField.setTextFormat(format);
messageBitmapData.fillRect(messageRect,0xffff0000);
//trace("messageTextField.text=" + messageTextField.text);
messageBitmapData.draw(messageTextField);
canvasBD.copyPixels(messageBitmapData,messageRect, messagePoint);
}
} // end class
} // end package
|