Chapter 12. Galactic War: Sprites and Collision Boxes

The goal of this chapter is to develop a way to handle the game objects moving around on the screen and to enhance the Galactic War game with some significant new gameplay features using sprites rather than just images and vectors.

Here are the specific topics you will learn about:

  • Upgrading Galactic War to a sprite-based game

  • Adding new artwork to the game

  • Adding new functionality to the gameplay

Creating the Project

The Galactic War game has so much potential that I’m eager to implement, but the game has been somewhat hobbled up to this point due to it being limited—first by vectors, then by simple bitmaps. Now that we have this useful new Sprite class available with some serious functionality built into it, the game will really start to resemble what I am envisioning for it.

The first thing I want to do is enlarge the applet window to 800 × 600. I realize that 640 × 480 provides better support for users of low-end PCs, but the truth of the matter is that most users run their PCs at 1024 × 768, while a similarly large percentage use 1280 × 960 or 1280 × 1024. Only a tiny minority of PC users run the system at the lower resolutions. A resolution of 800 × 600 will give the game more breathing room due to the large size of the asteroids.

As you can see in Figure 12.1, the game has been completely converted to bitmapped graphics, finally doing away with the vestiges of its vector graphics ancestry.

The new version of Galactic War.

Figure 12.1. The new version of Galactic War.

The Galactic War Bitmaps

There are a lot of high-quality bitmaps in the game now, giving it a sharp, catchy appearance. The first and most significant change to the game is that it now uses a background bitmap instead of a blank, black background. The bluespace.png background image was created by Reiner Prokein and is shown in Figure 12.2. This is one example of many backgrounds, sprites, and tiles you can find in the Reiner’s Tileset collection at www.reinerstileset.de.

The background image used in Galactic War.

Figure 12.2. The background image used in Galactic War.

There are five different types of asteroids of the large variety in this iteration of Galactic War. These gigantic rocks will be blasted into smaller pieces in later iterations of the game; at this point, shooting one of them simply causes it to disappear. I wanted to use very large starting asteroids to make the game more interesting by breaking them up into many smaller rocks. The asteroids shown in Figure 12.3 were rendered by a talented 3D artist by the name of Edgar Ibarra, who actually created these asteroid models for a project I was working on several years ago. I have converted the asteroid bitmaps to the PNG format using Paint Shop Pro, as well as applied a transparency mask in the process.

The five unique asteroids featured in the game.

Figure 12.3. The five unique asteroids featured in the game.

The spaceship has also been upgraded significantly from the version presented in the previous chapter. I based the spaceship on a design by Reiner Prokein and significantly modified it to give it a more distinct look with a pseudo-3D appearance like an X-Wing from Star Wars. (Note the four guns, top and bottom.) Figure 12.4 shows the ship sprite. This is a great-looking ship, if I do say so myself. I’d like to add a small fire animation coming out of the engine nozzles when you press the Up arrow key to apply thrust (future enhancement?).

The player’s spaceship used in Galactic War.

Figure 12.4. The player’s spaceship used in Galactic War.

The New and Improved Source Code

There’s a lot of source code here for the new version of the game. This is necessary because the game is now taking shape with a lot of functionality, and it has room for new features in upcoming chapters. Future chapters will actually involve additions and changes to the monumental work done in this chapter, which is now a new foundational version of the game. The first thing I’d like to point out is that Galactic War no longer uses the VectorShape class, so you can remove it from the project. I have made no changes to BaseGameEntity or ImageEntity, so those can remain in the project. (ImageEntity is used by the Sprite class internally.) You can see the current state of the project by looking at the list of files now required by the project:

  • BaseGameEntity.java

  • GalacticWar.java

  • ImageEntity.java

  • Point2D.java

  • Sprite.java

Tip

Two obviously glaring omissions from the game so far have been sound effects and music. We will add sound to the game in Chapter 15, “Galactic War: Finishing the Game.”

The first code listing here includes the main class definition for the game, along with the global variables. I have highlighted key changes to the game in bold text.

/*****************************************************
* GALACTIC WAR, Chapter 12
*****************************************************/
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.util.*;
import java.lang.System;

/*****************************************************
* Primary class for the game
*****************************************************/
public class GalacticWar extends Applet implements Runnable, KeyListener {
    //global constants
    static int SCREENWIDTH = 800;
    static int SCREENHEIGHT = 600;
    static int CENTERX = SCREENWIDTH / 2;
    static int CENTERY = SCREENHEIGHT / 2;
    static int ASTEROIDS = 10;
    static int BULLETS = 10;
    static int BULLET_SPEED = 4;
    static double ACCELERATION = 0.05;

    //sprite state values
    static int SPRITE_NORMAL = 0;
    static int SPRITE_COLLIDED = 1;

    //the main thread becomes the game loop
    Thread gameloop;

    //double buffer objects
    BufferedImage backbuffer;
    Graphics2D g2d;

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

    //define the game objects
    ImageEntity background;
    Sprite ship;
    Sprite[] ast = new Sprite[ASTEROIDS];
    Sprite[] bullet = new Sprite[BULLETS];
    int currentBullet = 0;

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

    //define the sound effects objects
    SoundClip shoot;
    SoundClip explode;

    //simple way to handle multiple keypresses
    boolean keyDown, keyUp, keyLeft, keyRight, keyFire;
    //frame rate counter
    int frameCount = 0, frameRate = 0;
    long startTime = System.currentTimeMillis();

The showBounds and collisionTesting variables are used to draw some helpful information on the screen, which is invaluable while developing a complex game. Figure 12.5 shows the game with bounding boxes and collision testing turned on. When a collision occurs, the bounding boxes of the two objects are drawn in red instead of blue. You can turn off collision testing altogether, if needed for testing.

Bounding boxes and collisions are toggled with the B and C keys.

Figure 12.5. Bounding boxes and collisions are toggled with the B and C keys.

This brings up an important point about the current state of the game. There are a lot of new features in the game, and it pretty much looks the way it will when it is finished—except for a scrolling background and a few other tidbits. The game really doesn’t take any action at this point based on a collision. Instead, collisions are detected, and that status information is made available to the game through the sprite’s state property (which is generic enough for use however you see fit). We’ll revisit the process of responding to collisions in the next chapter.

Tip

We need a little more functionality out of the java.awt.Point class, so we’ll be building our own Point2D class in this chapter. Try not to confuse this with java.awt.geom.Point2D, which is the base class for java.awt.Point, and has no relation to our custom Point2D class.

Next up is the init() event method of the applet.

   /*****************************************************
    * applet init event
    *****************************************************/
   public void init() {
       //create the back buffer for smooth graphics
       backbuffer = new BufferedImage(SCREENWIDTH, SCREENHEIGHT,
           BufferedImage.TYPE_INT_RGB);
       g2d = backbuffer.createGraphics();
   
       //load the background image
       background = new ImageEntity(this);
       background.load("bluespace.png");
   
       //set up the ship
       ship = new Sprite(this, g2d);
       ship.load("spaceship.png");
       ship.setPosition(new Point2D(CENTERX, CENTERY));
       ship.setAlive(true);
   
       //set up the bullets
       for (int n = 0; n<BULLETS; n++) {
           bullet[n] = new Sprite(this, g2d);
           bullet[n].load("plasmashot.png");
       }
   
       //set up the asteroids
       for (int n = 0; n<ASTEROIDS; n++) {
           ast[n] = new Sprite(this, g2d);
           ast[n].setAlive(true);
           //load the asteroid image
           int i = rand.nextInt(5)+1;
           ast[n].load("asteroid" + i + ".png");
           //set to a random position on the screen
           int x = rand.nextInt(SCREENWIDTH);
           int y = rand.nextInt(SCREENHEIGHT);
           ast[n].setPosition(new Point2D(x, y));
           //set rotation angles to a random value
           ast[n].setFaceAngle(rand.nextInt(360));
           ast[n].setMoveAngle(rand.nextInt(360));
           ast[n].setRotationRate(rand.nextDouble());
           //set velocity based on movement direction
           double ang = ast[n].moveAngle() - 90;
           double velx = calcAngleMoveX(ang);
           double vely = calcAngleMoveY(ang);
           ast[n].setVelocity(new Point2D(velx, vely));
       }
   
       //start the user input listener
       addKeyListener(this);
   }

The next section of code is the main game update portion of the game, including the applet update() event, the paint() event, and three methods to draw the game objects: drawShip(), drawBullets(), and drawAsteroids().

   /*****************************************************
    * applet update event to redraw the screen
    *****************************************************/
   public void update(Graphics g) {
       //calculate frame rate
       frameCount++;
       if (System.currentTimeMillis() > startTime + 1000) {
           startTime = System.currentTimeMillis();
           frameRate = frameCount;
           frameCount = 0;
       }
   
       //draw the background
       g2d.drawImage(background.getImage(),0,0,
           SCREENWIDTH-1,SCREENHEIGHT-1,this);
         //draw the game graphics
         drawAsteroids();
         drawShip();
         drawBullets();
   
         //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 (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);
         }
   
         //repaint the applet window
         paint(g);
   }
   
   /*****************************************************
    * drawShip called by applet update event
    *****************************************************/
   public void drawShip() {
         // set the transform for the image
         ship.transform();
         ship.draw();
         if (showBounds) {
             if (ship.state() == SPRITE_COLLIDED)
              ship.drawBounds(Color.RED);
          else
               ship.drawBounds(Color.BLUE);
         }
   }
   
   /*****************************************************
    * drawBullets called by applet update event
    *****************************************************/
   public void drawBullets() {
       for (int n = 0; n < BULLETS; n++) {
           if (bullet[n].alive()) {
               //draw the bullet
               bullet[n].transform();
               bullet[n].draw();
               if (showBounds) {
                   if (bullet[n].state() = = SPRITE_COLLIDED)
                       bullet[n].drawBounds(Color.RED);
                   else
                    bullet[n].drawBounds(Color.BLUE);
               }
           }
       }
   }
   /*****************************************************
    * drawAsteroids called by applet update event
    ******************************************************/
   public void drawAsteroids() {
       for (int n = 0; n < ASTEROIDS; n++) {
           if (ast[n].alive()) {
               //draw the asteroid
               ast[n].transform();
               ast[n].draw();
               if (showBounds) {
                   if (ast[n].state() = = SPRITE_COLLIDED)
                       ast[n].drawBounds(Color.RED);
                   else
                       ast[n].drawBounds(Color.BLUE);
               }
           }
       }
   }
   /*****************************************************
    * applet window repaint event--draw the back buffer
    *****************************************************/
   public void paint(Graphics g) {
       g.drawImage(backbuffer, 0, 0, this);
   }

The next section of code updates the game via the gameloop thread, which calls gameUpdate(). This method, in turn, calls methods to process user input; update the ship, bullets, and asteroids; and perform collision testing.

   /*****************************************************
    * thread start event -- start the game loop running
    *****************************************************/
   public void start() {
       gameloop = new Thread(this);
       gameloop.start();
   }
   /*****************************************************
    * thread run event (game loop)
    *****************************************************/
   public void run() {
       //acquire the current thread
       Thread t = Thread.currentThread();
       //keep going as long as the thread is alive
       while (t == gameloop) {
           try {
               Thread.sleep(20);
           }
           catch(InterruptedException e) {
               e.printStackTrace();
           }
           //update the game loop
           gameUpdate();
           repaint();
       }
   }
   /*****************************************************
    * thread stop event
    *****************************************************/
   public void stop() { 
       gameloop = null;
   }
   
   /*****************************************************
    * move and animate the objects in the game
    *****************************************************/
   private void gameUpdate() {
       checkInput();
       updateShip();
       updateBullets();
       updateAsteroids();
       if (collisionTesting) checkCollisions();
   }
   
   /*****************************************************
    * Update the ship position based on velocity
    *****************************************************/
   public void updateShip() {
       ship.updatePosition();
       double newx = ship.position().X();
       double newy = ship.position().Y();
   
       //wrap around left/right
       if (ship.position().X() < -10)
           newx = SCREENWIDTH + 10;
       else if (ship.position().X() > SCREENWIDTH + 10)
           newx = -10;
   
       //wrap around top/bottom
       if (ship.position().Y() < -10)
           newy = SCREENHEIGHT + 10;
       else if (ship.position().Y() > SCREENHEIGHT + 10)
           newy = -10;
   
       ship.setPosition(new Point2D(newx, newy));
       ship.setState(SPRITE_NORMAL);
   }
   
   /*****************************************************
    * Update the bullets based on velocity
    *****************************************************/
   public void updateBullets() {
       //move the bullets
       for (int n = 0; n < BULLETS; n++) {
           if (bullet[n].alive()) {
   
               //update bullet's x position
               bullet[n].updatePosition();
   
               //bullet disappears at left/right edge
               if (bullet[n].position().X() < 0 ||
                   bullet[n].position().X() > SCREENWIDTH)
               {
                   bullet[n].setAlive(false);
               }
   
               //update bullet's y position
               bullet[n].updatePosition();
   
               //bullet disappears at top/bottom edge
               if (bullet[n].position().Y() < 0 ||
                   bullet[n].position().Y() > SCREENHEIGHT)
               {
                   bullet[n].setAlive(false);
               }
   
               bullet[n].setState(SPRITE_NORMAL);
           }
       }
   }
   
   /*****************************************************
    * Update the asteroids based on velocity
    *****************************************************/
   public void updateAsteroids() {
       //move and rotate the asteroids
       for (int n = 0; n < ASTEROIDS; n++) {
           if (ast[n].alive()) {
               //update the asteroid's position and rotation
               ast[n].updatePosition();
               ast[n].updateRotation();
               int w = ast[n].imageWidth()-1;
               int h = ast[n].imageHeight()-1;
               double newx = ast[n].position().X();
               double newy = ast[n].position().Y();
   
               //wrap the asteroid around the screen edges
               if (ast[n].position().X() < -w)
                   newx = SCREENWIDTH + w;
               else if (ast[n].position().X() > SCREENWIDTH + w)
                   newx = -w;
               if (ast[n].position().Y() < -h)
                   newy = SCREENHEIGHT + h;
               else if (ast[n].position().Y() > SCREENHEIGHT + h)
                   newy = -h;
   
               ast[n].setPosition(new Point2D(newx,newy));
               ast[n].setState(SPRITE_NORMAL);
           }
       }
   }
   
   /*****************************************************
    * Test asteroids for collisions with ship or bullets
    *****************************************************/
   public void checkCollisions() {
       //check for collision between asteroids and bullets
       for (int m = 0; m<ASTEROIDS; m++) {
           if (ast[m].alive()) {
               //iterate through the bullets
               for (int n = 0; n < BULLETS; n++) {
                   if (bullet[n].alive()) {
                      //collision?
                      if (ast[m].collidesWith(bullet[n])) {
                          bullet[n].setState(SPRITE_COLLIDED);
                          ast[m].setState(SPRITE_COLLIDED);
                          explode.play();
                      }
                   }
               }
           }
       }
       //check for collision asteroids and ship
       for (int m = 0; m<ASTEROIDS; m++) {
           if (ast[m].alive()) {
               if (ship.collidesWith(ast[m])) {
                   ast[m].setState(SPRITE_COLLIDED);
                   ship.setState(SPRITE_COLLIDED);
                   explode.play();
               }
           }
       }
   }

The next section of code processes keyboard input. The game has progressed to the point where the simplistic keyboard input from earlier chapters was insufficient, so I’ve added support to the game for multiple key presses now. This works through the use of several global variables: keyLeft, keyRight, and so on. These boolean variables are set to true during the keyPressed() event and set to false during the keyReleased() event method. This provides support for multiple keys at the same time in a given frame of the game loop. There is a practical limit to the number of keys you will be able to press at a time, but this code makes the game fluid-looking, and the input is smoother than the jerky input in the last chapter.

   /*****************************************************
    * process keys that have been pressed
    *****************************************************/
   public void checkInput() {
       if (keyLeft) {
           //left arrow rotates ship left 5 degrees
           ship.setFaceAngle(ship.faceAngle() - 5);
           if (ship.faceAngle() < 0) ship.setFaceAngle(360 - 5);
       }
       else if (keyRight) {
           //right arrow rotates ship right 5 degrees
           ship.setFaceAngle(ship.faceAngle() + 5);
           if (ship.faceAngle() > 360) ship.setFaceAngle(5);
       }
       if (keyUp) {
           //up arrow applies thrust to ship
           applyThrust();
       }
   }
   
   /*****************************************************
    * key listener events
    *****************************************************/
   public void keyTyped(KeyEvent k) { }
   public void keyPressed(KeyEvent k) {
       switch (k.getKeyCode()) {
       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;
       }
   }
   public void keyReleased(KeyEvent k) {
       switch (k.getKeyCode()) {
       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;
       }
   }
   
   public void applyThrust() {
       //up arrow adds thrust to ship (1/10 normal speed)
       ship.setMoveAngle(ship.faceAngle() - 90);
   
       //calculate the X and Y velocity based on angle
       double velx = ship.velocity().X();
       velx += caleAngleMoveX(ship.moveAngle()) * ACCELERATION;
       double vely = ship.velocity().Y();
       vely += calcAngleMoveY(ship.moveAngle()) * ACCELERATION;
       ship.setVelocity(new Point2D(velx, vely));
   }
   
   public void fireBullet() {
       //fire a bullet
       currentBullet++;
       if (currentBullet > BULLETS - 1) currentBullet = 0;
       bullet[currentBullet].setAlive(true);
   
       //set bullet's starting point
       int w = bullet[currentBullet].imageWidth();
       int h = bullet[currentBullet].imageHeight();
       double x = ship.center().X() - w/2;
       double y = ship.center().Y() - h/2;
       bullet[currentBullet].setPosition(new Point2D(x,y));
   
       //point bullet in same direction ship is facing
       bullet[currentBullet].setFaceAngle(ship.faceAngle());
       bullet[currentBullet].setMoveAngle(ship.faceAngle() - 90);
   
       //fire bullet at angle of the ship
       double angle = bullet[currentBullet].moveAngle();
       double svx = calcAngleMoveX(angle) * BULLET_SPEED;
       double svy = calcAngleMoveY(angle) * BULLET_SPEED;
       bullet[currentBullet].setVelocity(new Point2D(svx, svy));
   
       //play shoot sound
       shoot.play();
   }

The last section of code concludes the main code listing for this new version of Galactic War, implementing the now-familiar calcAngleMoveX() and calc AngleMoveY() methods.

       /*****************************************************
        * Angular motion for X and Y is calculated
        *****************************************************/
       public double calcAngleMoveX(double angle) {
           double movex = Math.cos(angle * Math.PI / 180);
           return movex;
       }
       public double calcAngleMoveY(double angle) {
           double movey = Math.sin(angle * Math.PI / 180);
           return movey;
       }
   }

What You Have Learned

This significant chapter produced a monumental new version of Galactic War that is a foundation for the chapters to come. The final vestiges of the game’s vector-based roots have been discarded, and the game is now fully implemented with bitmaps. In this chapter, you learned:

  • How game entities can become unmanageable without a handler (such as the Sprite class).

  • How the use of a sprite class dramatically cleans up the source code for a game.

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.

Which support class helps manage the position and velocity of sprites?

2.

During which keyboard event should you disable a keypress variable, when detecting multiple key presses with global variables?

3.

What is the name of the sprite collision detection routine used in Galactic War?

4.

Which method in the Applet class provides a way to load images from a JAR file?

5.

Which Java package do you need to import to use the Graphics2D class?

6.

What three numeric data types does the Point2D class support for the X and Y values?

7.

How does the use of a class such as Point2D improve a game’s source code, versus using simple variables?

8.

Which property in the Sprite class determines the angle at which the sprite will move?

9.

Which property in the Sprite class determines the angle at which a sprite is pointed?

10.

How many milliseconds must the game use as a delay in order to achieve a frame rate of 60 frames per second?

On Your Own

The Galactic War game is in a transition at this point, after having been upgraded significantly from vector-based graphics. At present, it does not perform any action due to collisions other than to report that a collision has occurred. We want to separate the collision testing code from the collision response code. Add a method that is called from gameUpdate() that displays the position (x,y) of any object that has collided with another object, for debugging purposes. You can do this by looking at a sprite’s state property.

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

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