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
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.
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.
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 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?).
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
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.
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.
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; } }
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.
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.”
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.
3.16.139.62