Talking about Astro-PANIC! game, Actionscript 3, Flash and Game development.
If you havent’t not already bought Flash Game Development by Example and want to see how in detail I am explaining every game, after publishing the guide to Tetris creation, here it is the chapter covering Astro-PANIC!, with some minor change to make it fit on the blog.
Astro-PANIC! was released as an all machine language Commodore 64 game to be typed in the February 1984 issue of COMPUTE!’s Gazette magazine. At that time there wasn’t any blog with source codes to download or copy/paste into your projects, so the only way to learn from other programmers was buying computer magazines and typing the example codes on your computer.
Since I suppose you never played this game, I would recommend you play it a bit on this site.
DEFINING GAME DESIGN
Here are the rules to design our Astro-PANIC! prototype:
* The player controls a spaceship with the mouse, being able to move it horizontally on the bottom of the screen.
* At each level, a given number of enemy spaceships appear and roam around the stage at a constant speed in a constant direction.
* Enemies cannot leave the stage, and they will bounce inside it as they touch stage edges.
* Enemies don’t shoot, and the only way they will kill the player is by touching the spaceship.
* The player can only have one bullet on stage at any time, and hitting an enemy with the bullet will destroy it.
* Destroying all enemies means passing the level, and at each level the number of enemies and their speed increases.
These are the basic rules. We’ll add some minor improvements during the design of the game itself, but before we start drawing the graphics, keep in mind we’ll design something with the look and feel of old coin operator monitors, with bright glowing graphics.
CREATING THE GAME AND DRAWING THE GRAPHICS
Create a new file (File | New). Then, from New Document window select Actionscript 3.0. Set its properties as width to 640 px, height to 480 px, background color to #000000 (black) and frame rate to 60. Also define the Document Class as Main
and save the file as astro-panic.fla
.
Though 30 frames per second is the ideal choice for smooth animations, we will use 60 frames per second to create a very fast paced game.
There are three actors in this game: the player-controlled spaceship, the bullet and the enemy. In astro-panic.fla
, create three new Movie Clip symbols and call them spaceship_mc
for the spaceship, bullet_mc
for the bullet, and enemy_mc
for the enemy. Set them all as exportable for ActionScript. Leave all other settings at their default values, just like you did during the making of Tetris.
From left to right: The spaceship (spaceship_mc
), the bullet (bullet_mc
), and the enemy (enemy_mc
).
I made all assets with the shape of a circle. The spaceship is half a circle with a radius of 30 pixels, the bullet is a circle with a 4 pixels radius, and the enemy is a circle with a radius of 25 pixels. All of them have the registration point in their centers, and enemy_mc
has a dynamic text field in it called level
. At the moment I am writing a couple of zeros to test how the dynamic text field fits in the enemy shape.
Now we are ready to code.
ADDING AND CONTROLLING THE SPACESHIP
As usual we know we are going to use classes to manage both enter frame and mouse click events, so we’ll import all the required classes immediately.
The spaceship is controlled with the mouse, but can only move along x-axis.
Without closing astro_panic.fla
, create a new file and from New Document window select ActionScript 3.0 Class
. Save this file as Main.as
in the same path you saved astro_panic.fla
. Then write:
package {
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
public class Main extends Sprite {
private var spaceship:spaceship_mc;
public function Main() {
placeSpaceship();
addEventListener(Event.ENTER_FRAME,onEnterFrm);
}
private function placeSpaceship():void {
spaceship=new spaceship_mc();
addChild(spaceship);
spaceship.y=479;
}
private function onEnterFrm(e:Event):void {
spaceship.x=mouseX;
if (spaceship.x<30) {
spaceship.x=30;
}
if (spaceship.x>610) {
spaceship.x=610;
}
}
}
}
At this time you should know everything about the concept behind this script. placeSpaceship is the function which constructs, adds to Display List and places the spaceship_mc
DisplayObject called spaceship
.
In enter_frame
function we just move the spaceship in the same position of the x-axis of the mouse. We don’t want the spaceship to hide in a corner, so it won’t be able to follow the axis of the mouse if it gets too close to stage edges.
Test the movie, and move the mouse. Your spaceship will follow it, while being bound to the ground.
Now we should give the spaceship an old arcade look.
ADDING A GLOW FILTER
AS3 allows us to dynamically apply a wide range of filters to DisplayObjects on the fly. We’ll add a glow filter to simulate old ‘arcades’ pixel luminosity.
flash.filters.GlowFilter
class lets us apply a glow effect to DisplayObjects.
First, we need to import it.
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.filters.GlowFilter;
At this time, we can simply create a new variable to construct a GlowFilter
object. Change placeSpaceship
function this way:
private function placeSpaceship():void {
//...
var glow:GlowFilter=new GlowFilter(0x00FFFF,1,6,6,2,2);
spaceship.filters=new Array(glow);
}
We specify the color as 0x00FFFF (cyan to draw the spaceship), the alpha (1 = full opacity), and the amount of horizontal and vertical blur (both 6).
I want you to notice that I used 6 for horizontal and vertical blur because I like the effect I achieve with such value. If you are planning to use a lot of filters, remember values that are a power of 2 (such as 4 and 8, but not 6) render more quickly than other values.
The remaining two arguments are the strength, that determines the spread of the filter (if you use Photoshop, it’s something like spread and size of the glow filter you can apply on layers) and the quality.
Quality can range from 1 to 15 but values higher than 3 may affect performances and the same final effect can be set playing with blur.
Finally the filter is added.
spaceship.filters=new Array(glow);
filters
DisplayObject’s property wants an array with all the filters you want to associate to the DisplayObject. In our case, we are adding only one filter but we have to include it in the array anyway.
Test the movie and you will see your spaceship glow.
In the previous picture, you can see the difference between the spaceship without and with the glow effect applied.
Now your spaceship is ready to fire.
MAKING SPACESHIP FIRE
Nobody would face an alien invasion with a harmless spaceship, so we are going to make it fire.
We need to create a variable to manage bullet_mc
DisplayObject and I have said the spaceship can fire only one bullet at a time, so we need another variable to tell us if the spaceship is already firing. If it’s firing, it cannot fire. If it’s not firing, it can fire.
Add two new class level variables:
private var spaceship:spaceship_mc;
private var isFiring:Boolean=false;
private var bullet:bullet_mc;
isFiring
is the Boolean variable that we’ll use to determine if the spaceship is firing. false
means it’s not firing.
bullet
will represent the bullet itself.
The player will be able to fire with mouse click, so a listener is needed in Main
function:
public function Main() {
placeSpaceship();
addEventListener(Event.ENTER_FRAME,onEnterFrm);
stage.addEventListener(MouseEvent.CLICK,onMouseCk);
}
Now every time the player clicks the mouse, onMouseCk
function is called.
This is the function:
private function onMouseCk(e:MouseEvent):void {
if (! isFiring) {
placeBullet();
isFiring=true;
}
}
It’s very easy: if isFiring
is false
(the spaceship isn’t already firing), placeBullet
function is called to physically place a bullet then isFiring
is set to true
because now the spaceship is firing.
The same placeBullet
function isn’t complex:
private function placeBullet():void {
bullet=new bullet_mc();
addChild(bullet);
bullet.x=spaceship.x;
bullet.y=430;
var glow:GlowFilter=new GlowFilter(0xFF0000,1,6,6,2,2);
bullet.filters=new Array(glow);
}
It’s very similar to placeSpaceship
function, the bullet is created, added to Display List, placed on screen, and a red glow effect is added.
The only thing I would explain is the concept behind x
and y
properties:
bullet.x=spaceship.x;
Setting bullet’s x
property equal to spaceship’s x
property will place the bullet exactly where the spaceship is at the moment of firing.
bullet.y=430;
430
is a good y
value to make the bullet seem as it were just fired from the turret. Test the movie, and you will be able to fire a bullet with a mouse click.
The bullet at the moment remains static in the point where we fired it.
MAKING THE BULLET FLY
To make the bullet fly, we have to define its speed and move it upwards. Then we’ll remove it once it leaves the stage and reset isFiring
to false
to let the player fire again.
Add a constant to class level variables:
private const BULLET_SPEED:uint=5;
private var spaceship:spaceship_mc;
private var isFiring:Boolean=false;
private var bullet:bullet_mc;
BULLET_SPEED
is the amount of pixels the bullet will fly at each frame. We won’t manage upgrades or power-ups, so we can say its value will never change. That’s why it’s defined as a constant.
To manage bullet movement, we need to add some lines at the end of onEnterFrm
function.
You may wonder why we are managing both the spaceship and the bullet inside the same class rather than creating a separate class for each one. You’ll discover it when you manage enemies’ movement, later in this article.
Meanwhile, add this code to onEnterFrm
function.
private function onEnterFrm(e:Event):void {
//...
if (isFiring) {
bullet.y-=BULLET_SPEED;
if (bullet.y<0) {
removeChild(bullet);
bullet=null;
isFiring=false;
}
}
}
The new code is executed only if isFiring
is true. We are sure we have a bullet on stage when isFiring
is true.
bullet.y-=BULLET_SPEED;
Moves the bullet upward by BULLET_SPEED
pixels.
if (bullet.y<0) { ... }
This if statement checks if y
property is less than 0
. This means the bullet flew off the screen. In this case we physically remove the bullet from the game with
removeChild(bullet);
bullet=null;
and we give the player the capability of firing again with
isFiring=false;
Test the movie and fire, now your bullets will fly until they reach the top of the stage. Then you will be able to fire again.
Since nobody wants to fire for the sake of firing, we'll add some enemies to shoot down.
ADDING ENEMIES
We want a battle with more and more enemies as the player progresses through levels, so we have to define a variable to tell us which level is currently being played and a variable to manage the enemy DisplayObject. Add these two class level variables:
private const BULLET_SPEED:uint=5;
private var spaceship:spaceship_mc;
private var isFiring:Boolean=false;
private var bullet:bullet_mc;
private var enemy=enemy_mc;
private var level:uint=1;
level
is the current level, that starts from 1
.
As the game starts, we have to place enemies on the screen according to level number. Add these lines to Main
function:
public function Main() {
placeSpaceship();
for (var i:uint=1; i
The for
loop will call placeEnemy
function (which obviously places an enemy) for level+2
times, so we'll have 3 enemies at level one, 4 enemies on level two, and so on.
Notice how an argument with the current enemy count is passed: knowing the cardinality of an enemy will come in hand later.
As you can imagine, placeEnemy
function at the moment is not that much more than a copy/paste of placeBullet
function, we are just placing enemies in a random position, not too close to the edges of the stage, and not too close to the player.
private function placeEnemy(enemy_level:uint):void {
enemy=new enemy_mc();
enemy.x=Math.random()*500+70;
enemy.y=Math.random()*200+50;
var glow:GlowFilter=new GlowFilter(0xFF00FF,1,6,6,2,2);
enemy.filters=new Array(glow);
addChild(enemy);
}
Test the game and you will see three enemies appear in random positions.
Don't worry if in some cases they overlap: the game won't deal with collisions among enemies so it does not matter.
MOVING ENEMIES
As a static enemy won't scare anyone, let's make enemies move.
With the knowledge you have at the moment, you would suggest creating spaceship_mc
class and using an enter frame listener to update each enemy position. Most scripts relies on Event.ENTER_FRAME
event is simultaneously dispatched to all DisplayObjects listening for it, so you don't have to synchronize animations.
That's true, but with many DisplayObjects to be updated, this technique although being the most correct from a programming point of view can dramatically increase the work of the Flash player.
In this game we won't deal with such an huge number of moving DisplayObjects to represent a risk for your CPU, anyway it's time to learn something new.
Add this new class level variable:
private const BULLET_SPEED:uint=5;
private var spaceship:spaceship_mc;
private var isFiring:Boolean=false;
private var bullet:bullet_mc;
private var enemy=enemy_mc;
private var level:uint=1;
private var enemyVector:Vector.=new Vector.();
You just defined your first Vector.
A Vector is an array with elements of the same predefined type. Such type is defined as "base type", and can be any kind of type, including custom classes like I've done.
The base type must be specified when the Vector is created or when an instance is created using the class constructor.
So to declare a Vector of enemy_mc
class we'll use:
private var enemyVector:Vector.
Notice how the base type is declared using a dot (.) then writing the class name between angle brackets (< and >).
Then you would construct it declaring base type again, this way:
enemyVector=new Vector.();
Now the question is: when should you use a Vector rather than an Array? You should use a Vector every time you are dealing with collections of data of the same type, as Vector management has been proved to be faster than Array management.
Again, the increased performance in this game would be unnoticeable since the data we manage isn't that big, anyway it's important you know how to use vectors.
Back to our enemies, we have to make them move in a random direction at a constant speed, but we said tougher enemies will move faster, so it's time to learn some trigonometry basics. Look at this picture:
We have a circle, and a radius that we know has the same length no matter its angle. The radius represents the constant enemy speed, to be split into horizontal and vertical speed, called vx
and vy
.
Thanks to trigonometry, we can determine vx
by multiplying the radius by the cosine of the angle formed by the radius and the horizontal axis, and vy
multiplying the radius by the sine of such angle.
This concept can be translated into AS3 adding these lines at the end of placeEnemy
function:
private function placeEnemy(enemy_level:uint):void {
//...
var dir:Number=Math.random()*Math.PI*2;
enemy.xspeed=enemy_level*Math.cos(dir);
enemy.yspeed=enemy_level*Math.sin(dir);
enemyVector.push(enemy);
}
Let's see how we can choose a random direction:
var dir:Number = Math.random()*Math.PI*2;
dir
is the variable which stores the random direction. It's a random number between 0 and 360 degrees, just expressed in radians. The radian is the standard unit of angular measure, and describes the plane angle subtended by a circular arc as the length of the arc divided by the radius of the arc.
Math.PI
returns the value of PI, 3.141592653589793
enemy.xspeed=enemy_level*Math.cos(dir);
enemy.yspeed=enemy_level*Math.sin(dir);
Once we know enemy direction, it's easy to determine its horizontal and vertical speed thanks to the trigonometry formulas you just learned. Just notice how speed is multiplied by enemy_level
argument. This way the latest enemies to be added are the faster and consequently the harder to kill.
This simple feature will allow us to have levels with increasing difficulty, with a new, fastest enemy spaceship to be added at every level.
enemyVector.push(enemy);
Finally, the enemy itself is added to enemyVector
Vector with push method as if it was an array, since push works in the same way for both Arrays and Vectors.
Everything is now ready to make onEnterFrm
function iterate through enemyVector
Vector and update each enemy position according to its x
and y
speed.
Add this line to onEnterFrm
function:
private function onEnterFrm(e:Event) {
//...
enemyVector.forEach(manageEnemy);
}
forEach
method (notice the uppercase E) executes a function for each item in the Vector.
This means manageEnemy
function will be executed for each enemyVector
item, but you can't define this function as you like, because it must have some mandatory arguments.
The function has to be created with three arguments: the current Vector item, the index of such item, and the Vector itself. Also, the function won't return anything, so we will declare as void
.
This is manageEnemy
function:
private function manageEnemy(c:enemy_mc,index:int,v:Vector.):void {
var currentEnemy:enemy_mc=c;
currentEnemy.x+=currentEnemy.xspeed;
currentEnemy.y+=currentEnemy.yspeed;
if (currentEnemy.x<25) {
currentEnemy.x=25;
currentEnemy.xspeed*=-1;
}
if (currentEnemy.x>615) {
currentEnemy.x=615;
currentEnemy.xspeed*=-1;
}
if (currentEnemy.y<25) {
currentEnemy.y=25;
currentEnemy.yspeed*=-1;
}
if (currentEnemy.y>455) {
currentEnemy.y=455;
currentEnemy.yspeed*=-1;
}
}
let's see first how it has been declared:
private function manageEnemy(c:enemy_mc,index:int,v:Vector.):void
As you can see, the three arguments are the current enemy, its index in the Vector and the Vector itself. All arguments are automatically passed to the function; you don't have to worry about anything when calling it in the forEach
method.
Then in comes a line I used only for the sake of layout:
var currentEnemy:enemy_mc = c;
I was forced to call the first argument c
to make a function declaration fit in a single row, but obviously it would have been better to call it currentEnemy
, so just created a variable with a more appropriate name.
currentEnemy.x+=currentEnemy.xspeed;
currentEnemy.y+=currentEnemy.yspeed;
That's how I update currentEnemy position according to its xspeed
and yspeed
properties.
Enemies cannot fly off the stage, so the remaining lines are just to make them bounce inside stage edges. I will explain only the first situation: when the enemy is about to leave the stage to the left.
if (currentEnemy.x<25) { ... }
The if statement checks if enemy x position is less than 25 (enemy's radius). This would mean the enemy is flying off the stage to the left, and we must prevent it. First we stop it at the very leftmost position it can go with:
currentEnemy.x=25;
Then, we invert its horizontal speed this way:
currentEnemy.x=25;
The remaining if statements check and prevent the enemies from flying off the stage respectively to right, up, and down sides.
Test the movie and you will see three enemies moving and bouncing around the stage, at a constant speed while each one has a different speed.
Now enemies are quite dangerous because they move around the screen, anyway nothing happens when they touch your spaceship.
Obviously hitting an enemy with the spaceship means losing the game or at least one life, so let's make enemies deadly.
BEING KILLED BY AN ENEMY
Both enemies and the spaceship have a perfect circular shape. This will help us to determine when an enemy and the spaceship collide. Being basically two circles, we can say they collide when the distance between their centers is less than the sum of both the radius.
Let's start creating a quick function to determine the distance between two Sprites using the Pythagorean Theorem:
private function distance(from:Sprite,to:Sprite):Number {
var distX:Number=from.x-to.x;
var distY:Number=from.y-to.y;
return distX*distX+distY*distY;
}
There isn't that much to explain, since we are just applying a world famous formula, but I want you to notice I am not performing any square root because it's quite CPU-expensive. It won't be a problem as long as I remember to compare the collision distance applying the power of two, which is way faster than applying a square root.
Everything is ready to check for collisions, so add these lines at the end of manageEnemy
function:
private function manageEnemy(c:enemy_mc,index:int,v:Vector.):void {
//...
if (distance(spaceship,currentEnemy)<3025) {
die();
}
}
Look at this statement:
if (distance(spaceship,currentEnemy)<3025) { ... }
It determines if the distance between the spaceship and the current enemy is less than 3025, which is 25 (enemy radius) + 30 (spaceship radius) = 55 (collision distance) by the power of two. Easy and fast. Obviously you are free to store all these values in constants; I am using these raw values for a matter of speed.
Once an enemy collides with the spaceship, die
function is called. Here it is:
private function die():void {
var glow:GlowFilter=new GlowFilter(0x00FFFF,1,10,10,6,6);
spaceship.filters=new Array(glow);
removeEventListener(Event.ENTER_FRAME,onEnterFrm);
stage.removeEventListener(MouseEvent.CLICK,onMouseCk);
}
I am sure you figured out how it works: first a new, bigger glow is applied to the spaceship, and then all the event listeners are removed. The game stops.
Test the game, and let an enemy hit the spaceship. The game will stop with the enemy hitting a greatly glowing spaceship and nothing more will happen.
That's enough at the moment, because before making something interesting happen when the spaceship dies, we must make it able to kill enemies with its bullets.
KILLING AN ENEMY
Knowing the bullet has a perfect circular shape, there's nothing easier at this time than letting the spaceship kill an enemy. We have to check if the distance between the bullet (if any) and the enemy is less than the sum of their radius, just as we made it with the spaceship.
At the end of manageEnemy
function, add these lines:
private function manageEnemy(c:enemy_mc,index:int,v:Vector.):void {
//...
if (isFiring) {
if (distance(bullet,currentEnemy)<841) {
killEnemy(currentEnemy);
}
}
}
First we check if there's a bullet flying around the game just looking at isFiring
value. If it's true, then we see if the distance between the current spaceship and the bullet is less than 841, which is 25 (enemy radius) + 4 (bullet radius) = 29 (collision distance) by the power of two. In this case, killEnemy
function is called; just like die
function was called when the enemy and the spaceship collided. The only difference is we need to know which enemy the player killed, so we pass it as argument. Again, feel free to replace numbers with constants.
This is killEnemy
function:
private function killEnemy(theEnemy:enemy_mc):void {
var glow:GlowFilter=new GlowFilter(0xFF00FF,1,10,10,6,6);
theEnemy.filters=new Array(glow);
removeEventListener(Event.ENTER_FRAME,onEnterFrm);
stage.removeEventListener(MouseEvent.CLICK,onMouseCk);
}
The function works absolutely the same way as die
function does: adds a glow to the dying enemy and completely stops the game.
Test the movie and shoot to an enemy, and you will see it glow and the game will stop.
At this time all main events are defined. We can work on level progression.
KILLING AN ENEMY - FOR GOOD
A level is completed when all enemies have been killed. When you kill an enemy, the game must continue rather than stop like it does now. We must flag enemies killed by the spaceship so they won't harm anymore, and let the game continue.
First, when we create a new enemy, let's set a new property called killed
. It will be true
if the enemy has been killed, so it starts with false
.
private function placeEnemy(enemy_level:uint):void {
enemy=new enemy_mc ;
enemy.killed=false;
//...
}
Then we have to heavily recode killEnemy
function. We won't remove listeners as we don't want the game to stop, but we'll set killed property to true and remove the bullet as if it had flown out of the stage.
private function killEnemy(theEnemy:enemy_mc):void {
var glow:GlowFilter=new GlowFilter(0xFF00FF,1,10,10,6,6);
theEnemy.filters=new Array(glow);
// don't remove listeners
theEnemy.killed=true;
removeChild(bullet);
bullet=null;
isFiring=false;
}
Last but not least, we'll update enemy position and check for collision with the player or the bullet only if the enemy is still alive, that means its killed property is false.
private function manageEnemy(c:enemy_mc,index:int,v:Vector.):void {
var currentEnemy:enemy_mc=c;
if (! currentEnemy.killed) {
//...
}
}
This way the whole function is executed only if killed
property is false
.
Test the movie, and you will be able to kill all enemies.
At this time the player would expect to see killed enemies removed from the screen, maybe with some kind of animation.
KILLING AN ENEMY - WITH STYLE
To make something happen to the enemy when it's about to die, we could make it grow and fade out. It's a good and simple way to animate its death.
Moreover, we have to remove dead enemies from enemyVector Vector because there's no point in managing them as they should be removed from stage.
We know there is only one bullet at a time, so there can be only one enemy hit by a bullet in a single frame. This is precious information because it allows us to manage all deaths with a single class level variable, which we'll call enemyToRemove, indicating the index in enemyVector Vector of the enemy to remove. It starts at -1 which means there's no enemy to remove.
private const BULLET_SPEED:uint=5;
private var spaceship:spaceship_mc;
private var isFiring:Boolean=false;
private var bullet:bullet_mc;
private var enemy=enemy_mc;
private var level:uint=1;
private var enemyVector:Vector.=new Vector.();
private var enemyToRemove:int=-1;
Once the variable is declared, we have to add a new block of code to manage dying enemies. Do you remember the whole manageEnemy
function is executed only if killed property is false?
Now it's time to execute some code when it's true and the enemy has been hit by the bullet.
private function manageEnemy(c:enemy_mc,index:int,v:Vector.):void {
var currentEnemy:enemy_mc=c;
if (! currentEnemy.killed) {
//...
} else {
currentEnemy.width++;
currentEnemy.height++;
currentEnemy.alpha-=0.01;
if (currentEnemy.alpha<=0) {
removeChild(currentEnemy);
currentEnemy=null;
enemyToRemove=index;
}
}
}
Let's see what we are doing:
currentEnemy.width++;
currentEnemy.height++;
Increases enemy width and height to make it bigger.
currentEnemy.alpha-=0.01;
Makes it a bit less opaque decreasing its alpha
property by 0.01.
if (currentEnemy.alpha<0) { ... }
Checks if the alpha
property is less than 0. This means the enemy is completely transparent and it's time to remove it from the game.
removeChild(currentEnemy);
currentEnemy=null;
Removes the enemy from the Display List and clears its variable.
enemyToRemove=index;
Finally, setting the new currentEnemy
variable to index, that represents the current index in enemyVector
Vector.
Now in onEnterFrm
function we can remove the corresponding item from the Vector. Doing it in a function called by forEach
method would produce some warnings as you are making the Vector shorter while it's currently being scanned.
At the end of onEnterFrm
function, add these lines:
private function onEnterFrm(e:Event):void {
//...
if (enemyToRemove>=0) {
enemyVector.splice(enemyToRemove,1);
enemyToRemove=-1;
}
}
Their meaning is quite obvious: if enemyToRemove
has a value greater than its default value -1, then remove the item from enemyVector
Vector with splice method, then set enemyToRemove
to -1 again as there aren't any more enemies to remove.
splice
method adds elements to and removes elements from the Vector. The same method is also available for arrays. In our case, the first parameter (enemyToRemove
) is the index of the element where the deletion begins, and the second parameter (1) the number of elements to be deleted. Basically I am saying the Vector to remove one element starting from index enemyToRemove
. An optional third parameter can be used to provide a list of one or more comma-separated values to insert into the Vector starting from the index specified in the first parameter. We don't need this optional third parameter in this case.
Test the movie and shoot to an enemy to see it explode.
The game now starts to look good, but once you destroyed all enemies nothing happens.
ADVANCING LEVELS
Once all enemies have been destroyed, the player must be able to play the next level, with more enemies moving faster.
For our convenience, we should manage level creation with a function, changing Main
function removing the for
loop and adding a new function called playLevel
.
public function Main() {
placeSpaceship();
playLevel();
addEventListener(Event.ENTER_FRAME,onEnterFrm);
stage.addEventListener(MouseEvent.CLICK,onMouseCk);
}
This function is just a cut/paste of the for
loop previously included in Main
function, but this way we can call playLevel
from elsewhere.
private function playLevel():void {
for (var i:uint=1; i
And in this specific case, we are calling it from onEnterFrm
function once we removed an enemy:
private function onEnterFrm(e:Event) {
//...
if (enemyToRemove>=0) {
enemyVector.splice(enemyToRemove,1);
enemyToRemove=-1;
if (enemyVector.length==0) {
level++;
playLevel();
}
}
}
If the length of enemyVector
Vector is 0, there are no enemies left, so it's time to increase level variable and call playLevel function to start a new level.
Test the movie and try to beat as many levels as you can.
In the previous picture, level 2 with four enemies and level 4 with six enemies. The game gets harder as the player progresses through levels.
Now, something for the score maniacs.
MANAGING CURRENT SCORE AND HIGH SCORE
When playing games with a specific goal, such as saving the princess or escaping from the castle, players know exactly why they are playing: they must save the princess or escape from the castle.
In games like Astro-PANIC!, where there's no goal and you just have to survive as long as possible, the only way to have players come back to our game and play it again is giving the possibility to save their best score.
People will play again and again to achieve a better score.
At this time, we need two more class level variables: one to save the current score, which we call score, and another variable called hiscore
, which will save our best score ever.
Add these two new variables:
private const BULLET_SPEED:uint=5;
private var spaceship:spaceship_mc;
private var isFiring:Boolean=false;
private var bullet:bullet_mc;
private var enemy=enemy_mc;
private var level:uint=1;
private var enemyVector:Vector.=new Vector.();
private var enemyToRemove:int=-1;
private var score:uint=0;
private var hiscore:uint=0;
Finally, it's time to make a good use of the level
dynamic text. We'll display the number of the enemy. The higher the number, the faster the enemy, and the more points it will give once killed. This will help the player to choose which enemy to kill, making his own strategy.
private function placeEnemy(enemy_level:uint):void {
enemy=new enemy_mc ;
enemy.level.text=enemy_level;
//...
}
Once an enemy dies, the score is updated. Although being a very simple game, we can add complex scoring system by giving the enemy a score based on its level and its height.
The higher you kill an enemy, the more points it will give you.
private function killEnemy(theEnemy:enemy_mc):void {
//...
score+=int(theEnemy.level.text)*4-Math.floor(theEnemy.y/100);
trace(score);
}
Once you die, I want the score to be written on the output window, and eventually the high score to be updated.
private function die():void {
var glow:GlowFilter=new GlowFilter(0x00FFFF,1,10,10,6,6);
spaceship.filters=new Array(glow);
removeEventListener(Event.ENTER_FRAME,onEnterFrm);
stage.removeEventListener(MouseEvent.CLICK,onMouseCk);
trace("Your score: "+score);
trace("Current hiscore: "+hiscore);
if (score>hiscore) {
trace("CONGRATULATIONS!! NEW HISCORE");
hiscore=score;
}
}
There's not that much to explain in this code, as it's just a bunch of screen outputs.
Test the movie and play until you die:
You should see in the output window something like this:
6
9
...
56
62
66
Your score: 66
Current hiscore: 0
CONGRATULATIONS!! NEW HISCORE
So you are able to manage scores and high scores.
The big problem is the high score is reset to 0 every time you start a new game, making it useless. We have to find a way to save data on players' local computers.
SAVING DATA ON YOUR LOCAL COMPUTER
AS3 provides a class, SharedObject, to let us save a limited amount of data on our local computer. The class does not create cookies, but something very similar called LocalSharedObjects, and the concept is the same.
It's very important to understand that LocalSharedObjects maintain local persistence. This means that you can play the game, make an high score, turn off your computer, and next time you'll play the game on the same computer, it will retrieve the high score.
Exactly what we need. Let's see how to use it. First, we need to import the SharedObject
class:
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.filters.GlowFilter;
import flash.net.SharedObject;
Then we will remove hiscore class level variable as we won't use it anymore as it just keeps the high score when the script is running, and we will create a new variable called sharedHiScore, to handle with SharedObject class.
private const BULLET_SPEED:uint=5;
private var spaceship:spaceship_mc;
private var isFiring:Boolean=false;
private var bullet:bullet_mc;
private var enemy=enemy_mc;
private var level:uint=1;
private var enemyVector:Vector.=new Vector.();
private var enemyToRemove:int=-1;
private var score:uint=0;
private var hiscore:uint=0; // remove this one
private var sharedHiScore:SharedObject;
At this time, when the game is run, we can face two cases:
1: The game has never been run on the computer, so we have to somehow initialize the SharedObject.
2: The game has already been run on the computer, no matter if someone made an high score or not, no matter even if someone has played. In this case the SharedObject is initiazlied.
To make this, we need to add this code to Main
function:
public function Main() {
sharedHiScore=SharedObject.getLocal("hiscores");
if (sharedHiScore.data.score==undefined) {
sharedHiScore.data.score=0;
trace("No High Score found");
} else {
trace("Current High Score: "+sharedHiScore.data.score);
}
sharedHiScore.close();
placeSpaceship();
playLevel();
addEventListener(Event.ENTER_FRAME,onEnterFrm);
stage.addEventListener(MouseEvent.CLICK,onMouseCk);
}
Let's see its meaning:
sharedHiScore = SharedObject.getLocal("hiscores");
getLocal
method returns a reference to a locally persistent SharedObject (in this case hiscores
) that is available only on the current client. If the SharedObject does not already exist, getLocal
method creates one.
if (sharedHiScore.data.score==undefined) { ... }
When looking at score value inside hiscores
SharedObject, if it's undefined
it means there isn't any variable called score
in hiscores
SharedObject or there isn't any hiscores
SharedObject at all.
sharedHiScore.data.score = 0;
trace("No High Score found");
In this case we need to initialize score variable in hiscores SharedObject. We'll set it to zero. At the same time, we print a message in the Output window saying we did not find any high score.
else { trace("Current High Score: "+sharedHiScore.data.score); }
If we found a high score, we show it in the Output window.
sharedHiScore.close();
When we are done with the SharedObject, we have to close it. close method does this job.
Now, when the player dies, we have to check his score with the SharedObject high scores and eventually update it. The last part of die function must be changed this way:
private function die():void {
var glow:GlowFilter=new GlowFilter(0x00FFFF,1,10,10,6,6);
spaceship.filters=new Array(glow);
removeEventListener(Event.ENTER_FRAME,onEnterFrm);
stage.removeEventListener(MouseEvent.CLICK,onMouseCk);
trace("Your score: "+score);
sharedHiScore=SharedObject.getLocal("hiscores");
trace("Current hiscore: "+sharedHiScore.data.score);
if (score>sharedHiScore.data.score) {
trace("CONGRATULATIONS!! NEW HISCORE");
sharedHiScore.data.score=score;
}
sharedHiScore.close();
}
As you can see, the SharedObject is opened once again, and its score variable is compared with score class level variable. As you can see, both variables can have the same name because they refer to different classes. Then if needed we update the SharedObject and finally we close it.
Test the movie and the first time you will see in the Output window:
No High Score found
Now play a game, and if when you die you scored, let's say, 18, you will see in the Output window:
Your score: 18
Current hiscore: 0
CONTRATULATIONS!! NEW HISCORE
At this time you must close and restart the game as it does not provide a "replay", and once you restart it (you can also restart you computer if you want) you will see:
Current High Score: 18
The game remembered the latest high score you made.
SUMMARY
The most important things you learned during this article are the capability of adding filters on the fly to your DisplayObjects and the feature of saving data on your computer using SharedObjects. You can manage any kind of save game with SharedObjects, such as the latest level beaten, the amount of gold the player owns, or any other kind of information.
WHERE TO GO NOW
To provide a better experience to players, you could place some dynamic text fields showing the current level and the score, as well as the high score. Then you can add some lines to onMouseCk function, you can make the player restart the game if he clicks the mouse when the game is over. You only have to reset the score, the level, and the variable that states the game is over.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.