import javax.swing.*; import java.awt.*; import java.util.List; /** * This is the main canvas for the Lunar Lander game. It is responsible for * drawing the world and zooming/panning as necessary. * * @author Michael Yu */ public class GameCanvas extends JPanel { /** * Construct a new game canvas object */ public GameCanvas(boolean trace) { this.trace = trace; } /** * Get x coordinate of the lower left corner of the viewing window * * @return the x coordinate of the lower left corner of the viewing window */ public double getLLX() { return llx; } /** * Get y coordinate of the lower left corner of the viewing window * * @return the y coordinate of the lower left corner of the viewing window */ public double getLLY() { return lly; } /** * Get x coordinate of the upper right corner of the viewing window * * @return the x coordinate of the upper right corner of the viewing window */ public double getURX() { return urx; } /** * Get Y coordinate of the upper right corner of the viewing window * * @return the y coordinate of the upper right corner of the viewing window */ public double getURY() { return ury; } /** * Set viewing window to the 'zoomed out' state */ public void zoomOut() { double posX = LunarLanderGame.lander.getPosition().getX(); llx = posX - .5 * LunarLanderGame.moon.getWorldWidth(); lly = 0; urx = posX + .5 * LunarLanderGame.moon.getWorldWidth(); ury = LunarLanderGame.moon.getWorldHeight(); zoomed = false; } /** * Set viewing window to the 'zoomed in' state */ public void zoomIn() { double width = LunarLanderGame.moon.getWorldWidth() / 3; double height = LunarLanderGame.moon.getWorldHeight() / 3; llx = LunarLanderGame.lander.getPosition().getX() - width / 2; urx = llx + width; lly = LunarLanderGame.lander.getPosition().getY() - 2 * height / 3; ury = lly + height; zoomed = true; } /** * Update the viewing window */ public void updateViewingWindow() { LunarLander lander = LunarLanderGame.lander; double posX = lander.getPosition().getX(); double posY = lander.getPosition().getY(); Moon moon = LunarLanderGame.moon; TerrainSegment segment = moon.getEncompassingTerrainSegment(posX); // Zoom in or out if necessary if (!zoomed && posY - segment.getLeftEndPoint().getY() < 170) { zoomIn(); } else if (zoomed && posY - segment.getLeftEndPoint().getY() > 250) { zoomOut(); } // Calculate margin double windowWidth = urx - llx; double windowHeight = ury - lly; double margin = .3 * Math.min(windowWidth, windowHeight); // Wrap window location if lander location was wrapped if (posX < llx) { llx -= LunarLanderGame.moon.getWorldWidth(); urx -= LunarLanderGame.moon.getWorldWidth(); } // Wrap window location if lander location was wrapped if (posX > urx) { llx += LunarLanderGame.moon.getWorldWidth(); urx += LunarLanderGame.moon.getWorldWidth(); } // Pan right if (posX - llx > windowWidth - margin) { llx = posX - windowWidth + margin; urx = llx + windowWidth; } // Pan left if (posX - llx < margin) { llx = posX - margin; urx = llx + windowWidth; } // Pan up if (posY - lly > windowHeight - margin) { lly = posY - windowHeight + margin; ury = lly + windowHeight; } // Pan down if (posY - lly < margin) { lly = posY - margin; ury = lly + windowHeight; } } /** * Used to implement double-buffering * * @param g is the graphics context */ public void update(Graphics g) { Graphics offScreenGraphics; // Check if dimensions have changed or buffer not created if (offScreenBuffer == null || (!(offScreenBuffer.getWidth(this) == this.getSize().width && offScreenBuffer.getHeight(this) == this.getSize() .height))) { offScreenBuffer = this.createImage(getSize().width, getSize().height); } offScreenGraphics = offScreenBuffer.getGraphics(); // Paint onto offscreen buffer paint(offScreenGraphics); // Now transfer to front g.drawImage(offScreenBuffer, 0, 0, this); } /** * Paint the canvas * * @param g is the graphics context */ public void paint(Graphics g) { ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); super.paintComponent(g); setBackground(Color.black); switch (mode) { case TITLE_SCREEN: paintTitleScreen(g); break; case IN_GAME: paintInGame(g); break; } } /** * Paint the title screen * * @param g is the graphics context */ public void paintTitleScreen(Graphics g) { g.setColor(Color.white); Font f = new Font("Courier", Font.BOLD, Math.min(36, getHeight())); g.setFont(f); int strWidth = g.getFontMetrics().stringWidth("Lunar Lander"); g.drawString("Lunar Lander", (getWidth() - strWidth) / 2, (getHeight() + f.getSize()) / 2); } /** * Paint the screen when in game * * @param g is the graphics context */ public void paintInGame(Graphics g) { updateViewingWindow(); LunarLander lander = LunarLanderGame.lander; // Draw lander positions trace if (trace) { drawPreviousLanderPositions(g); } // Draw moon terrain LunarLanderGame.moon.draw(g); // Draw any display messages if (displayMessage != null) { g.setColor(Color.white); Font f = new Font("Courier", Font.BOLD, Math.min(36, getHeight())); g.setFont(f); int strWidth = g.getFontMetrics().stringWidth(displayMessage); g.drawString(displayMessage, (getWidth() - strWidth) / 2, (getHeight() + f.getSize()) / 2); } // Draw lunar lander lander.draw(g); // Display velocities, altitude, etc. drawDashboard(g); // draw coordinates drawCoordinates(g); } /** * Draws the X coordinates of the world on the viewing window * @param g the graphics context */ protected void drawCoordinates(Graphics g) { int interval = zoomed ? 20 : 50; double worldWidth = LunarLanderGame.moon.getWorldWidth(); double canvasWidth = getWidth(); Font f = new Font("Courier", Font.PLAIN, Math.min(12, getHeight())); g.setFont(f); for(int i=0; i<(int)LunarLanderGame.WORLD_WIDTH; i+=interval) { // DO MIKE VOODOO int x1 = (int) ((double) canvasWidth * ((i - llx) / (urx - llx))); int x2 = (int) ((double) canvasWidth * ((i - worldWidth - llx) / (urx - llx))); int x3 = (int) ((double) canvasWidth * ((i + worldWidth - llx) / (urx - llx))); int strWidth = g.getFontMetrics().stringWidth("" + i); g.setColor(Color.white); g.drawString("" + i, x1 - strWidth / 2, getHeight() - 20 + f.getSize() + 3); g.drawString("" + i, x2 - strWidth / 2, getHeight() - 20 + f.getSize() + 3); g.drawString("" + i, x3 - strWidth / 2, getHeight() - 20 + f.getSize() + 3); } } /** * Display dots at the previous lander positions * * @param g is the graphics context */ protected void drawPreviousLanderPositions(Graphics g) { g.setColor(Color.gray); List previousPositions = LunarLanderGame.recorder.getPositions(); List times = LunarLanderGame.recorder.getTimes(); double t = 0; for (int i = 0; i < times.size(); i++) { Double time = (Double) times.get(i); if (time.doubleValue() >= t) { t += DOT_TRACE_TIME_INTERVAL; Vect2D position = (Vect2D) previousPositions.get(i); double posX = position.getX(); double posY = position.getY(); // Wrap positions if (posX < llx) { posX += LunarLanderGame.moon.getWorldWidth(); } else if (posX > urx) { posX -= LunarLanderGame.moon.getWorldWidth(); } int screenX = (int) ((double) this.getWidth() * (posX - llx) / (urx - llx)); int screenY = (int) ((double) this.getHeight() * (1.0 - (posY - lly) / (ury - lly))); g.drawOval(screenX, screenY, 1, 1); } } } /** * Display the current velocities, altitude, etc. * * @param g is the graphics context */ public void drawDashboard(Graphics g) { LunarLander lander = LunarLanderGame.lander; Graphics2D g2 = (Graphics2D) g; java.awt.font.FontRenderContext frc = g2.getFontRenderContext(); Font f = new Font("Courier", Font.PLAIN, 15); g.setFont(f); int canvasWidth = this.getWidth(); int fontHeight = (int) f.getStringBounds("0", frc).getHeight(); int fontWidth = (int) f.getStringBounds(" ", frc).getWidth(); int colWidth1 = 7 * fontWidth; int colWidth2 = 8 * fontWidth; int colWidth3 = 100; int colWidth4 = 21 * fontWidth; int colWidth5 = 8 * fontWidth; int dashboardWidth = colWidth1 + colWidth2 + colWidth3 + colWidth4 + colWidth5; int ulx = (canvasWidth - dashboardWidth) / 2; int topMargin = 20; // Turn of anti-aliasing Object aaStatus = g2.getRenderingHint(RenderingHints.KEY_ANTIALIASING); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); // Draw Time String time = "" + (int) LunarLanderGame.time; g.drawString("TIME", ulx, topMargin + fontHeight); g.drawString(time, ulx + colWidth1 + colWidth2 - time.length() * fontWidth, topMargin + fontHeight); // Draw Fuel String fuel = "" + round(lander.getFuel(), 1); g.drawString("FUEL", ulx, topMargin + 2 * fontHeight); g.drawString(fuel, ulx + colWidth1 + colWidth2 - fuel.length() * fontWidth, topMargin + 2 * fontHeight); // Draw Score String score = "" + LunarLanderGame.score; g.drawString("SCORE", ulx, topMargin + 3 * fontHeight); g.drawString(score, ulx + colWidth1 + colWidth2 - score.length() * fontWidth, topMargin + 3 * fontHeight); // Draw Altitude String altitude = "" + round(lander.getPosition().getY(), 1); g.drawString("ALTITUDE", ulx + colWidth1 + colWidth2 + colWidth3, topMargin + fontHeight); g.drawString(altitude, ulx + dashboardWidth - altitude.length() * fontWidth, topMargin + fontHeight); // Draw Horizontal Velocity double roundedHV = round(lander.getVelocity().getX(), 1); String hv = Math.abs(roundedHV) + " "; if (lander.getVelocity().getX() > 0) { hv += "\u2192"; // Up arrow } else if (lander.getVelocity().getX() < 0) { hv += "\u2190"; // Down arrow } else { hv += " "; } g.drawString("HORIZONTAL VELOCITY", ulx + colWidth1 + colWidth2 + colWidth3, topMargin + 2 * fontHeight); g.drawString(hv, ulx + dashboardWidth + (2 * fontWidth) - hv.length() * fontWidth, topMargin + 2 * fontHeight); // Draw Vertical Velocity double roundedVV = round(lander.getVelocity().getY(), 1); String vv = Math.abs(roundedVV) + " "; if (lander.getVelocity().getY() > 0) { vv += "\u2191"; // Right arrow } else if (lander.getVelocity().getY() < 0) { vv += "\u2193"; // Left arrow } else { vv += " "; } g.drawString("VERTICAL VELOCITY", ulx + colWidth1 + colWidth2 + colWidth3, topMargin + 3 * fontHeight); g.drawString(vv, ulx + dashboardWidth + (2 * fontWidth) - vv.length() * fontWidth, topMargin + 3 * fontHeight); // Restore anti-aliasing settings g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, aaStatus); } /** * Set the display mode * * @param mode is the desired mode */ public void setMode(int mode) { this.mode = mode; if (mode == IN_GAME) { zoomOut(); } } /** * Set the in-game display message * * @param message is the message to be displayed in game */ public void setDisplayMessage(String message) { displayMessage = message; } /** * Round a floating point number to the given number of decimal places */ private static double round(double val, int precision) { val = Math.floor(val * Math.pow(10, precision) + 0.5); return val / Math.pow(10, precision); } // General constants private static final double DOT_TRACE_TIME_INTERVAL = .35; // Time interval between drawing dots // Mode constants public static final int TITLE_SCREEN = 0; public static final int IN_GAME = 1; // Instance variables private double llx; // Lower-left x coordinate of viewing window private double lly; // Lower-left y coordinate of viewing window private double urx; // Upper-right x coordinate of viewing window private double ury; // Upper-right y coordinate of viewing window private boolean zoomed; // Is the viewing window zoomed in? private boolean trace = false; // Trace the flight path? private int mode; // Current mode of the game (title screen or in game) private String displayMessage; // The message to display private Image offScreenBuffer; // Used for double buffering }