// LunarLander.java, version 1.10, July 7, 2008. // Applet to demonstrate numerical integration in the simulation of a lunar lander. // // Copyright 2000-2008 by Rick Wagner, all rights reserved. // // Created for use in the USC computer science course CS102. Use of this code // is authorized for educational purposes only with proper attribution. Any // derivative must carry this notice. No use without attribution. import java.applet.*; import java.awt.*; public class LunarLander extends Applet implements Runnable { // Instance variables (private where possible) private String sVerNum = "1.10"; // Only constructors can run here. private Dimension dPanel; // The applet panel size. private Image imOffScreen = null; // Offscreen image for double buffering. private Graphics grOffScreen = null; // Offscreen graphics for double buffering. private Lander lunarLander; // A single Lander instance. private Thread tLander = null; // Thread for applet animation (repainting). private int iSleepInterval = 50; // Thread sleep interval. private float t = (float) 0.5; // Time slice for integration. private float throttle = (float) 0.5; // 0.6856 for constant-throttle soft landing. private Button startButton; private Button resetButton; private float gsfTotalTime = 0; // Realtime accumulator. // To allow browsers to get information about the applet: public String getAppletInfo() { return "LunarLander applet, version " + sVerNum + ", a lunar lander simulator program,\n" + "by Rick Wagner, copyright 2000-2008, all rights reserved."; } // Initialize the applet (primarily for building the GUI) public void init() { setBackground(Color.lightGray); dPanel = this.size(); // The applet panel size (set in html code) // Create new Lander object (defined as a class below): lunarLander = new Lander(); lunarLander.setThrottle(throttle); System.out.println(getAppletInfo()); startButton = new Button("Start"); startButton.setForeground(Color.black); startButton.setBackground(Color.lightGray); startButton.enable(); resetButton = new Button("Reset"); resetButton.setForeground(Color.black); resetButton.setBackground(Color.lightGray); resetButton.disable(); this.add(startButton); this.add(resetButton); } public void run() { float v = 0; long lStartTime = System.currentTimeMillis(); // Get the current time in milliseconds. gsfTotalTime = 0; if (throttle > 0) lunarLander.setFlame(true); this.requestFocus(); // So we can get keyboard input. while (lunarLander.getAltitude() > 0) { try { tLander.sleep(iSleepInterval); // sleep() throws an exception. } catch(InterruptedException e1) { } v = lunarLander.getSpeed(); gsfTotalTime = ((float) (System.currentTimeMillis() - lStartTime)) / 1000; lunarLander.update(t); repaint(); } lunarLander.setFlame(false); repaint(); // Repaint the applet frame. resetButton.enable(); System.out.println("v = " + v); if (-v > 2.5) { this.showStatus("Crash! Velocity = " + Math.round(v) + " m/s."); } else { this.showStatus("Nice landing! Velocity = " + Math.round(v) + " m/s."); } if (tLander != null) tLander.stop(); tLander = null; } public void start() { this.requestFocus(); // So we can get keyboard input this.showStatus("Up and down arrow keys to adjust throttle."); } // The applet runtime interpreter passes g to this applet frame painting function public void paint(Graphics g) { g.clearRect(0, 0, dPanel.width, dPanel.height); // Necessary with double buffering. this.setBackground(Color.lightGray); g.drawString("Realtime: " + Float.toString(gsfTotalTime) + " seconds", 4, 51); // Ask the lander to paint itself: lunarLander.paint(g); // Surface of the moon: g.setColor(Color.gray); g.fillRect(1, dPanel.height - dPanel.height / 13, dPanel.width - 2, dPanel.height - 2); // Raised border for the applet: g.setColor(Color.white); g.drawLine(0, 0, dPanel.width - 1, 0); g.drawLine(0, 0, 0, dPanel.height); g.setColor(Color.black); g.drawLine(0, dPanel.height - 1, dPanel.width - 1, dPanel.height - 1); g.drawLine(dPanel.width - 1, 0, dPanel.width - 1, dPanel.height - 1); } // Implements double buffering public void update(Graphics g) { if (imOffScreen == null) { // Make sure the offscreen and graphics exist imOffScreen = this.createImage(dPanel.width, dPanel.height); grOffScreen = imOffScreen.getGraphics(); grOffScreen.clearRect(0, 0, dPanel.width, dPanel.height); } this.paint(grOffScreen); g.drawImage(imOffScreen, 0, 0, null); } public boolean mouseDown(Event e, int x, int y) // For mouse events { this.requestFocus(); // So we can get keyboard input return true; } public boolean keyDown(Event e, int k) // For keyboard events { //System.out.println("KeyDown " + k + "\n"); switch (k) { case 'a': // Autopilot on; { lunarLander.setAutoPilot(true); lunarLander.setThrottle(0); this.showStatus("Autopilot on"); break; } case 1004: // Up arrow (increase throttle) { if (lunarLander.getAutoPilot()) { this.showStatus("On autopilot"); } else { throttle += (float) 0.05; if (throttle > 1) throttle = 1; lunarLander.setThrottle(throttle); if (lunarLander.getAltitude() > 0) { this.showStatus("Throttle increased to " + throttle); } } break; } case 1005: // Down arrow (decrease throttle) { if (lunarLander.getAutoPilot()) { this.showStatus("On autopilot"); } else { throttle -= (float) 0.05; if (throttle < 0) throttle = 0; lunarLander.setThrottle(throttle); if (lunarLander.getAltitude() > 0) { this.showStatus("Throttle decreased to " + throttle); } } break; } case 32: // Space bar; { if (startButton.isEnabled()) { startButtonAction(); } else { if (resetButton.isEnabled()) { resetButtonAction(); } } break; } } return true; } public boolean action(Event e, Object o) { if (e.target == startButton) { startButtonAction(); } if (e.target == resetButton) { resetButtonAction(); this.requestFocus(); // So we can get keyboard input } return false; } public void startButtonAction() { tLander = new Thread(this); tLander.start(); startButton.disable(); } public void resetButtonAction() { resetButton.disable(); startButton.enable(); tLander = null; lunarLander = null; lunarLander = new Lander(); throttle = (float) 0.5; lunarLander.setThrottle(throttle); this.showStatus("Throttle reset to " + throttle); lunarLander.setFlame(false); gsfTotalTime = 0; repaint(); } } // End of applet LunarLander class // Class for the lunar lander: class Lander { private float gsfGravity = (float) 1.62; private Color cBodyColor = Color.black; private Color cBodyFillColor = Color.white; private Color cFlameColor = Color.yellow; private float _sfThrottle; // Ranges from 0 to 1.0, default 0. private float _sfVerticalSpeed; // Default 0 meters per second. private float _sfAltitude; // Default 1000 meters. private float _sfFuelMass; // Default 1700 kg. private float _sfLanderMass; // Default 900 private float _sfMaxBurnRate; // Default 10 kg per second. private float _sfMaxThrust; // Default 5000 newtons. private int iNumPoints = 0; private int iNumFlamePoints = 0; private int bodyX[]; private int bodyY[]; private int flameX[]; private int flameY[]; private int tempBodyY[]; private int tempFlameY[]; private Polygon body; private Polygon flame; private boolean _bFlame; private boolean _bAutoPilot; private float _sfTotalTime; // Default constructor: Lander() { int i = 0; // Loop index. _sfThrottle = 0; // Ranges from 0 to 1.0, default 0. _sfVerticalSpeed = 0; // Default 0 meters per second. _sfAltitude = 1000; // Default 1000 meters. _sfFuelMass = 1700; // Default 1700 kg. _sfLanderMass = 900; // Default 900 kg. _sfMaxBurnRate = 10; // Default 10 kg per second. _sfMaxThrust = 5000; // Default 5000 newtons. _bFlame = false; // Flag for flame state. _bAutoPilot = false; // Flag for autopilot state. _sfTotalTime = 0; // Landing time accumulator. iNumPoints = 28; // Number of points in the lander body polygon. iNumFlamePoints = 7; // Number of points in the rocket flame. bodyX = new int[iNumPoints]; // Arrays for the polygon constructor. bodyY = new int[iNumPoints]; tempBodyY = new int[iNumPoints]; // Only the Y dimension changes in this simulation. // Here we define the shape of the lander body polygon: bodyX[0] = 4; bodyY[0] = -2; bodyX[1] = 2; bodyY[1] = -8; bodyX[2] = 6; bodyY[2] = -8; bodyX[3] = 10; bodyY[3] = -1; bodyX[4] = 8; bodyY[4] = -1; bodyX[5] = 8; bodyY[5] = 0; bodyX[6] = 14; bodyY[6] = 0; bodyX[7] = 14; bodyY[7] = -1; bodyX[8] = 12; bodyY[8] = -1; bodyX[9] = 8; bodyY[9] = -8; bodyX[10] = 10; bodyY[10] = -8; bodyX[11] = 10; bodyY[11] = -18; bodyX[12] = 6; bodyY[12] = -18; bodyX[13] = 4; bodyY[13] = -24; bodyX[14] = -4; bodyY[14] = -24; bodyX[15] = -6; bodyY[15] = -18; bodyX[16] = -10; bodyY[16] = -18; bodyX[17] = -10; bodyY[17] = -8; bodyX[18] = -8; bodyY[18] = -8; bodyX[19] = -12; bodyY[19] = -1; bodyX[20] = -14; bodyY[20] = -1; bodyX[21] = -14; bodyY[21] = 0; bodyX[22] = -8; bodyY[22] = 0; bodyX[23] = -8; bodyY[23] = -1; bodyX[24] = -10; bodyY[24] = -1; bodyX[25] = -6; bodyY[25] = -8; bodyX[26] = -2; bodyY[26] = -8; bodyX[27] = -4; bodyY[27] = -2; // Now we scale the polygon to the size we want: for (i = 0; i < iNumPoints; i++) { bodyX[i] = 2 * bodyX[i] + 100; bodyY[i] = 2 * bodyY[i] + 100; } body = new Polygon(bodyX, bodyY, iNumPoints); // Polygon object constructor flameX = new int[iNumFlamePoints]; flameY = new int[iNumFlamePoints]; tempFlameY = new int[iNumFlamePoints]; // Here we define the shape of the flame: flameX[0] = 0; flameY[0] = 12; flameX[1] = 2; flameY[1] = 8; flameX[2] = 4; flameY[2] = 2; flameX[3] = 4; flameY[3] = -2; flameX[4] = -4; flameY[4] = -2; flameX[5] = -4; flameY[5] = 2; flameX[6] = -2; flameY[6] = 8; // Now we scale the polygon to the size we want: for (i = 0; i < iNumFlamePoints; i++) { flameX[i] = 2 * flameX[i] + 100; flameY[i] = 2 * flameY[i] + 100; } flame = new Polygon(flameX, flameY, iNumFlamePoints); // Polygon object constructor } // End default constructor. float getSpeed() // Accessor. { return _sfVerticalSpeed; } float getAltitude() // Accessor. { return _sfAltitude; } float getFuelMass() // Accessor. { return _sfFuelMass; } boolean getAutoPilot() // Accessor. { return _bAutoPilot; } void setThrottle(float setting) // Mutator. { _sfThrottle = setting; } void setFlame(boolean b) // Mutator. { _bFlame = b; } void setAutoPilot(boolean b) // Mutator. { _bAutoPilot = b; } void update(float t) // Mutator. { // Update the lander's state after the passage of t seconds: int i = 0; int iTempFlameY = 0; // Intermediate variable declarations for code clarity: float sfDeltaV = 0; // Velocity change. float sfDeltaY = 0; // Altitude change. float sfDeltaM = 0; // Fuel mass change. float sfForce = 0; // Current thrust. float sfMass = 0; // Total mass of the lander; // Autopilot feature: float sfThrottleAdjustment = (float) 0.2; // Fuel flow rate: if (_sfThrottle > 0 && _sfFuelMass <= 0) _sfThrottle = 0; // Update the accumulated time for this landing: _sfTotalTime += t; // Velocity change: sfForce = _sfThrottle * _sfMaxThrust; sfMass = _sfLanderMass + _sfFuelMass; if (sfMass == 0) // Defensive programming. { System.out.println("Houston, we have a problem: can't divide by zero."); } else { sfDeltaV = t * ((sfForce / sfMass) - gsfGravity); // Extra parentheses for clarity. } _sfVerticalSpeed += sfDeltaV; // Altitude change: sfDeltaY = t * _sfVerticalSpeed; // Negative speed is downward. _sfAltitude += sfDeltaY; // Fuel mass change: sfDeltaM = -t * _sfThrottle * _sfMaxBurnRate; // Fuel mass can only decrease. _sfFuelMass += sfDeltaM; // Further adjustments: if (_sfAltitude < 0) { _sfAltitude = 0; _sfVerticalSpeed = 0; // Landed. } if (_sfFuelMass < 0) { _sfFuelMass = 0; // Out of fuel; _bFlame = false; } else { if (_sfAltitude > 0) { if (_sfThrottle > 0) { _bFlame = true; } } } // Autopilot feature: if (_bAutoPilot) { if (_sfAltitude > 0) { if (_sfVerticalSpeed < -0.7 * Math.sqrt(_sfAltitude)) // Lucky hack. { _sfThrottle += sfThrottleAdjustment; if (_sfThrottle > 1) _sfThrottle = 1; } else { _sfThrottle -= sfThrottleAdjustment; // No deadband. Always adjusting. if (_sfThrottle < 0) _sfThrottle = 0; } } } // Move the lander according to altitude: for (i = 0; i < iNumPoints; i++) { tempBodyY[i] = bodyY[i] + (1000 - (int) _sfAltitude) / 2; } body = null; body = new Polygon(bodyX, tempBodyY, iNumPoints); // Polygon object constructor for (i = 0; i < iNumFlamePoints; i++) { iTempFlameY = flameY[i] - 98; iTempFlameY *= 4.0 * _sfThrottle; iTempFlameY += 98 + 4.0 * _sfThrottle; tempFlameY[i] = iTempFlameY + (1000 - (int) _sfAltitude) / 2; } flame = null; flame = new Polygon(flameX, tempFlameY, iNumFlamePoints); // Polygon object constructor } // End of update() function. // The lander paint() function is called in the applet paint() function: public void paint(Graphics g) { g.drawString("Simtime: " + Float.toString(_sfTotalTime) + " seconds", 4, 40); if (_bFlame && _sfThrottle > 0) { g.setColor(cFlameColor); g.fillPolygon(flame); } g.setColor(cBodyFillColor); g.fillPolygon(body); g.setColor(cBodyColor); g.drawPolygon(body); } } // End of Lander class // Version History // // 1.08 October 30, 2001 Fixed the "zero-throttle start flame off bug" reported by "Captain" // Hannes Mayer. // 1.09 October 31, 2001 Added the feature for keyboard (space bar) control of the buttons for // total "hands off the mouse" operation. // 1.10 July 7, 2008 Adds finer granularity in the length of the flame graphic.