/** * File: GamePanel.java * Author: Brian Borowski * Date created: August 1, 2011 * Date last modified: December 29, 2021 */ import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.Random; import javax.swing.JPanel; import javax.swing.Timer; public class GamePanel extends JPanel implements ActionListener, KeyListener, Runnable { public static final Random RANDOM_GENERATOR = new Random(); private static final long serialVersionUID = 1L; private static final int GAME_DURATION = 60, MAX_MISSILES = 15, MAX_SPACESHIPS = 3, FADE_IN = 0, FADE_OUT = 1; private final Landscape landscape; private final Sky sky; private final Launcher launcher; private final Missile[] missiles; private final Spaceship[] spaceships; private final HashSet alienBombs; private final LinkedList groundDamages; private final SpaceshipGenerator spaceshipGenerator; private final Timer timer; private final int width, height, bombGroundHeight; private final Font statsFont = new Font("Dialog", Font.BOLD, 16), titleFont = new Font("Dialog", Font.PLAIN, 56), largeFont = new Font("Dialog", Font.PLAIN, 40), smallFont = new Font("Monospaced", Font.BOLD, 32); private Graphics offscreenGraphics = null; private Image offscreenImage = null; private boolean isMovingLeft, isMovingRight, isLaunching, isPaused, isOver; private volatile boolean isRunning, isHelpMode; private Thread thread; private int score, timeRemaining, fadeType, alpha; public GamePanel(final int width, final int height) { this.width = width; this.height = height; bombGroundHeight = height - 75; isHelpMode = true; landscape = new Landscape(this); sky = new Sky(this, landscape.getHeight()); spaceships = new Spaceship[MAX_SPACESHIPS]; alienBombs = new HashSet(); groundDamages = new LinkedList(); spaceshipGenerator = new SpaceshipGenerator(this, spaceships); launcher = new Launcher( this, 300, width / 2, height - landscape.getHeight()); missiles = new Missile[MAX_MISSILES]; timer = new Timer(1000, this); setPreferredSize(new Dimension(width, height)); addKeyListener(this); setFocusable(true); } public void startGame(boolean showHelp) { if (showHelp) { isHelpMode = true; isRunning = false; } else { isHelpMode = false; isRunning = true; } isPaused = false; isOver = false; if (thread == null) { thread = new Thread(this); thread.start(); } } public void run() { if (isHelpMode) { // Waste some time until window is fully ready within OS. renderGame(); paintScreen(); try { Thread.sleep(500); } catch (final InterruptedException ie) { } } while (isHelpMode) { if (fadeType == FADE_IN) { if (alpha < 255) { ++alpha; renderGame(); paintScreen(); } } else { if (alpha > 0) { --alpha; renderGame(); paintScreen(); } else { isRunning = true; isHelpMode = false; reset(); } } try { Thread.sleep(5); } catch (final InterruptedException ie) { } } long lastTimeNano = System.nanoTime(); while (isRunning) { final long startTimeNano = System.nanoTime(); advanceGame((startTimeNano - lastTimeNano) / 1e9f); renderGame(); paintScreen(); lastTimeNano = startTimeNano; } } public void stopGame() { // Pause the generator so you can see where the spaceships were at // the instant the game ended. timer.stop(); spaceshipGenerator.pause(); isHelpMode = false; isRunning = false; try { if (thread != null) { thread.join(); } } catch (final InterruptedException ie) { } thread = null; } public void pauseGame() { if (!isHelpMode) { timer.stop(); spaceshipGenerator.pause(); launcher.pause(); isPaused = true; isMovingLeft = false; isMovingRight = false; isLaunching = false; } } public void resumeGame() { if (!isHelpMode) { isPaused = false; launcher.resume(); spaceshipGenerator.resume(); if (timeRemaining > 0) { timer.start(); } } } private void reset() { score = 0; timeRemaining = GAME_DURATION; launcher.reset(); for (int i = missiles.length - 1; i >= 0; --i) { missiles[i] = null; } alienBombs.clear(); groundDamages.clear(); timer.start(); if (thread == null) { thread = new Thread(this); thread.start(); } spaceshipGenerator.start(); } private void paintScreen() { // Actively render the buffer image to the screen. Graphics g; try { // Get the panel's graphic context. g = this.getGraphics(); if (g != null && offscreenImage != null) { g.drawImage(offscreenImage, 0, 0, null); } g.dispose(); } catch (final Exception e) { } } private void renderGame() { // Draw the current frame to an image buffer. if (offscreenImage == null) { offscreenImage = createImage(width, height); if (offscreenImage == null) { return; } else { offscreenGraphics = offscreenImage.getGraphics(); } } if (isHelpMode) { displayHelp(offscreenGraphics); } else { paintItems(offscreenGraphics); } } private void paintItems(final Graphics g) { Graphics2D g2d = (Graphics2D)g; // Draw background. landscape.paintComponent(g2d); sky.paintComponent(g2d); for (Iterator iter = groundDamages.iterator(); iter.hasNext();) { GroundDamage groundDamage = iter.next(); groundDamage.paintComponent(g2d); } // Draw alien bombs before the spaceships // (to be sure the bomb does not appear in front of the spaceship). for (AlienMissile alienBomb : alienBombs) { alienBomb.paintComponent(g2d); } // Draw spaceships. for (int i = spaceships.length - 1; i >= 0; --i) { if (spaceships[i] != null) { spaceships[i].paintComponent(g2d); } } // Draw missiles. for (int i = missiles.length - 1; i >= 0; --i) { if (missiles[i] != null) { missiles[i].paintComponent(g2d); } } // Draw launcher after missiles // (to make sure missile does not appear in front of launcher). launcher.paintComponent(g2d); g2d.setColor(Color.white); g2d.setFont(statsFont); g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); // Draw score. g2d.drawString("Score: " + score, 5, 20); // Draw remaining time g2d.drawString("Time remaining: " + timeRemaining, 474, 20); if (isOver) { g2d.setColor(new Color(0, 0, 0, 64)); g2d.fillRect(0, 0, width, height); // Draw game over message. g2d.setColor(Color.yellow); g2d.setFont(largeFont); String str = "GAME OVER"; int strWidth = g.getFontMetrics().charsWidth(str.toCharArray(), 0, str.length()), strHeight = g.getFontMetrics().getHeight(); g2d.drawString(str, width / 2 - strWidth / 2, height / 2 - strHeight / 2); g2d.setColor(Color.white); g2d.setFont(statsFont); str = "Press S to start a new game."; strWidth = g.getFontMetrics().charsWidth(str.toCharArray(), 0, str.length()); g2d.drawString(str, width / 2 - strWidth / 2, height / 2 - strHeight / 2 + 30); } else if (isPaused) { g2d.setColor(new Color(0, 0, 0, 64)); g2d.fillRect(0, 0, width, height); // Draw paused message. g2d.setColor(Color.gray); g2d.setFont(largeFont); final String str = "PAUSED"; int strWidth = g.getFontMetrics().charsWidth(str.toCharArray(), 0, str.length()), strHeight = g.getFontMetrics().getHeight(); g2d.drawString(str, width / 2 - strWidth / 2, height / 2 - strHeight / 2); } g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); } public void actionPerformed(final ActionEvent ae) { --timeRemaining; if (timeRemaining <= 0) { isOver = true; stopGame(); renderGame(); paintScreen(); } } private void updateLauncher(final float elapsedSeconds) { if ( !(isMovingLeft && isMovingRight) ) { if (isMovingLeft) { launcher.move(Launcher.LEFT, elapsedSeconds); } else if (isMovingRight) { launcher.move(Launcher.RIGHT, elapsedSeconds); } } } private void updateSpaceships(final float elapsedSeconds) { spaceshipGenerator.update(elapsedSeconds); for (int i = spaceships.length - 1; i >= 0; --i) { final Spaceship spaceship = spaceships[i]; if (spaceship != null) { if (!spaceship.isVisible()) { score -= spaceship.getPointsLost(); } if (!spaceship.isVisible() || spaceship.isDisposable()) { spaceships[i] = null; } } } } private void launchMissiles() { if (isLaunching) { for (int i = missiles.length - 1; i >= 0; --i) { if (missiles[i] == null) { final Missile missile = launcher.generateMissile(); if (missile != null) { missiles[i] = missile; } break; } } } } private void advanceGame(final float elapsedSeconds) { if (isPaused) { return; } updateLauncher(elapsedSeconds); updateSpaceships(elapsedSeconds); // Accumulate all bombs. for (Spaceship spaceship : spaceships) { if (spaceship != null) { AlienMissile alienBomb = spaceship.getAlienBomb(); if (alienBomb != null) { alienBombs.add(alienBomb); } } } final Rectangle launcherRect = launcher.getBoundingRectangle(); // Move bombs down, or convert them to ground damage objects. for (Iterator iter = alienBombs.iterator(); iter.hasNext();) { AlienMissile alienBomb = iter.next(); if (alienBomb.getBoundingRectangle().intersects(launcherRect)) { alienBomb.setHitGround(true); iter.remove(); launcher.setHit(); } else if (alienBomb.getYPos() < bombGroundHeight) { alienBomb.moveDown(elapsedSeconds); } else if (!alienBomb.getHitGround()) { alienBomb.setHitGround(true); iter.remove(); groundDamages.add(new GroundDamage(alienBomb.getXPos(), bombGroundHeight)); } } // Move the missiles. launchMissiles(); for (int i = missiles.length - 1; i >= 0; --i) { final Missile missile = missiles[i]; if (missile == null) { continue; } missile.moveUp(elapsedSeconds); // Test if missile falls out of view. if (missile.getYPos() < 0) { // Remove missile from array. missiles[i] = null; } // Test for collisions with spaceships. for (Spaceship spaceship : spaceships) { if (spaceship != null && !spaceship.isHit()) { final Rectangle sRect = spaceship.getBoundingRectangle(); if (missile != null) { final Rectangle mRect = missile.getBoundingRectangle(); if (sRect.intersects(mRect)) { score += spaceship.getPointsEarned(); spaceship.setHit(); missiles[i] = null; } } } } } } public void keyPressed(final KeyEvent keyEvent) { switch (keyEvent.getKeyCode()) { case KeyEvent.VK_LEFT: isMovingLeft = true; break; case KeyEvent.VK_RIGHT: isMovingRight = true; break; case KeyEvent.VK_SPACE: if (!isHelpMode) { isLaunching = true; } else { fadeType = FADE_OUT; } break; case KeyEvent.VK_P: if (isRunning) { if (isPaused) { resumeGame(); } else { pauseGame(); } } break; case KeyEvent.VK_S: if (!isRunning) { startGame(false); reset(); } break; default: break; } } public void keyReleased(final KeyEvent keyEvent) { switch (keyEvent.getKeyCode()) { case KeyEvent.VK_LEFT: isMovingLeft = false; break; case KeyEvent.VK_RIGHT: isMovingRight = false; break; case KeyEvent.VK_SPACE: isLaunching = false; break; default: break; } } public void keyTyped(final KeyEvent keyEvent) { } protected void paintComponent(final Graphics g) { super.paintComponent(g); if (offscreenImage != null) { g.drawImage(offscreenImage, 0, 0, null); } } private void displayHelp(final Graphics g) { Graphics2D g2d = (Graphics2D)g; g2d.setColor(Color.BLACK); g2d.fillRect(0, 0, width, height); g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g2d.setFont(titleFont); g2d.setColor(new Color(255, 128, 0, alpha)); String str = "Space Warz"; int strWidth = g2d.getFontMetrics().charsWidth(str.toCharArray(), 0, str.length()); g2d.drawString(str, width / 2 - strWidth / 2, height / 6); g2d.setFont(largeFont); g2d.setColor(new Color(196, 196, 196, alpha)); str = "Instructions"; strWidth = g2d.getFontMetrics().charsWidth(str.toCharArray(), 0, str.length()); g2d.drawString(str, width / 2 - strWidth / 2, height / 4 + 25); g2d.setFont(smallFont); g2d.setColor(new Color(128, 128, 128, alpha)); str = "Fire:"; g2d.drawString(str, width / 8, height / 4 + 75); str = "SPACE"; strWidth = g2d.getFontMetrics().charsWidth(str.toCharArray(), 0, str.length()); g2d.drawString(str, 7 * width / 8 - strWidth, height / 4 + 75); str = "Move Right:"; g2d.drawString(str, width / 8, height / 4 + 110); str = "RIGHT ARROW"; strWidth = g2d.getFontMetrics().charsWidth(str.toCharArray(), 0, str.length()); g2d.drawString(str, 7 * width / 8 - strWidth, height / 4 + 110); str = "Move Left:"; g2d.drawString(str, width / 8, height / 4 + 145); str = "LEFT ARROW"; strWidth = g2d.getFontMetrics().charsWidth(str.toCharArray(), 0, str.length()); g2d.drawString(str, 7 * width / 8 - strWidth, height / 4 + 145); str = "Pause/Resume:"; g2d.drawString(str, width / 8, height / 4 + 180); str = "P"; strWidth = g2d.getFontMetrics().charsWidth(str.toCharArray(), 0, str.length()); g2d.drawString(str, 7 * width / 8 - strWidth, height / 4 + 180); str = "Hit:"; g2d.drawString(str, width / 8, height / 4 + 215); str = "FAST +20, SLOW +10"; strWidth = g2d.getFontMetrics().charsWidth(str.toCharArray(), 0, str.length()); g2d.drawString(str, 7 * width / 8 - strWidth, height / 4 + 215); str = "Miss:"; g2d.drawString(str, width / 8, height / 4 + 250); str = "FAST -10, SLOW -20"; strWidth = g2d.getFontMetrics().charsWidth(str.toCharArray(), 0, str.length()); g2d.drawString(str, 7 * width / 8 - strWidth, height / 4 + 250); g2d.setFont(largeFont); g2d.setColor(new Color(196, 196, 196, alpha)); str = "Press SPACE to start!"; strWidth = g2d.getFontMetrics().charsWidth(str.toCharArray(), 0, str.length()); g2d.drawString(str, width / 2 - strWidth / 2, height / 4 + 310); g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); } }