Solution for Programming Exercise 6.6
This page contains a sample solution to one of the exercises from Introduction to Programming Using Java.
Exercise 6.6:
For this exercise, you should modify the SubKiller game from Subsection 6.3.6. You can start with the existing source code, from the file SubKiller.java. Modify the game so it keeps track of the number of hits and misses and displays these quantities. That is, every time the depth charge blows up the sub, the number of hits goes up by one. Every time the depth charge falls off the bottom of the screen without hitting the sub, the number of misses goes up by one. There is room at the top of the canvas to display these numbers. To do this exercise, you only have to add a half-dozen lines to the source code. But you have to figure out what they are and where to add them. To do this, you'll have to read the source code closely enough to understand how it works.
You can do this exercise by adding as few as seven lines to the original version, SubKiller.java (plus changing the name of the class, if you want to do that). I used two lines to declare instance variables named hits and misses. These variables have to be updated whenever the depth charge hits the sub or falls off the bottom of the canvas. These events are already detected by the canvas, in the updateForNextFrame() method of the Bomb class. At the point where this method detects that the depth charge has hit the sub, I add the command "hits++;" to chalk up another hit for the user. At the point in where it is determined that the y-coordinate of the depth charge has exceeded the height of the canvas, I add the command "misses++;" to record the fact that the sub has escaped destruction this time.
The only other thing to do is to display the number of hits and misses at the top of the canvas. This is part of drawing the picture, so it is done in the draw() method. The information is output with two drawString commands. We need one more command to set the color for the strings:
g.setFill(Color.BLACK); g.fillText("Number of hits: " + hits, 15, 24); g.fillText("Number of misses: " + misses, 15, 45);
(Using the coordinates in these statements, the messages about hits and misses were drawn over the "(ANIMATION PAUSED)" message, so I also moved the "(ANIMATION PAUSED)" message from x-coordinate 30 to x-coordinate 300 to make room for the new messages.)
I made one further non-essential change when I decided that I wanted the output to be displayed in a larger font. I create a new font, store it in an instance variable named infoFont, and use the command "g.setFont(infoFont);" before drawing the strings.
The source code is shown below. Changes from the original version are shown in red.
import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.layout.Pane; import javafx.stage.Stage; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.input.KeyCode; import javafx.animation.AnimationTimer; import javafx.scene.paint.Color; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; /** * This program implements a simple arcade game in which the user tries to blow * up a "submarine" (a black oval) by dropping "depth charges" (a red disk) from * a "boat" (a blue roundrect). The user moves the boat with the left- and * right-arrow keys and drops the depth charge with the down-arrow key. * The sub moves left and right erratically along the bottom of the canvas. * The number of hits and the number of misses are shown at the top of the window. */ public class SubKillerWithScore extends Application { public static void main(String[] args) { launch(); } //------------------------------------------------------------------------ private AnimationTimer timer; // AnimationTimer that drives the animation. private final int width = 640, height = 480; // The size of the canvas private Boat boat; // The boat, bomb, and sub objects are defined private Bomb bomb; // by nested classes Boat, Bomb, and Submarine, private Submarine sub; // which are defined later in this class. private int hits; // The number of times the user has hit the sub. private int misses; // The number of times the user has missed the sub. private Font infoFont = Font.font(null, FontWeight.BOLD, 16); // A font for displaying the numbers of hits and misses. private Canvas canvas; // The canvas where everything is drawn. private Stage stage; // This program's window. /** * Start method creates the window content and configures event listening. */ public void start(Stage stage) { /* Create objects. */ boat = new Boat(); sub = new Submarine(); bomb = new Bomb(); canvas = new Canvas(width,height); this.stage = stage; /* Set up the GUI */ Pane root = new Pane(canvas); Scene scene = new Scene(root); stage.setScene(scene); stage.setTitle("Sub Killer -- Use arrow keys to play!"); stage.setResizable(false); /* Configure event listeners and animation. */ scene.setOnKeyPressed( evt -> { // The key listener responds to keyPressed events on the canvas. Only // the left-, right-, and down-arrow keys have any effect. The left- and // right-arrow keys move the boat while down-arrow releases the bomb. KeyCode code = evt.getCode(); // Which key was pressed? if (code == KeyCode.LEFT) { // Move the boat left. (If this moves the boat out of the frame, its // position will be adjusted in the boat.updateForNewFrame() method.) boat.centerX -= 15; } else if (code == KeyCode.RIGHT) { // Move the boat right. (If this moves boat out of the frame, its // position will be adjusted in the boat.updateForNewFrame() method.) boat.centerX += 15; } else if (code == KeyCode.DOWN) { // Start the bomb falling, if it is not already falling. if ( bomb.isFalling == false ) bomb.isFalling = true; } } ); stage.focusedProperty().addListener( (obj,oldVal,newVal) -> { // This listener turns the animation off when this program's // window does not have the input focus. if (newVal) { // The window has gained focus. timer.start(); } else { // The window has lost focus. timer.stop(); } draw(); // Appearance changes depending on focus. }); timer = new AnimationTimer( ) { // The handle method is called once per frame while the // animation is running. There should be about 60 // frames per second. long previousFrameTime; public void handle(long time) { if (time - previousFrameTime > 0.99e9/60) { // This program is designed to run at 60 frames per // second. Since handle() can be called more frequently // than that, we do an update-and-draw only if the // time since the previous update-and-draw is greater // than 0.99/60 seconds. Note that time is measured in // nanoseconds, where one second equals 1e9 nanoseconds. boat.updateForNewFrame(); bomb.updateForNewFrame(); sub.updateForNewFrame(); draw(); previousFrameTime = time; } } }; /* Show the window. */ stage.show(); timer.start(); // (Start animation -- probably redundant, since // focus listener should be called when window opens.) } // end start() /** * The draw() method draws the current state of the game. It * draws a gray or cyan border around the canvas to indicate whether or not * the window has the input focus. It draws the boat, sub, and bomb by * calling their respective draw() methods. This method is called in * each frame and when the window gains or loses focus. */ public void draw() { GraphicsContext g = canvas.getGraphicsContext2D(); g.setFont(infoFont); g.setFill(Color.rgb(0,200,0)); // Fill with green, erasing previous frame. g.fillRect(0,0,width,height); if (stage.isFocused()) { // draw a CYAN border on the window when the window has focus. g.setStroke(Color.CYAN); } else { // Draw gray border and a message when window does not have focus. g.setFill(Color.BLACK); g.fillText("(ANIMATION PAUSED)", 300, 30); // (x-coord was changed) g.setStroke(Color.DARKGRAY); } g.setLineWidth(3); g.strokeRect(1.5,1.5,width-3,height-3); // Draw a 3-pixel border. boat.draw(g); // The three objects draw themselves. sub.draw(g); bomb.draw(g); g.setFill(Color.BLACK); g.fillText("Number of hits: " + hits, 15, 24); g.fillText("Number of misses: " + misses, 15, 45); } // end draw() /** * This nested class defines the boat. */ private class Boat { int centerX, centerY; // Current position of the center of the boat. Boat() { // Constructor centers the boat horizontally, 80 pixels from top. centerX = width/2; centerY = 80; } void updateForNewFrame() { // Makes sure boat has not moved off screen. if (centerX < 0) centerX = 0; else if (centerX > width) centerX = width; } void draw(GraphicsContext g) { // Draws the boat at its current location. g.setFill(Color.BLUE); g.fillRoundRect(centerX - 40, centerY - 20, 80, 40, 20, 20); } } // end nested class Boat /** * This nested class defines the bomb. */ private class Bomb { int centerX, centerY; // Current position of the center of the bomb. boolean isFalling; // If true, the bomb is falling; if false, it // is attached to the boat. Bomb() { // Constructor creates a bomb that is initially attached to boat. isFalling = false; } void updateForNewFrame() { // If bomb is falling, take appropriate action. if (isFalling) { if (centerY > height) { // Bomb has missed the submarine. It is returned to its // initial state, with isFalling equal to false. isFalling = false; misses++; // The number of misses goes up by one. } else if (Math.abs(centerX - sub.centerX) <= 36 && Math.abs(centerY - sub.centerY) <= 21) { // Bomb has hit the submarine. The submarine // enters the "isExploding" state. sub.isExploding = true; sub.explosionFrameNumber = 1; isFalling = false; // Bomb reappears on the boat. hits++; // The number of hits goes up by one. } else { // If the bomb has not fallen off the canvas or hit the // sub, then it is moved down 6 pixels. centerY += 6; } } } void draw(GraphicsContext g) { // Draw the bomb. if ( ! isFalling ) { // If not falling, set centerX and centerY // to show the bomb on the bottom of the boat. centerX = boat.centerX; centerY = boat.centerY + 23; } g.setFill(Color.RED); g.fillOval(centerX - 8, centerY - 8, 16, 16); } } // end nested class Bomb /** * This nested class defines the sub. */ private class Submarine { int centerX, centerY; // Current position of the center of the sub. boolean isMovingLeft; // Tells whether the sub is moving left or right boolean isExploding; // Set to true when the sub is hit by the bomb. int explosionFrameNumber; // If the sub is exploding, this is the number // of frames since the explosion started. Submarine() { // Create the sub at a random x-coordinate, 40 pixels from bottom. centerX = (int)(width*Math.random()); centerY = height - 40; isExploding = false; isMovingLeft = (Math.random() < 0.5); } void updateForNewFrame() { // Move sub or increase explosionFrameNumber. if (isExploding) { // If the sub is exploding, add 1 to explosionFrameNumber. // When the number reaches 25, the explosion ends and the // sub reappears in a random position. explosionFrameNumber++; if (explosionFrameNumber == 25) { centerX = (int)(width*Math.random()); centerY = height - 40; isExploding = false; isMovingLeft = (Math.random() < 0.5); } } else { // Move the sub. if (Math.random() < 0.02) { // In one frame out of every 50, on average, the sub // reverses its direction of motion. isMovingLeft = ! isMovingLeft; } if (isMovingLeft) { // Move the sub 3 pixels to the left. If it moves off // the left edge of the canvas, move it back to the left // edge and start it moving to the right. centerX -= 3; if (centerX <= 0) { centerX = 0; isMovingLeft = false; } } else { // Move the sub 3 pixels to the right. If it moves off // the right edge of the canvas, move it back to the right // edge and start it moving to the left. centerX += 3; if (centerX > width) { centerX = width; isMovingLeft = true; } } } } void draw(GraphicsContext g) { // Draw sub and, if it is exploding, the explosion. g.setFill(Color.BLACK); g.fillOval(centerX - 30, centerY - 15, 60, 30); if (isExploding) { // Draw an "explosion" that grows in size as the number of // frames since the start of the explosion increases. g.setFill(Color.YELLOW); g.fillOval(centerX - 3*explosionFrameNumber, centerY - 1.5*explosionFrameNumber, 6*explosionFrameNumber, 3*explosionFrameNumber); g.setFill(Color.RED); g.fillOval(centerX - 1.5*explosionFrameNumber, centerY - explosionFrameNumber/2, 3*explosionFrameNumber, explosionFrameNumber); } } } // end nested class Submarine } // end class SubKillerWithScore