Chapter 15. Galactic War: Finishing the Game

The things you have learned in this book all culminate in this last chapter involving Galactic War. The game will be enhanced, polished, and ready for a production environment at the end of this chapter. All that will remain to do is to package up the entire game, resources and all, into a JAR file for distribution on the web (which is covered in the next and final chapter of the book).

Here are the key topics of interest in this chapter:

  • Adding power-ups to the game

  • Implementing a global game state with a start and end screen

  • Polishing the game and preparing it for production

Let’s Talk about Power-Ups

Let’s face it; Galactic War is a difficult game. It’s nearly impossible to clear the asteroids with the weapon we’ve been using up to this point—a single peashooter, for the most part. I want to totally enhance the game in this chapter and make it ready for primetime—for distribution on the web. To meet that lofty goal, there are quite a few things to cover in this chapter. I’ll itemize what I’d like to accomplish:

  • Add power-ups to upgrade the ship’s weapons system

  • Add power-ups to restore health and shields

  • Add power-ups to gain extra score points

Figure 15.1 shows the six power-ups that will be added to the game in this chapter.

Galactic War has six different power-ups that enhance the ship and increase your score.

Figure 15.1. Galactic War has six different power-ups that enhance the ship and increase your score.

Tip

The finished version of the game can be played online at www.jharbour.com/BeginningJava. You need to have the Java Runtime Environment for SE 6 Update 22 installed (the same as required for all of the book’s examples).

Ship and Bonus-Point Power-Ups

There are three different power-ups that simply increase your score for 250, 500, and 1,000 points. These power-ups are released randomly when you destroy a tiny asteroid—the last stage of the asteroid’s deterioration after the large, medium, and small stages.

The shield power-up will increase your shield strength by 1/4 (not the full refill that you were expecting?). Likewise, the cola can increases your ship’s health by 1/4, up to the maximum value displayed in the health bar at the top of the screen. Figure 15.2 shows the three states the ship can take on during gameplay.

There are now three modes for the ship: normal, thrusters, and shields.

Figure 15.2. There are now three modes for the ship: normal, thrusters, and shields.

Wait, the ship doesn’t have any shields! Oh, right; that’s a feature we’ll add to the game in this chapter as well.

Weapon Upgrades

The weapon upgrade power-up is by far the most interesting new feature of the game, and it is very welcome given how difficult it is to stay alive in this game! You can earn up to five levels of weapon upgrades in this game. I had my son, Jeremiah, help me design the weapon patterns for each upgrade, and the result is shown in Figure 15.3.

The five levels of weapon upgrades for your ship.

Figure 15.3. The five levels of weapon upgrades for your ship.

The upgrades were implemented a little differently than our design here, but the result is unmistakable. The biggest difference is upgrade level five: Rather than firing side to side, the two additional shots go upward at a slight angle. I made this adjustment while playing the game when it seemed to be more effective than firing them at 90-degree angles. Let’s take a tour of the five weapon upgrades as they were implemented in the game.

Standard Weapon

The standard weapon is shown being fired in Figure 15.4. Note the single bullet icon in the upper-right corner of the screen, showing the current weapon upgrade level.

The standard weapon is a single bullet.

Figure 15.4. The standard weapon is a single bullet.

Weapon Level Two

The first weapon upgrade allows the ship to fire two shots at the same time, as shown in Figure 15.5. There are now two bullet icons at the upper-right. After play testing the game for a while, I decided to make this the starting weapon level. You can still lose this by getting hit and then drop down to the standard weapon if you aren’t careful.

Two bullets definitely do a lot more damage!

Figure 15.5. Two bullets definitely do a lot more damage!

Weapon Level Three

Weapon upgrade level three allows the ship to fire three shots at the same time, as shown in Figure 15.6. There are now three bullet icons at the upper-right.

The third weapon upgrade will keep you alive much longer.

Figure 15.6. The third weapon upgrade will keep you alive much longer.

Weapon Level Four

The fourth weapon upgrade gives you four shots at a time, spreading out at slightly wider angles than the previous level, capable of meting out massive damage to the horde of asteroids, as shown in Figure 15.7.

Four shots at a time is good for your self-confidence.

Figure 15.7. Four shots at a time is good for your self-confidence.

Weapon Level Five

Heavy gunner! Weapon level five is truly staggering, delivering massive amounts of firepower to the ship. Take care, though—if you get hit, your ship is taken down a notch to level four again. The angles of spread at level five are slightly wider than level four, and two additional shots fire out roughly sideways from the ship (see Figure 15.8).

Weapon upgrade level five gives your ship six projectiles!

Figure 15.8. Weapon upgrade level five gives your ship six projectiles!

Enhancing Galactic War

I’m going to start at the top of the GalacticWar.java file and note the changes made as we move down through the source code. We’ll take a look at a few screenshots along the way to explain what’s happening in the code. As you can clearly see in the pages to follow, the new game engine (via the Game class) makes enhancements incredibly easy to add to the game.

Tip

If you run into any problems updating the source code with the new improvements, I recommend you open up the complete Galactic War project, located in the chapter resources.

New Sprite Types

The first change to the program involves adding some new sprite type definitions in GalacticWar.java. Near the top of the program listing is a set of sprite types. Add the new items shown in bold text.

//sprite types
final int SPRITE_SHIP = 1;
final int SPRITE_ASTEROID_BIG = 10;
final int SPRITE_ASTEROID_MEDIUM = 11;
final int SPRITE_ASTEROID_SMALL = 12;
final int SPRITE_ASTEROID_TINY = 13;
final int SPRITE_BULLET = 100;
final int SPRITE_EXPLOSION = 200;
final int SPRITE_POWERUP_SHIELD = 300;
final int SPRITE_POWERUP_HEALTH = 301;
final int SPRITE_POWERUP_250 = 302;
final int SPRITE_POWERUP_500 = 303;
final int SPRITE_POWERUP_1000 = 304;
final int SPRITE_POWERUP_GUN = 305;

New Game States

To give the game the ability to start, play, and end (with the option to restart), we need to add some conditional gameplay states and make use of the pause property in the sprite engine (found in the Game class). Add the following lines just below the new sprite definitions, above the toggle variables. The new code is shown in bold.

//game states
final int GAME_MENU = 0;
final int GAME_RUNNING = 1;
final int GAME_OVER = 2;

//various toggles
boolean showBounds = false;
boolean collisionTesting = true;

When the game first starts up, you see the title screen, which is shown in Figure 15.9. This screen shows the keys you press to control the ship.

The title screen of Galactic War displays the key controls.

Figure 15.9. The title screen of Galactic War displays the key controls.

New Sprite Images

Now let’s add some new sprite image definitions using the ImageEntity class. The ship has a new shield feature, and we have a whole bunch of new images for power-ups and the updated user interface (such as the health and shield meters).

Scroll down a bit more to the block of code showing definitions for all of the ImageEntity objects used in the game. Add the new code shown in bold. (Note also the minor change to the shipImage array, which now has three elements.)

//define the images used in the game
ImageEntity background;
ImageEntity bulletImage;
ImageEntity[] bigAsteroids = new ImageEntity[5];
ImageEntity[] medAsteroids = new ImageEntity[2];
ImageEntity[] smlAsteroids = new ImageEntity[3];
ImageEntity[] tnyAsteroids = new ImageEntity[4];
ImageEntity[] explosions = new ImageEntity[2];
ImageEntity[] shipImage = new ImageEntity[3];
ImageEntity[] barImage = new ImageEntity[2];
ImageEntity barFrame;
ImageEntity powerupShield;
ImageEntity powerupHealth;
ImageEntity powerup250; 
ImageEntity powerup500;
ImageEntity powerup1000;
ImageEntity powerupGun;

Health/Shield Meters, Score, Firepower, and Game State Variables

Now let’s add some global variables to keep track of such things as the ship’s health, shield power, game state, as well as more obvious things such as current score, high score, and weapon upgrade level. Add the following code after the image definitions, before the Random line. New code is shown in bold.

//health/shield meters and score
int health = 20;
int shield = 20;
int score = 0;
int highscore = 0;
int firepower = 1;
int gameState = GAME_MENU;

//create a random number generator
Random rand = new Random();

New Input Keys

We need to add support for the new shield ability. I’ve defined the Shift key to activate the ship’s shield, but you may change this key if you prefer a different one. Locate the key input tracking variables a few lines below the last change you just made, and note the new variable added in bold.

The collision toggle and bounding box toggle are both still active in the game. Although they were used for testing, they are now known as undocumented hidden cheats in the game!

//some key input tracking variables
boolean keyLeft, keyRight, keyUp, keyFire, keyB, keyC, keyShield;

Sound and Music Objects

Immediately below the key tracking variable definitions, add the following code for the sound and music objects (or make sure the code looks like this, if it differs in your source code listing). Note the changes in bold.

//some key input tracking variables
boolean keyLeft, keyRight, keyUp, keyFire, keyB, keyC, keyShield;

//sound effects and music
MidiSequence music = new MidiSequence();
SoundClip shoot = new SoundClip();
SoundClip explosion = new SoundClip();

Loading Media Files

Unfortunately, all of these new features come with a price—load times. The game loads up very fast on your local PC, but can take 10 to 20 seconds to load from a website, depending on your connection speed. All the images and sounds used in this game are fairly small because they are stored in the compressed PNG format. The biggest file is the background, which is about 300 KB. All remaining image and sound files are well under 100 KB, and most of them are in the 1 to 10 KB range, which is extremely small indeed. I suspect that without the background image and the large explosion, the game would load up almost instantly. When packaged into a JAR (which is covered in the next chapter), the entire game is 600 KB.

Let’s add all of the new code to gameStartup() to load all of the new images, sounds, and music in the game. There are also some gameplay-related changes in this method that you should look out for. All new code and changes are highlighted in bold.

void gameStartup() {
     //load sounds and music
     music.load("music.mid");
     shoot.load("shoot.au");
     explosion.load("explode.au");

     //load the health/shield bars
     barFrame = new ImageEntity(this);
     barFrame.load("barframe.png");
     barImage[0] = new ImageEntity(this);
     barImage[0].load("bar_health.png");
     barImage[1] = new ImageEntity(this);
     barImage[1].load("bar_shield.png"); 
        //load powerups
        powerupShield = new ImageEntity(this);
        powerupShield.load("powerup_shield2.png");
        powerupHealth = new ImageEntity(this);
        powerupHealth.load("powerup_cola.png");
        powerup250 = new ImageEntity(this);
        powerup250.load("powerup_250.png");
        powerup500 = new ImageEntity(this);
        powerup500.load("powerup_500.png");
        powerup1000 = new ImageEntity(this);
        powerup1000.load("powerup_1000.png");
        powerupGun = new ImageEntity(this);
        powerupGun.load("powerup_gun.png");

        //load the background image
        background = new ImageEntity(this);
        background.load("bluespace.png");

        //create the ship sprite--first in the sprite list
        shipImage[0] = new ImageEntity(this);
        shipImage[0].load("spaceship.png");
        shipImage[1] = new ImageEntity(this);
        shipImage[1].load("ship_thrust.png");
        shipImage[2] = new ImageEntity(this);
        shipImage[2].load("ship_shield.png");

        AnimatedSprite ship = new AnimatedSprite(this, graphics());
        ship.setSpriteType(SPRITE_SHIP);
        ship,setImage(shipImage[0].getImage());
        ship.setFrameWidth(ship.imageWidth());
        ship.setFrameHeight(ship.imageHeight());
        ship.setPosition(new Point2D(SCREENWIDTH/2, SCREENHEIGHT/2));
        ship.setAlive(true);
        //start ship off as invulnerable
        ship.setState(STATE_EXPLODING);
        collisionTimer = System.currentTimeMillis(); 
        sprites().add(ship);

        //load the bullet sprite image
        bulletImage = new ImageEntity(this);
        bulletImage.load("plasmashot.png");

        //load the explosion sprite image
        explosions[0] = new ImageEntity(this);
        explosions[0].load("explosion.png"); 
        explosions[1] = new ImageEntity(this);
        explosions[1].load("explosion2.png");

        //load the big asteroid images (5 total)
        for (int n = 0; n<5; n++) {
            bigAsteroids[n] = new ImageEntity(this);
            String fn = "asteroid" + (n+1) + ".png";
            bigAsteroids[n].load(fn);
        }
        //load the medium asteroid images (2 total)
        for (int n = 0; n<2; n++) {
            medAsteroids[n] = new ImageEntity(this);
            String fn = "medium" + (n+1) + ".png";
            medAsteroids[n].load(fn);
        }
        //load the small asteroid images (3 total)
        for (int n = 0; n<3; n++) {
           smlAsteroids[n] = new ImageEntity(this);
           String fn = "small" + (n+1) + ".png";
           smlAsteroids[n].load(fn);
        }
        //load the tiny asteroid images (4 total)
        for (int n = 0; n<4; n++) {
           tnyAsteroids[n] = new ImageEntity(this);
           String fn = "tiny" + (n+1) + ".png";
           tnyAsteroids[n].load(fn);
        }

       //start off in pause mode
       pauseGame();                                                             

   //delete this block of code, which has been moved to another method
   /**** moved to resetGame
            //create the random asteroid sprites
            for (int n = 0; n<ASTEROIDS; n++){
                                   createAsteroid();
           }
           */
        }

Game State Issue—Resetting the Game

The game used to just start up with asteroids flying at your ship, without any chance to prepare yourself! To avoid this problem, I’ve added an overall state system to the game, which now starts off in GAME_MENU mode. During normal gameplay, the state is GAME_PLAYING. When your ship blows up, the state is GAME_OVER. To make it possible to restart the game after dying (in which case, the high score is retained), we need a way to reset the key variables and objects—but the game should not reload any files! Add the resetGame() method just below gameStartup().

private void resetGame() {
     //restart the music soundtrack
     music.setLooping(true);
     music.play();

     //save the ship for the restart
     AnimatedSprite ship = (AnimatedSprite) sprites().get(0);

     //wipe out the sprite list to start over!
     sprites().clear();

     //add the saved ship to the sprite list
     ship.setPosition(new Point2D(SCREENWIDTH/2, SCREENHEIGHT/2));
     ship.setAlive(true);
     ship.setState(STATE_EXPLODING);
     collisionTimer = System.currentTimeMillis();
     ship.setVelocity(new Point2D(0, 0));
     sprites().add(ship);

     //create the random asteroid sprites
     for (int n = 0; n<ASTEROIDS; n++) {
     createAsteroid();
     }
     //reset variables
     health = 20;
     shield = 20;
     score = 0;
     firepower = 2;

     }

Detecting the Game-Over State

The next method in the source code listing is gameTimedUpdate(), an event passed by the parent Game class. We need to add a bit of code here to handle the GAME_OVER state, which occurs when there is only one sprite left in the game—the ship. Figure 15.10 shows the game in this state after the health meter has dropped to zero.

If your health drops to zero, the game is over—you lose!

Figure 15.10. If your health drops to zero, the game is over—you lose!

   void gameTimedUpdate() {
       checkInput();

       if (!gamePaused() && sprites().size() == 1) {
                resetGame();
                gameState = GAME_OVER;

       }

   }

Screen Refresh Updates

I’ve made a whole bunch of changes to the game screen, which is refreshed regularly during the applet’s update() and paint() events. I’ve removed the testing/debugging displays, which showed the ship’s vitals and other things. We want the game screen to look nice now, without any clutter. Because there are so many changes involved, you may want to just delete any commented-out code and rewrite this method as indicated. I will show new or changed code in bold and deleted code in italics.

    void gameRefreshScreen() {
        Graphics2D g2d = graphics();

//*** REMOVE OR COMMENT OUT THIS BLOCK
         //the ship is always the first sprite in the linked list
      AnimatedSprite ship = (AnimatedSprite)sprites().get(0);
******/

        //draw the background
        g2d.drawImage(background.getImage()><subscript>0>0></subscript>SCREENWIDTH-1,SCREEN-HEIGHT-1,this);
//*** REMOVE OR COMMENT OUT THIS BLOCK
/*       //print status information on the screen
         g2d.setColor(Color.WHITE);
         g2d.drawString("FPS: " + frameRate(), 5, 10);
         long x = Math.round(ship.position().X());
         long y = Math.round(ship.position().Y());
         g2d.drawString("Ship: " + x + "," + y , 5, 25);
         g2d.drawString("Move angle: " + Math.round(ship.moveAngle())+90, 5, 40);
         g2d.drawString("Face angle: " + Math.round(ship.faceAngle()), 5, 55);
         if (ship.state()= =STATE_NORMAL)
              g2d.drawString("State: NORMAL", 5, 70);
         else if (ship.state()= =STATE_COLLIDED)
              g2d.drawString("State: COLLIDED", 5, 70);
         else if (ship.state()= =STATE_EXPLODING)
              g2d.drawString("State: EXPLODING", 5, 70);
         g2d.drawString("Sprites:  " + sprites().size(), 5, 120);

         if (showBounds) {
             g2d.setColor(Color.GREEN);
             g2d.drawString("BOUNDING BOXES", SCREENWIDTH-150, 10);
         }
         if (collisionTesting) {
             g2d.setColor(Color.GREEN);
             g2d.drawString("COLLISION TESTING", SCREENWIDTH-150, 25);
         }
******/
         //what is the game state?
         if (gameState = = GAME_MENU) {
             g2d.setFont(new Font("Verdana", Font.BOLD, 36));
             g2d.setColor(Color.BLACK);
             g2d.drawString("GALACTIC WAR", 252, 202);
             g2d.setColor(new Color(200,30,30));
             g2d.drawString("GALACTIC WAR", 250, 200);

             int x = 270, y = 15;
             g2d.setFont(new Font("Times New Roman", Font.ITALIC | Font.BOLD,
20));
             g2d.setColor(Color.YELLOW);
             g2d.drawString("CONTROLS:", x, ++y*20);
             g2d.drawString("ROTATE - Left/Right Arrows", x+20, ++y*20);
             g2d.drawString("THRUST - Up Arrow", x+20, ++y*20);
             g2d.drawString("SHIELD - Shift key (no scoring)", x+20, ++y*20);
             g2d.drawString("FIRE - Ctrl key", x+20, ++y*20);

             g2d.setColor(Color.WHITE);
             g2d.drawString("POWERUPS INCREASE FIREPOWER!", 240, 480);

             g2d.setFont(new Font("Ariel", Font.BOLD, 24));
             g2d.setColor(Color.ORANGE);
             g2d.drawString("Press ENTER to start", 280, 570);
         }
          else if (gameState == GAME_RUNNING) {
               //draw health/shield bars and meters
               g2d.drawImage(barFrame.getImage(), SCREENWIDTH - 132, 18, this);
               for (int n = 0; n < health; n++) {
                   int dx = SCREENWIDTH - 130 + n * 5;
                   g2d.drawImage(barImage[0].getImage(), dx, 20, this);
               }
               g2d.drawImage(barFrame.getImage(), SCREENWIDTH - 132, 33, this);
               for (int n = 0; n < shield; n++) {
                   int dx = SCREENWIDTH - 130 + n * 5;
                   g2d.drawImage(barImage[1].getImage(), dx, 35, this);
               }
               //draw the bullet upgrades
               for (int n = 0; n < firepower; n++) {
                   int dx = SCREENWIDTH - 220 + n * 13;
                   g2d.drawImage(powerupGun.getImage(), dx, 17, this);
               }

               //display the score
               g2d.setFont(new Font("Verdana", Font.BOLD, 24));
               g2d.setColor(Color.WHITE);
               g2d.drawString("" + score, 20, 40);
               g2d.setColor(Color.RED);
               g2d.drawString("" + highscore, 350, 40);
         }

         else if (gameState == GAME_OVER) {
             g2d.setFont(new Font("Verdana", Font.BOLD, 36));
             g2d.setColor(new Color(200, 30, 30));
             g2d.drawString("GAME OVER", 270, 200);

             g2d.setFont(new Font("Arial", Font.CENTER_BASELINE, 24));
             g2d.setColor(Color.ORANGE);
             g2d.drawString("Press ENTER to restart", 260, 500);
         }
    }

Preparing to End

The gameShutdown() event comes next. As you’ll recall, this method was left empty in the previous chapter, but now we need to use it properly. A well-behaved Java program will free up resources before the program ends. In the case of Galactic War, I prefer to rely on Java’s built-in garbage collector to free up resources automatically. However, it is necessary to shut off the music and any sound effects currently playing before the applet ends because sometimes a MIDI sequence will keep playing after the game has ended.

void gameShutdown() {
      music.stop();
      shoot.stop();
      explosion.stop();
   }

Updating New Sprites

Next up is the spriteUpdate() event method. There are a lot of new additions here but no changes, as we have all these new power-ups that need to be handled when they appear on the screen. The most important thing to do here is to warp the power-ups along with everything else in the game. Then, in addition, the power-ups need to wobble, or alternate the rotation back and forth, so they stand out from the other sprites.

Just as an example, take a look at Figure 15.11. This zoom-in of the ship firing shows how much the sprite engine is handling at one time. In this figure, I count 60 sprites in just this small portion of the screen (which, granted, is where most of the action is currently taking place). All of the asteroids are rotating by some random value. The flaming bullets are rotated and adjusted every time they move along their paths. The ship rotates with user input. Every time you destroy a tiny sprite, an eight-frame animation is played. That’s a lot of action! It’s a good thing we developed the sprite engine in the last chapter, or none of this would have been possible using the old method of handling sprites with arrays. (Oh, and in case you were wondering—the bullets have not passed through any of those tiny sprites; they have just been spawned by the destruction of a larger sprite and will soon be annihilated by the incoming fire.)

There are a lot of sprites in any normal game, but this is only 1/4 of the screen.

Figure 15.11. There are a lot of sprites in any normal game, but this is only 1/4 of the screen.

The new code in spriteUpdate(), marked in bold, adds additional cases to the switch statement for dealing with the power-ups.

   public void spriteUpdate(AnimatedSprite sprite) {
        switch(sprite.spriteType()) {
        case SPRITE_SHIP:
            warp(sprite);
            break;
        case SPRITE_BULLET:
            warp(sprite);
            break;
        case SPRITE_EXPLOSION:
            if (sprite.currentFrame() == sprite.totalFrames()-1) {
                sprite.setAlive(false);
            }
            break;
        case SPRITE_ASTEROID_BIG:
        case SPRITE_ASTEROID_MEDIUM:
        case SPRITE_ASTEROID_SMALL:
        case SPRITE_ASTEROID_TINY:
            warp(sprite);
            break;
        case SPRITE_POWERUP_SHIELD:
        case SPRITE_POWERUP_HEALTH:
        case SPRITE_POWERUP_250:
        case SPRITE_POWERUP_500:
        case SPRITE_POWERUP_1000:
        case SPRITE_POWERUP_GUN:
            warp(sprite);
            //make powerup animation wobble
            double rot = sprite.rotationRate();
            if (sprite.faceAngle() > 350) {
                sprite.setRotationRate(rot * -1);
                sprite.setFaceAngle(350);
            }
            else if (sprite.faceAngle() < 10) {
                sprite.setRotationRate(rot * -1);
                sprite.setFaceAngle(10);
            }
            break;
   
        }
   
   }

Grabbing Power-Ups

Next in the source code listing is the spriteCollision() event. All we need to do here is handle all the new power-ups that are in the game; this means that only the ship should collide with the power-ups. I’ve moved the test for the collision testing toggle to the top of this method, out of the keyboard handling code, because it belongs here instead. Some new lines have been added to increase the score whenever a bullet hits an asteroid and to deal with collisions when the shield is up. Note the changes in bold, as usual.

public void spriteCollision(AnimatedSprite spr1, AnimatedSprite spr2) {
     //jump out quickly if collisions are off
     if (!collisionTesting) return;

     //figure out what type of sprite has collided
     switch(spr1.spriteType()) {
     case SPRITE_BULLET: 
         //did bullet hit an asteroid?
         if (isAsteroid(spr2.spriteType())) {
             bumpScore(5);
               spr1.setAlive(false);
               spr2.setAlive(false);
               breakAsteroid(spr2);
         }
         break;
     case SPRITE_SHIP:
         //did asteroid crash into the ship?
         if (isAsteroid(spr2.spriteType())) {
             if (spr1.state() == STATE_NORMAL) {
                 if (keyShield) {
                     shield -= 1;
                 }
                 else {
                     collisionTimer = System.currentTimeMillis();
                     spr1.setVelocity(new Point2D(0, 0));
                     double x = spr1.position().X() - 10;  
                     double y = spr1.position().Y() - 10;
                     startBigExplosion(new Point2D(x, y));
                     spr1.setState(STATE_EXPLODING);
                     //reduce ship health after a hit
                     health -= 1;
                     if (health < 0) {
                         gameState = GAME_OVER;
                     }
                     //lose firepower when you get hit
                     firepower--;
                     if (firepower < 1) firepower = 1;
                 }
                 spr2.setAlive(false);
                 breakAsteroid(spr2);
              }
              //make ship temporarily invulnerable
              else if (spr1.state() == STATE_EXPLODING) {
                  if (collisionTimer + 3000 <
                       System.currentTimeMillis()) {
                       spr1.setState(STATE_NORMAL);
                  }
              }
          }   
          break;
case SPRITE_POWERUP_SHIELD:
    if (spr2.spriteType()==SPRITE_SHIP) {
          shield += 5;
          if (shield > 20) shield = 20;
          spr1.setAlive(false);
    }
    break;

case SPRITE_POWERUP_HEALTH:
    if (spr2.spriteType()==SPRITE_SHIP){
          health += 5;
          if (health > 20) health = 20;
          spr1.setAlive(false);
    }
    break;

case SPRITE_POWERUP_250:
    if (spr2.spriteType()= =SPRITE_SHIP) {
        bumpScore(250);
        spr1.setAlive(false);
    }
    break;

case SPRITE_POWERUP_500:
   if (spr2.spriteType()= =SPRITE_SHIP) {
         bumpScore(500);
         spr1.setAlive(false);
    }
    break;

case SPRITE_POWERUP_1000:
    if (spr2.spriteType()==SPRITE_SHIP) {
         bumpScore(1000);
         spr1.setAlive(false);
    }
    break;

case SPRITE_POWERUP_GUN:
    if (spr2.spriteType()= =SPRITE_SHIP) {
         firepower++;
if (firepower > 5) firepower = 5;
spr1.setAlive(false);
    }
    break;

 }

}

New Input Keys

The game now uses the Shift key to engage the ship’s shields and the Enter key to continue when the game is in the GAME_MENU or GAME_OVER state. There’s also a way to exit out of the game now: When the game is running, you can hit Escape to end the game (see Figure 15.12).

The Escape key will end the game immediately and allow you to start over.

Figure 15.12. The Escape key will end the game immediately and allow you to start over.

Here are the new key handlers in bold.

   public void gameKeyDown(int keyCode) {
        switch(keyCode) {
        case KeyEvent.VK_LEFT:
            keyLeft = true;
            break;
        case KeyEvent.VK_RIGHT:
            keyRight = true;
            break;
        case KeyEvent.VK_UP:
            keyUp = true;
            break;
        case KeyEvent.VK_CONTROL:
            keyFire = true;
            break;
        case KeyEvent.VK_B:
            //toggle bounding rectangles
            showBounds = !showBounds;
            break;
        case KeyEvent.VK_C:
            //toggle collision testing
            collisionTesting = !collisionTesting;
            break;
        case KeyEvent.VK_SHIFT:
            if ((!keyUp)&&(shield > 0))
                keyShield = true;
            else
                keyShield = false;
            break;
        case KeyEvent.VK_ENTER:
            if (gameState == GAME_MENU) {
                resetGame();
                resumeGame();
                gameState = GAME_RUNNING;
            }
            else if (gameState == GAME_OVER) {
                resetGame();
                resumeGame();
                gameState = GAME_RUNNING;
            }
            break;
        case KeyEvent.VK_ESCAPE:
            if (gameState == GAME_RUNNING) {
                pauseGame();> 
                gameState = GAME_OVER;
            }
            break;
        }
    }

Now let’s add a single new case to the gameKeyUp() event as well.

   public void gameKeyUp(int keyCode) {
        switch(keyCode) {
        case KeyEvent.VK_LEFT:
            keyLeft = false;
            break;
   case KeyEvent.VK_RIGHT:
       keyRight = false;
       break;
   case KeyEvent.VK_UP:
       keyUp = false;
       break;
   case KeyEvent.VK_CONTROL:
       keyFire = false;
       fireBullet();
       break;
   case KeyEvent.VK_SHIFT:
          keyShield = false;
          break;
       }
   }

Spawning Power-Ups

In the previous chapter we added a new method to the game called spawnPowerup(), which was left empty at the time. Due to that foresight, we do not have to make any changes to the breakAsteroid() method that makes this call. Instead, here is the fully functional spawnPowerup(). At the top of the code, a random percentage determines whether the power-up is actually created. I have it currently set to 12 percent, which provides some fair gameplay. If you want to make the game more difficult, reduce this value. To make it easier, increase it.

Even though 12 percent doesn’t sound like very many power-ups, keep in mind that every large asteroid produces three “mediums,” each of which produces three “smalls,” each of which produces three “tinys” (see Figure 15.13). That’s a whopping 27 tiny asteroids for every large one, and since the game starts out with 10 large ones—well, you can do that kind of math. In a single game session, 12 percent will generate about 30 power-ups! I think this value should be reduced to about 20 to make the game a bit more challenging, but each power-up has a limited lifetime, so it’s possible in the heat of battle that the player will only manage to grab a few of them.

Here are all of the asteroids you’ll run into in the game (pun intended).

Figure 15.13. Here are all of the asteroids you’ll run into in the game (pun intended).

The spawnPowerup() method creates a single power-up sprite with some standard properties that all power-ups share, and then it sets the specific properties using a random number. Since there are six power-ups, this random number determines the type of power-up.

   private void spawnPowerup(AnimatedSprite sprite) {
         //only a few tiny sprites spit out a powerup
         int n = rand.nextInt(100);
         if (n > 12) return;

         //use this powerup sprite
         AnimatedSprite spr = new AnimatedSprite(this, graphics());
         spr.setRotationRate(8);
         spr.setPosition(sprite.position());
         double velx = rand.nextDouble();
         double vely = rand.nextDouble();
         spr.setVelocity(new Point2D(velx, vely));
         spr.setLifespan(1500);
         spr.setAlive(true);
         //customize the sprite based on powerup type
         switch(rand.nextInt(6)) {
         case 0:
             //create a new shield powerup sprite
             spr.setImage(powerupShield.getImage());
             spr.setSpriteType(SPRITE_POWERUP_SHIELD);
             sprites().add(spr);
             break;
         case 1:
             //create a new health powerup sprite
             spr.setImage(powerupHealth.getImage());
             spr.setSpriteType(SPRITE_POWERUP_HEALTH);
             sprites().add(spr);
             break;
         case 2:
             //create a new 250-point powerup sprite
             spr.setImage(powerup250.getImage());
             spr.setSpriteType(SPRITE_POWERUP_250);
             sprites().add(spr);
             break;
         case 3:
             //create a new 500-point powerup sprite
             spr.setImage(powerup500.getImage());
             spr.setSpriteType(SPRITE_POWERUP_500);
             sprites().add(spr);
             break;
         case 4:
             //create a new 1000-point powerup sprite
             spr.setImage(powerup1000.getImage());
             spr.setSpriteType(SPRITE_POWERUP_1000);
             sprites().add(spr);
             break;
         case 5:
             //create a new gun powerup sprite
             spr.setImage(powerupGun.getImage());
             spr.setSpriteType(SPRITE_POWERUP_GUN);
             sprites().add(spr);
             break;
        }

   }

Making the Shield Work

Although the key events turn the ship’s shield on or off, the real work is done in the checkInput() method shown here. Let’s take a close-up look at the shield in action. Figure 15.14 shows the ship bombarded with asteroids, but the shield is taking all of the impact and protecting the ship (at least until the shield runs out!).

This close-up view shows multiple asteroids impacting the ship’s shields (and breaking apart into small asteroids or just blowing up).

Figure 15.14. This close-up view shows multiple asteroids impacting the ship’s shields (and breaking apart into small asteroids or just blowing up).

We also need to make a change to the new global game state so it will ignore input events unless the game is running—in other words, it should ignore gameplay input changes when in the GAME_MENU or GAME_OVER state. The new code is shown in bold.

   public void checkInput() {
        if (gameState != GAME_RUNNING) return;
    
        //the ship is always the first sprite in the linked list
        AnimatedSprite ship = (AnimatedSprite)sprites().get(0);
        if (keyLeft) {
            //left arrow rotates ship left 5 degrees
            ship.setFaceAngle(ship.faceAngle() - SHIPROTATION);
            if (ship.faceAngle() <0)
                ship.setFaceAngle(360 - SHIPROTATION);

        } else if (keyRight) {
            //right arrow rotates ship right 5 degrees
            ship.setFaceAngle(ship.faceAngle() + SHIPROTATION);
            if (ship.faceAngle() > 360)
                ship.setFaceAngle(SHIPROTATION);
        }
        if (keyUp) {
            //up arrow applies thrust to ship
            ship.setImage(shipImage[1].getImage());
            applyThrust();
        }
        else if (keyShield) {
            ship.setImage(shipImage[2].getImage());
        }
       else
            //set ship image to normal non-thrust image
            ship.setImage(shipImage[0].getImage());
   }

Making Use of Weapon Upgrade Power-Ups

The new weapon upgrades are awesome, as you saw earlier in the chapter—wouldn’t you agree? This is the most interesting new gameplay feature, without a doubt. Each time you get a weapon upgrade, it adds another gun to your ship. However, if your ship gets hit, you lose an upgrade, so it’s pretty tough to keep those upgrades. The good news is, when you get level-four or level-five guns, your ship is so powerful that it’s fairly easy to clear out the asteroids in short order.

To support weapon upgrades, the code in fireBullet() has been completely rewritten, and two new support methods were needed: adjustDirection() and stockBullet(). The new “bullets” emerge from the center of the ship, and then spread out in various patterns, based on the upgrade level (which is in a variable called firepower). I’ll show you the new code for fireBullet() first.

   public void fireBullet() {
         //create the new bullet sprite
         AnimatedSprite[] bullets = new AnimatedSprite[6];
         switch(firepower) {
         case 1: //single shot
             bullets[0] = stockBullet();
             sprites().add(bullets[0]);
             break;
         case 2: //double shot
             bullets[0] = stockBullet();
             adjustDirection(bullets[0], -4);
             sprites().add(bullets[0]);
             bullets[1] = stockBullet();
             adjustDirection(bullets[1], 4);
             sprites().add(bullets[1]);      
             break;
         case 3: //triple shot
             bullets[0] = stockBullet();
             adjustDirection(bullets[0], -4);
             sprites().add(bullets[0]);
             bullets[1] = stockBullet();
             sprites().add(bullets[1]);
             bullets[2] = stockBullet();
             adjustDirection(bullets[2], 4);
             sprites().add(bullets[2]);
             break;
         case 4: //4-shot
             bullets[0] = stockBullet();
             adjustDirection(bullets[0], -5);
             sprites().add(bullets[0]);
             bullets[1] = stockBullet();
             adjustDirection(bullets[1], 5);
             sprites().add(bullets[1]);
             bullets[2] = stockBullet();
             adjustDirection(bullets[2], -10);
             sprites().add(bullets[2]);
             bullets[3] = stockBullet();
             adjustDirection(bullets[3], 10);
             sprites().add(bullets[3]);
             break;
         case 5: //5-shot
             bullets[0] = stockBullet();
             adjustDirection(bullets[0], -6);
             sprites().add(bullets[0]);
             bullets[1] = stockBullet();
             adjustDirection(bullets[1], 6);
             sprites().add(bullets[1]);
             bullets[2] = stockBullet();
             adjustDirection(bullets[2], -15);
             sprites().add(bullets[2]);
             bullets[3] = stockBullet();
             adjustDirection(bullets[3], 15);
             sprites().add(bullets[3]);
             bullets[4] = stockBullet();
             adjustDirection(bullets[4], -60);
             sprites().add(bullets[4]);
             bullets[5] = stockBullet();
             adjustDirection(bullets[5], 60);
             sprites().add(bullets[5]);
             break;
         }
         shoot.play();
   }

Here’s the new adjustDirection() support method, which basically just cuts down on the amount of code in fireBullet() because this code is repeated for every single bullet launched. This method is new, so you should add it below the fireBullet() method in your code listing for GalacticWar.java.

   private void adjustDirection(AnimatedSprite sprite, double angle) {
        angle = sprite.faceAngle() + angle;
        if (angle < 0) angle += 360;
        else if (angle > 360) angle -= 360;
        sprite.setFaceAngle(angle);
        sprite.setMoveAngle(sprite.faceAngle()-90);
        angle = sprite.moveAngle();
        double svx = calcAngleMoveX(angle) * BULLET_SPEED;
        double svy = calcAngleMoveY(angle) * BULLET_SPEED;
        sprite.setVelocity(new Point2D(svx, svy)); 
   }

The next support method that helps out fireBullet() is called stockBullet(). This method creates a stock bullet sprite with all of the standard values needed to fire a single bullet from the center of the ship. The custom upgraded bullets are modified from this stock bullet to create the various firepower patterns you see in the game. This method returns a new AnimatedSprite object.

   private AnimatedSprite stockBullet() {
         //the ship is always the first sprite in the linked list
         AnimatedSprite ship = (AnimatedSprite)sprites().get(0);
   
         AnimatedSprite bul = new AnimatedSprite(this, graphics());
         bul.setAlive(true);
         bul.setImage(bulletImage.getImage());
         bul.setFrameWidth(bulletImage.width());
         bul.setFrameHeight(bulletImage.height());
         bul.setSpriteType(SPRITE_BULLET);
         bul.setLifespan(90);
         bul.setFaceAngle(ship.faceAngle());
         bul.setMoveAngle(ship.faceAngle() - 90);
   
         //set the bullet's velocity
         double angle = bul.moveAngle();
         double svx = calcAngleMoveX(angle) * BULLET_SPEED;
         double svy = calcAngleMoveY(angle) * BULLET_SPEED;
         bul.setVelocity(new Point2D(svx, svy));
   
         //set the bullet's starting position
         double x = ship.center().X() - bul.imageWidth()/2;
         double y = ship.center().Y() - bul.imageHeight()/2;
         bul.setPosition(new Point2D(x,y));

         return bul;
     }

Tallying the Score

The final change to the Galactic War source code is the addition of a new method called bumpScore(). This is called in the collision routine to increase the player’s score for every asteroid hit by a weapon. (Collisions with the ship don’t count.)

   public void bumpScore(int howmuch) {
        score += howmuch;
        if (score > highscore)
            highscore = score;
   }

What You Have Learned

This has certainly been an eye-opening chapter! It’s amazing what is possible now that we have a sprite engine with such dynamic sprite-handling capabilities. It’s now possible, as you have seen, to add new power-ups and entirely new gameplay elements by simply adding new cases to the switch statements in the key event methods, as well as adding the few lines of code to load new images. The end result is now a fully polished, retail-quality game that’s ready to take on any game in the web-based casual game market.

Here’s what you have learned:

  • How to add power-ups to the game

  • How to enhance gameplay with new features

  • How to fire a spread of bullets at various angles

  • How to add a game state to give the game a start and an ending

Review Questions

The following questions will help you to determine how well you have learned the subjects discussed in this chapter. The answers are provided in Appendix A, “Chapter Quiz Answers.”

1.

What method in GalacticWar.java makes it possible to add power-ups to the game when a tiny asteroid is destroyed?

2.

What construct does the sprite engine (in Game.java) use to manage the sprites?

3.

How many weapon upgrades are available now in Galactic War?

4.

How many different point-value power-ups are there in the game?

5.

What method in GalacticWar.java returns a stock bullet sprite object, which is then tweaked to produce the upgraded bullet spreads?

6.

How many different asteroid images are there in Galactic War?

7.

If you wanted to add another weapon upgrade to the game, which method would you need to modify?

8.

How many sprites is the sprite engine capable of handling at a time?

9.

How many bullets are fired at a time with the fifth-level weapon upgrade?

10.

What is the name of the static int that represents the game state when the game is running normally?

On Your Own

There are so many possibilities with this game that I hardly know where to start. Since I consider the game finished in the sense that it is sufficiently stocked with features and gameplay elements to meet the goals I laid out for this book, I will just make some suggestions for the game.

I would like to add a black hole that randomly crosses the screen from time to time, sucking in everything it touches. Wouldn’t that be cool?

Another great feature would be to have an alien spacecraft come onto the screen from time to time and shoot at the player. To keep the alien ship from getting hit by asteroids, the ship would engage a shield whenever it collides with an asteroid; otherwise, it would have to navigate through the asteroid field, and that’s some code I would not care to write!

Here is yet another idea to improve gameplay, since the game is really hard. The game could start off with a single big asteroid for Level 1, and then add an additional big asteroid to each level the player completes. Although the game can handle an unlimited number of sprites, I would end the game at level 10 to keep it reasonable. Since the game currently just throws 10 asteroids at the player from the start, switching to a level-based system would greatly improve the fun factor!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.234.118