Asteroids Project


Prior to reading the walkthrough of my code, play the game I created first >>   Play Asteroids Game


Use the left arrow and right arrow keys for direction, up arrow key for thrusting, spacebar for firing.



In 1979 Atari launched Asteroids which became their all-time best selling game. Over time, Atari sold more than 100,000 units of Asteroids video game machines. Compared to the billions of games downloaded on mobile devices today, it was a spectacular success.


This version of Asteroids is written in HTML5, JavaScript and the Phaser.io framework.


I attempted to make the game as close to the original Atari arcade version as possible, using the rules of the original game and the source code that I found online. However, since this is for demostration purposes, I took the functionality just so far. If people write to me and there is enough demand, I will consider finishing the code in its entirety.



Project Files

Click here to download the project files. You will find the following in the zip file:




Organization of the game




Asteroids.html

  
<html>
    <head>
        <title>Asteroids</title>
        <meta charset="UTF-8" />
        <script src="js/phaser.min.js"></script>
        <script src="js/start.js"></script>
        <script src="js/game.js"></script>
        <script src="js/game_over.js"></script>
        <script src="js/main.js"></script>       
    </head>
   <body>
        <div id="gameDiv"> </div>
    </body>
</html>


Main.js

Creates a new game instance and represents the entire game and states.

 
var game;
game = new Phaser.Game(640, 480, Phaser.AUTO, '');
game.state.add('Start', Start);
game.state.add('Game', gameState);
game.state.add('Game_Over', Game_Over);
game.state.start('Start');


Start.js

Now initialize the Start state object. In start.js define a new object and add the functions below. When the state is started, the preload function will be called first, loading all the needed assets for the game. Once preloading finishes, create gets called, initializing the start screen.

 
var Start = {

    preload : function() {
        // Load all the needed resources for the start screen.
        game.load.image('start', 'assets/gamestart.png');
    },

    create: function () {

        // Add start screen.
        // It will act as a button to start the game.
        this.add.button(0, 0, 'start', this.startGame, this);

    },

    startGame: function () {

        // Change the state to the actual game.
        this.state.start('Game');
    }
};


game.js

Here is where game action occurs.

Basically computer games consist of three basic functions. They are not clearly defined in the code highlighted below, but try to be aware of these stages as you work through the code.




Loading Graphics and Declaring Variables

Load all the game graphics, and declare a new object called graphicAssets. Preload the graphic assets into the game state.


Sample Snippet:

 
var graphicAssets = {
    ship:{URL:'assets/ship.png', name:'ship'},
    ship_thrust:{URL:'assets/ship_thrust.png', name:'ship_thrust'},
    bullet:{URL:'assets/bullet.png', name:'bullet'},
    burst:{URL:'assets/burst.png', name:'burst'}...

The game.load.image method is used load all external content such as images, sounds. For instance, PNG files for the asteroids, ship and bullet.


Sample Snippet:

 
gameState.prototype = {
     preload: function () {
        game.load.image(graphicAssets.asteroid1_L.name, graphicAssets.asteroid1_L.URL);
        game.load.image(graphicAssets.asteroid1_M.name, graphicAssets.asteroid1_M.URL);
        game.load.image(graphicAssets.asteroid1_S.name, graphicAssets.asteroid1_S.URL);
        
        game.load.image(graphicAssets.asteroid2_L.name, graphicAssets.asteroid2_L.URL);
        game.load.image(graphicAssets.asteroid2_M.name, graphicAssets.asteroid2_M.URL);
        game.load.image(graphicAssets.asteroid2_S.name, graphicAssets.asteroid2_S.URL);...


Player Ship

Start the process of adding the player ship. A new object called shipProperties is declared containing all the ship properties.

 
var shipProperties = {
    startX: gameProperties.screenWidth * 0.5,
    startY: gameProperties.screenHeight * 0.5,
    acceleration: 300,
    drag: 100,
    maxVelocity: 300,
    angularVelocity: 200,
    startingLives: 3,
    timeToReset: 3,
    blinkDelay: 0.2,
};

Create a sprite representing the player ship. Call initGraphics function to add the ship sprite. Then call game.add.sprite function to make the sprite appear on screen at the specified x and y positions, these were set as attributes in the shipProperties. By default the sprite will face right, so set the angle to -90 degrees so it points up. Also set its anchor to 50% of its width and height.


Sample Snippet:

 
initGraphics: function () {
this.shipSprite = game.add.sprite(shipProperties.startX, shipProperties.startY, graphicAssets.ship.name);
        this.shipSprite.angle = -90;
        this.shipSprite.anchor.set(0.5, 0.5);...

Add the physics to the ship properties, e.g. acceleration, drag, maxVelocity, angularVelocity.

Here is a brief description of each property:


Now the actual ship physics are implemented. Call initPhysics function, which initializes the arcade physics system and add physics bodies to all the game objects.


Sample Snippet:

 
 initPhysics: function () {
        game.physics.startSystem(Phaser.Physics.ARCADE);
       
        game.physics.enable(this.shipSprite, Phaser.Physics.ARCADE);
        this.shipSprite.body.drag.set(shipProperties.drag);
        this.shipSprite.body.maxVelocity.set(shipProperties.maxVelocity);...


Controlling the player ship

Now that the ship has a physics body enabled, add some controls to move the ship.Declare properties for the sprite.

Identify key_left, key_right, key_thrust. In the initKeyboard function, add the following keyboard keys: left arrow key, right arrow key, up arrow key.

 
 initKeyboard: function () {
        this.key_left = game.input.keyboard.addKey(Phaser.Keyboard.LEFT);
        this.key_right = game.input.keyboard.addKey(Phaser.Keyboard.RIGHT);
        this.key_thrust = game.input.keyboard.addKey(Phaser.Keyboard.UP);
        this.key_fire = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);
    },

In the checkPlayerInput function (called from the update function) check for key presses and add responses to it. Check whether the left or right key is being pressed. Note that only one key is being checked at any time. When the LEFT key is being pressed, we set the angularVelocity for the ship’s physics body to a negative value. This rotates the ship counter clockwise. If the RIGHT key is being pressed, the ship rotates clockwise. Finally if neither left nor right key is being pressed, the ship stops rotating.

As for the UP or thrust key, call the arcade physics to determine how much acceleration there should be for the x and y axis of the ship. When the key is released, the acceleration is set back to 0 so that the drag can take over and eventually stop our ship from moving forward.

 
 checkPlayerInput: function () {
        if (this.key_left.isDown) {
            this.shipSprite.body.angularVelocity = -shipProperties.angularVelocity;
        } else if (this.key_right.isDown) {
            this.shipSprite.body.angularVelocity = shipProperties.angularVelocity;
        } else {
            this.shipSprite.body.angularVelocity = 0;
        }...


Display Ships Available


The number of player ships available is displayed as player ships instead of a number./p>

In the ship properties, the startingLives attribute for the number of player ships to start the game. This is set to three to match the original Atari arcade game.

var shipProperties = {...
startingLives: 3,...

Preload the graphics. Note the date and time were added. The images are cached and will not re-display even with a game reload. So adding the date and time tricks the browser into not caching the images.

 shipcounter1:{URL:'assets/shipcounter.png?' + new Date().getTime(), name:'shipcounter1'},
 shipcounter2:{URL:'assets/shipcounter.png?' + new Date().getTime(), name:'shipcounter2'},
 shipcounter3:{URL:'assets/shipcounter.png?' + new Date().getTime(), name:'shipcounter3'},

Call initGraphics function to add the three sprites at the specified x and y positions. They are intially visible.

this.shipSpriteCounter1 = game.add.sprite(20, 40, graphicAssets.shipcounter1.name);
this.shipSpriteCounter2 = game.add.sprite(40, 40, graphicAssets.shipcounter2.name);
this.shipSpriteCounter3 = game.add.sprite(60, 40, graphicAssets.shipcounter3.name);

In the gameState object keep track of the asteroid count, and the number of ships left.

Whenever a player ship collides with an asteroid decrement the shipLives property.

var gameState = function (game){...
this.asteroidsCount = asteroidProperties.startingAsteroids;
    
this.shipLives = shipProperties.startingLives;...

The images are initially set to visible, but as each ship is destroyed by an asteroid and the number of ships is reduced, the ship images are hidden one at a time.

 
if (this.shipLives == 2) {
        this.shipSpriteCounter3.visible = false;
        }
        if (this.shipLives == 1) {
        this.shipSpriteCounter2.visible = false;
        }
        if (this.shipLives == 0) {
        this.shipSpriteCounter1.visible = false;
        // GAME OVER

No Player Ships Left


When zero player ships are left the game is over, instead of the game terminating abruptly, the screen is frozen to allow the last ship break apart and drift out of sight for four seconds (SECOND * 4). Then the Game Over screen is displayed.

 
if (this.shipLives == 0) {
        this.shipSpriteCounter1.visible = false;
        // GAME OVER
        game.time.events.add(Phaser.Timer.SECOND * 4, this.gameOver, this); 


Player Ship Thrust


When the thrust key is pressed, the ship displays a small flame , and when the key is released the flame dissappears . The corresponding flame or non-flame sprite is displayed. This is handled in the checkPlayerInput function

 
if (this.key_thrust.isDown) {
         //swap the shipSprite with the ship_thrust image 
            shipflag = false;
            this.shipSprite.loadTexture('ship_thrust', 0, false);            
            game.physics.arcade.accelerationFromRotation(this.shipSprite.rotation, shipProperties.acceleration, this.shipSprite.body.acceleration);
           } else {
            shipflag = true;
            this.shipSprite.loadTexture('ship', 0, false);
            this.shipSprite.body.acceleration.set(0);
           }

The shipFlag variable above is set for use in the asteroidCollision function. If the ship is accelarating and was destroyed during thrust down, reset the sprite to non-thrust.

if (shipflag == false){
//ship was using destroyed during thrust down, reset the sprite to non-thrust here
this.shipSprite.loadTexture('ship', 0, false);
}


Bullets

Next add bullet properties such as speed, interval, lifespan, maxCount.

Here is a brief description:

 
 var bulletProperties = {
    speed: 400,
    interval: 250,
    lifeSpan: 2000,
    maxCount: 30,
};


Firing Bullets

A firing key is required and a bullet group object to manage the bullets, along with an interval property to track when the next round can be fired. In this case it is set to 0, so rounds are fired like a machine gun. In the initPhysics function bullet sprites re created. Apply the Phaser arcade physics to each sprite in the bullet group.

Next add the firing key and identify it as the spacebar key in the initKeyboard function.


Before firing, check whether the current internal game clock has passed the bullet Interval. Then get the first object in the bullet group. If a bullet is successfully retrieve from the bullet group, calculate where it will appear just in front of the player ship. Next move the bullet to the x and y coordinates and call the arcade physics to calculate how fast the bullet sprite should move.

 
 fire: function () {
        if (game.time.now > this.bulletInterval) {
            this.sndFire.play();
            
            var bullet = this.bulletGroup.getFirstExists(false);
            
            if (bullet) {
                var length = this.shipSprite.width * 0.5;
                var x = this.shipSprite.x + (Math.cos(this.shipSprite.rotation) * length);
                var y = this.shipSprite.y + (Math.sin(this.shipSprite.rotation) * length);
                
                bullet.reset(x, y);
                bullet.lifespan = bulletProperties.lifeSpan;
                bullet.rotation = this.shipSprite.rotation;
                
                game.physics.arcade.velocityFromRotation(this.shipSprite.rotation, bulletProperties.speed, bullet.body.velocity);
                this.bulletInterval = game.time.now + bulletProperties.interval;
            }
        }
    },

Call the checkBoundaries function for each bullet sprite that exists so that they wrap around a single screen. The game is a single screen world which means the game objects wrap around the game world. This means if any game objects pass outside the game world, it reappears on the opposite side of the screen. Note asteroids, the player ship, any sprite object uses this function.

 
checkBoundaries: function (sprite) {
        if (sprite.x < 0) {
            sprite.x = game.width;
        } else if (sprite.x > game.width) {
            sprite.x = 0;
        } 

        if (sprite.y < 0) {
            sprite.y = game.height;
        } else if (sprite.y > game.height) {
            sprite.y = 0;
        }
    },


Asteroids

Declare the asteroid properties, such as number of starting asteroids, max number of asteroids, etc.


These three properties are used for each asteroid size:


Each asteroid size have almost identical properties:


Sample Snippet:

 
 var asteroidProperties = {
    startingAsteroids: 2,  
    maxAsteroids: 20,
    incrementAsteroids: 2,
 
    asteroid1_L: { minVelocity: 50, maxVelocity: 150, minAngularVelocity: 0, maxAngularVelocity: 200, score: 20, nextSize: graphicAssets.asteroid1_M.name, pieces: 2 },
    asteroid1_M: { minVelocity: 50, maxVelocity: 200, minAngularVelocity: 0, maxAngularVelocity: 200, score: 50, nextSize: graphicAssets.asteroid1_S.name, pieces: 2 },
    asteroid1_S: { minVelocity: 50, maxVelocity: 300, minAngularVelocity: 0, maxAngularVelocity: 200, score: 100 },
    ...

Next declare an asteroid group in the initGraphics function.


Sample Snippet:

 
  this.asteroidGroup;
    this.asteroidsCount = asteroidProperties.startingAsteroids;...

Then enable physics for the group so each new asteroid added to the group will use the arcade physics and have a physics body by default.


Sample Snippet:

 
   this.asteroidGroup.enableBody = true;
        this.asteroidGroup.physicsBodyType = Phaser.Physics.ARCADE;...


Asteroid Types


There are three categories of asteroids, and each category has three sizes.

For lack of anything better to call them I gave them part numbers. For instance the first large asteroid type is labeled: asteroid1_L, medium is asteroid1_M, and small is asteroid1_S.

Only large asteroid objects need to be listed in the resetAsteroids function, one asteroid for each category.


Sample Snippet:

 
this.createAsteroid(x, y, graphicAssets.asteroid1_L.name);
this.createAsteroid(x, y, graphicAssets.asteroid2_L.name);
this.createAsteroid(x, y, graphicAssets.asteroid3_L.name);...

The x and y parameters are randomly generated, and the asteroids are created with an iterative loop based on the count of asteroid categories.

 
for (var i=0; i < this.asteroidsCount; i++ ) {
            var side = Math.round(Math.random());
            var x;
            var y;
            
            if (side) {
                x = Math.round(Math.random()) * gameProperties.screenWidth;
                y = Math.random() * gameProperties.screenHeight;
            } else {
                x = Math.random() * gameProperties.screenWidth;
                y = Math.round(Math.random()) * gameProperties.screenWidth;
            }


Creating asteroids

Create some asteroids. After the fire function, add the createAsteroid function. Create a new sprite and add it to the asteroids group, set anchor point in middle of asteroid sprite. The asteroid physics body rotates the sprites at a random angularVelocity between the range of minAngularVelocity and maxAngularVelocity declared in the asteroids properties.


A loop creates a number of asteroids and randomly positions them on screen. An asteroid can appear at the horizontal sides (left or right) or vertical sides (top or bottom). This position is randomly generated. The boundaries for each asteroid must be checked to keep it from leaving the game world and disappearing.

Also when an asteroid sprite is destroyed, check to see if the asteroid is the smallest size, if not then call the createAsteroid function to split the asteroid.

 
createAsteroid: function (x, y, size, pieces) {
        if (pieces === undefined) { pieces = 1; }
        
        for (var i=0; i<pieces; i++) {
            var asteroid = this.asteroidGroup.create(x, y, size);
            asteroid.anchor.set(0.5, 0.5);
            asteroid.body.angularVelocity = game.rnd.integerInRange(asteroidProperties[size].minAngularVelocity, asteroidProperties[size].maxAngularVelocity);

            var randomAngle = game.math.degToRad(game.rnd.angle());
            var randomVelocity = game.rnd.integerInRange(asteroidProperties[size].minVelocity, asteroidProperties[size].maxVelocity);

            game.physics.arcade.velocityFromRotation(randomAngle, randomVelocity, asteroid.body.velocity);
        }
    },


Splitting asteroids

Add collision detection so asteroids and the player ship can be destroyed. Once a collision with an asteroid is detected, destroy the player ship and break the asteroid. Lines 455 thru 457 call functions that display pieces of broken player ship.

 
asteroidCollision: function (target, asteroid) {
       if (shipflag == false){
        //ship was using destroyed during thrust down, reset the sprite to non-thrust here
        this.shipSprite.loadTexture('ship', 0, false);
      }
        this.sndDestroyed.play();
        
        target.kill();
        asteroid.kill();
        
        if (target.key == graphicAssets.ship.name) {
        // Call the function that shows the broken ship wreckage
          this.shippiece1Asteroid(asteroid.x, asteroid.y, graphicAssets.shippiece1.name, shippiece1Properties.shippiece1Pieces);
          this.shippiece2Asteroid(asteroid.x, asteroid.y, graphicAssets.shippiece2.name, shippiece2Properties.shippiece2Pieces); 
          this.shippiece3Asteroid(asteroid.x, asteroid.y, graphicAssets.shippiece3.name, shippiece3Properties.shippiece3Pieces);  
            this.destroyShip();
        }
        
        this.splitAsteroid(asteroid);
        this.updateScore(asteroidProperties[asteroid.key].score);
        
        if (!this.asteroidGroup.countLiving()) {
            game.time.events.add(Phaser.Timer.SECOND * gameProperties.delayToStartLevel, this.nextLevel, this);
        }
    },


Respawning the player ship


After the player ship hits an asteroid, if there are lives available, the player ship will respawn. During respawn the ship is invulnerable for about three seconds. The attribute timeToReset controls this, so set it to three. That way if the ship appears right on an asteroid there is a few seconds to move to safety to avoid instant destruction.

On respawn, for every 0.2 seconds, our ship will change its visibility so that it alternates between being visible and hidden causing it to blink, this is set in the ship properties blinkDelay to 0.2.

 
 resetShip: function () {
      //resets ship after each time it is destroyed
        this.shipIsInvulnerable = true;
        this.shipSprite.reset(shipProperties.startX, shipProperties.startY);  
        this.shipSprite.angle = -90;
        
        game.time.events.add(Phaser.Timer.SECOND * shipProperties.timeToReset, this.shipReady, this);
        game.time.events.repeat(Phaser.Timer.SECOND * shipProperties.blinkDelay, shipProperties.timeToReset / shipProperties.blinkDelay, this.shipBlink, this);
    },


Next Level


Once all asteroids have been destroyed, the next level begins and all the items in the asteroid group are removed. Even though none of the asteroids are visible or being updated, it still takes up memory so it would be best to remove all items.

 
nextLevel: function () {
        this.asteroidGroup.removeAll(true);
        
        if (this.asteroidsCount < asteroidProperties.maxAsteroids) {
            this.asteroidsCount += asteroidProperties.incrementAsteroids;
        }
        
        this.resetAsteroids();
    },


Final Score


The running score is based on the score variable. Large asteroids equal 20 points, medium asteroids equal 50 points, and small asteroids equal 100 points. The points are declared in the asteroidProperties function.

 
this.score = 0;

The score is calculated and updateScore function called as asteroids split in the asteroidSplit function.

 
this.updateScore(asteroidProperties[asteroid.key].score);

The update score is totaled in the updateScore function.

 
updateScore: function (score) {
this.score += score;
this.tf_score.text = this.score;
},

The final score displayed on the Game Over screen is captured in the update function. The finalscore is declared as a global variable.

 
finalscore = this.score;


game_over.js

The Game Over graphic is displayed, along with the final score (which was earlier stored as a global variable). The startGame function allows the user to start over by clicking on the screen.

 
var Game_Over = {

    preload : function() {
        // load all the needed resources for the Game Over screen.
        game.load.image('gameover', 'assets/gameover.png');
    },

    create : function() {

        // Create button to start game similar to the main menu.
        this.add.button(0, 0, 'gameover', this.startGame, this);

        // Last Score Info.
        game.add.text(260, 350, "LAST SCORE: " + finalscore.toString(), { font: "22px OCR A Extended", fill: "#fff", align: "center"});
    },

    startGame: function () {

        // Change the state to the actual game.
        this.state.start('Game');
   }
};