// RoboGame.java, version 1.13, first created July 25, 2006, last edited July 22, 2009. // Applet for robotic simulation. // Copyright 2006-2009 by Rick Wagner, all rights reserved. // No use without attribution. import java.applet.*; import java.awt.*; // This is an educational example of object oriented design for a robot group simulation applet. // Use of this source code is authorized for educational purposes only. No use without // proper attribution to Rick Wagner (Richard.J.Wagner@gmail.com). // 637 lines of code (including comments and white space). Compiled with the Sun J2SE (JDK 1.6). // The prefix naming convention used here is a modified Hungarian notation. // "s" is string, "sf" is single precision floating point, "i" is integer, "b" is boolean, // "d" is dimension, and "c" is color. // Uses the "over and under" curley braces style to make it easy to match open and close braces. // Uses single-precision floating point arithmetic to maximize performance. Double precision should // be reserved for only those applications requiring it (it cuts computation speed in half). Unfortunately, // the Java language authors chose to use double precision as their math library default. // This Java source code is for educational purposes only. Viewing or downloading the source implies your // acceptance of the restrictions: // 1. Use the source code for educational purposes only. // 2. Give appropriate attribution in all executables and listings derived from this source. // 3. Reproduce these restrictions and conditions in any derived programs. // Applet class definition, must be public because the brower creates an instance: public class RoboGame extends Applet // A public class must be named the same as the file { // that defines it // Applet instance variables: private String sVerNum = "1.13"; // Only constructors can run here ("" is a constructor) private String sCompileDate = "July 22, 2009"; private Dimension dApplet = null; // For the applet panel size (read from the html file) private Image imOffScreen = null; // Offscreen image for double buffering private Graphics grOffScreen = null; // Offscreen graphics for double buffering private Timer AnimationTimer = null; // Timer for control of the animation rate private int iTimerInterval = 0; // Timer interval in milliseconds private Label lblMessage = null; // To inform the user of applet state private final int iNumRobots = 17; // Set the number of robots here private Robot robotArray[] = new Robot[iNumRobots]; // Array of Robots private int iToggleState = 0; // Semaphore for applet state private int iInitFlag = 0; // To allow browsers to get information about the applet: public String getAppletInfo() { return "Robot social group applet, version " + sVerNum + ", by Rick Wagner, copyright 2006-2009,\nall rights reserved.\n\n" + "This is an educational example of object oriented design \n" + "using a timer to control animation rate.\n\n" + "Compiled " + sCompileDate + ". Source code use authorized for\n" + "educational purposes only. No use without attribution.\n"; } // Initialize the applet public void init() { int i = 0; float sfTheta = 0; float x = 0; float y = 0; String sTemp = null; // Applet parameter: sTemp = getParameter("InitFlag"); if (sTemp != null) { iInitFlag = Integer.valueOf(sTemp).intValue(); } this.setBackground(Color.lightGray); // For the 25% gray browser background (c0c0c0) dApplet = this.size(); // Size is set in the calling html file iTimerInterval = 33; // 33 ms is 30 frames per sec (TV rate) AnimationTimer = new Timer(this, iTimerInterval); // 40 ms is 25 frames per sec, 67 is 15 lblMessage = new Label("Welcome to Robot Game Simulation. Click to start."); lblMessage.setAlignment(Label.CENTER); this.add(lblMessage); for (i = 0; i < iNumRobots; i++) { robotArray[i] = new Robot(); robotArray[i].setColor(get10Color(i % 10)); } switch(iInitFlag) { case 0: { setRobotRandom(); break; } case 1: { setRobotCircular(); break; } default: { break; } } } // End of init() // Execute this code after initialization public void start() { this.requestFocus(); // So we can get keyboard input System.out.println("\n" + this.getAppletInfo()); // Identify self to the Java console-aware user } // End of start() // Execute this code when the browser leaves the page: public void stop() { AnimationTimer.stop(); } // End of stop(); public void onTimer() // Called by the timer { int i; for (i = 0; i < iNumRobots; i++) { robotArray[i].update(i, iNumRobots, robotArray); // Update the robots } repaint(); } // The applet frame painting function public void paint(Graphics g) { // Code for displaying images or drawing in the applet frame int i = 0; g.clearRect(0, 0, dApplet.width, dApplet.height); this.setBackground(Color.lightGray); // Having been done in init(), one wouldn't think // this is necessary (but it is). // Draw a recessed frame around the applet border. Designed for gray-on-gray browser background. g.setColor(Color.black); g.drawLine(0, 0, dApplet.width - 1, 0); g.drawLine(0, 0, 0, dApplet.height - 1); g.setColor(Color.white); g.drawLine(0, dApplet.height - 1, dApplet.width - 1, dApplet.height - 1); g.drawLine(dApplet.width - 1, 1, dApplet.width - 1, dApplet.height - 1); // Paint the robots: for (i = 0; i < iNumRobots; i++) { robotArray[i].paint(g, dApplet); } } // End of paint() // Implements double buffering for smooth animation public void update(Graphics g) { if (imOffScreen == null) { // Make sure the offscreen and graphics exist imOffScreen = this.createImage(dApplet.width, dApplet.height); grOffScreen = imOffScreen.getGraphics(); grOffScreen.clearRect(0, 0, dApplet.width, dApplet.height); } this.paint(grOffScreen); g.drawImage(imOffScreen, 0, 0, null); } public boolean mouseDown(Event e, int x, int y) { switch (iToggleState) { case 0: { lblMessage.setText("Click to stop."); this.showStatus("Click to stop."); AnimationTimer.start(); break; } case 1: { lblMessage.setText("Click to reset."); this.showStatus("Click to reset."); AnimationTimer.stop(); break; } case 2: { lblMessage.setText("Click to start."); this.showStatus("Click to start."); switch(iInitFlag) { case 0: { setRobotRandom(); break; } case 1: { setRobotCircular(); break; } default: { break; } } repaint(); break; } } iToggleState = (iToggleState + 1) % 3; this.requestFocus(); // Added July 4, 2008: works around bug added in return true; // Java 2SE. Enables keystroke reception. } public boolean keyDown(Event e, int k) { return true; } private Color get10Color(int i) { Color c = null; switch (i) { case 0: { c = Color.red; break; } case 1: { c = Color.blue; break; } case 2: { c = Color.white; break; } case 3: { c = Color.green; break; } case 4: { c = Color.yellow; break; } case 5: { c = Color.cyan; break; } case 6: { c = Color.orange; break; } case 7: { c = Color.pink; break; } case 8: { c = Color.magenta; break; } case 9: { c = Color.yellow.darker(); break; } } return c; } // End of get10Color() private void setRobotRandom() { float x = 0; float y = 0; float sfTheta = 0; int i = 0; int j = 0; boolean bClear = false; float sfDistance = 0; for (i = 0; i < iNumRobots; i++) { bClear = false; while (!bClear) { bClear = true; x = (float) (30 * Math.random()); // 30 times the paint scale factor is 600 pixels y = (float) (30 * Math.random()); // See if these coordinates are near any existing robot for (j = 0; j < iNumRobots; j++) { if (i != j) { // Compute the ith proposed coordinates distance to the jth robot sfDistance = ((x - robotArray[j].getX()) * (x - robotArray[j].getX())) + ((y - robotArray[j].getY()) * (y - robotArray[j].getY())); if (sfDistance < 2) { bClear = false; break; // Break out of for j } } } } // bClear is true so pose the ith robot sfTheta = (float) (2 * Math.PI * Math.random() - Math.PI); // Normalized to +/- pi from east. robotArray[i].pose(x, y, sfTheta); } repaint(); } // End of setRobotRandom(); private void setRobotCircular() { float x = 0; float y = 0; float sfTheta = 0; float sfDeltaAngle = 0; float sfTempAngle = 0; int i = 0; int j = 0; float sfDistance = 0; sfDeltaAngle = (float) (2 * Math.PI / iNumRobots); // Pie slice for circular position for (i = 0; i < iNumRobots; i++) { sfTempAngle += sfDeltaAngle; sfTheta = (float) (sfTempAngle - Math.PI); // Normalized to +/- pi from east. x = (float) (15 * Math.cos(sfTheta) + 15); // 30 times the paint scale factor is 600 pixels y = (float) (15 * Math.sin(sfTheta) + 15); sfTheta += (float) Math.PI; robotArray[i].pose(x, y, sfTheta); } } // End of setRobotRandom(); } // End of Applet class class Timer implements Runnable { // Calls the onTimer() function in the calling applet every iInterval milliseconds. // Copyright 1998 and 1999 by Rick Wagner, all rights reserved. No use without attribution. // Note: a timer object is now included in the J2SE, (JDK 1.5, 1.6 development kit). private Thread TimerThread = null; private int iInterval = 0; private RoboGame app = null; // The calling applet public Timer(RoboGame a, int i) // Interval constructor { iInterval = i; app = a; } public void start() { TimerThread = new Thread(this); TimerThread.start(); } public void stop() { if (TimerThread != null) TimerThread.stop(); TimerThread = null; } public void run() { long lStartTime = System.currentTimeMillis(); long lTemp = 0; int iSleepTime = iInterval / 10; if (iSleepTime == 0) iSleepTime = 1; while (true) { try { TimerThread.sleep(iSleepTime); // Wake up every 1/10 interval and see } // if it's time yet. catch(InterruptedException e1) { } lTemp = System.currentTimeMillis(); if (lTemp - lStartTime >= iInterval) { app.onTimer(); // Trigger the desired events lStartTime = lTemp; } } } } // End of Timer class class Robot { private float sfDirection; // Direction of travel in radians from east private float sfPx; // Position private float sfPy; private float sfX[] = new float[5]; // Float arrays for the robot outline polygon private float sfY[] = new float[5]; private int iX[] = new int[5]; // Integer arrays for constructing the polygon for painting private int iY[] = new int[5]; private float sfPaintScaleFactor; private Color cRobot; public Robot() // Default constructor { cRobot = Color.white; sfPx = 0; sfPy = 0; sfPaintScaleFactor = 20; // Robot is painted 20 pixels long pose(0, 0, 0); } public Robot(float px, float py, float theta) // Position and direction constructor { cRobot = Color.white; sfPx = px; sfPy = py; sfPaintScaleFactor = 20; // Robot is painted 20 pixels long pose(sfPx, sfPy, theta); } public void setColor(Color c) // Mutator { cRobot = c; } public float getX() // Accessor { return sfPx; } public float getY() // Accessor { return sfPy; } public void pose(float x, float y, float a) { // Sets the position and orientation of the robot and computes the outline polygon: int i = 0; float sfTempX = 0; float sfTempY = 0; float sfSin = 0; float sfCos = 0; sfX[0] = (float) -0.5; sfY[0] = (float) -0.375; sfX[1] = (float) 0.5; sfY[1] = (float) -0.375; sfX[2] = (float) 0.75; sfY[2] = (float) 0.0; sfX[3] = (float) 0.5; sfY[3] = (float) 0.375; sfX[4] = (float) -0.5; sfY[4] = (float) 0.375; sfSin = (float) Math.sin(a); // Compute these just once and store sfCos = (float) Math.cos(a); for (i = 0; i < 5; i++) { sfTempX = sfX[i]; sfTempY = sfY[i]; sfX[i] = x + sfTempX * sfCos - sfTempY * sfSin; sfY[i] = y + sfTempX * sfSin + sfTempY * sfCos; } sfPx = x; sfPy = y; sfDirection = a; } public void update(int iMyIndex, int iNumRobots, Robot[] rArray) { int i = 0; float sfIncrement = (float) 0.07; // Sets the default speed of forward // motion float sfDistance = 0; // Actually, the distance squared, float sfShortestDistance = 800 * 800; // we don't take the square root float sfSecondShortest = 800 * 800; // for performance reasons int iNearestRobotIndex = 0; int iSecondNearest = 0; float sfAngleToNearest = 0; float sfAngleToMidPoint = 0; float sfDistanceToMidPoint = 0; float sfSqrtDistance = 0; // Sometimes need the actual distance float sfDeltaAngle = 0; float sfDeltaX = 0; float sfDeltaY = 0; float sfMidPointX = 0; float sfMidPointY = 0; float sfTurnFactor = 0; // Find the distances to all the other robots and identify the shortest and next shortest for (i = 0; i < iNumRobots; i++) { if (i != iMyIndex) { sfDistance = ((sfPx - rArray[i].sfPx) * (sfPx - rArray[i].sfPx)) + ((sfPy - rArray[i].sfPy) * (sfPy - rArray[i].sfPy)); if (sfDistance < sfShortestDistance) { sfShortestDistance = sfDistance; iNearestRobotIndex = i; } else { if (sfDistance < sfSecondShortest) { sfSecondShortest = sfDistance; iSecondNearest = i; } } } } // Move away from nearest robot if too close if (sfShortestDistance < 3.1) { sfSqrtDistance = (float) Math.sqrt(sfDistanceToMidPoint); if (sfSqrtDistance < 0.25) sfSqrtDistance = (float) 0.25; // Find the direction *from* the nearest robot: sfAngleToNearest = (float) Math.atan2(sfPy - rArray[iNearestRobotIndex].sfPy, sfPx - rArray[iNearestRobotIndex].sfPx); // Find the delta angle to the nearest robot: sfDeltaAngle = normalizeAngle(sfAngleToNearest - sfDirection); // Change direction toward the delta angle by a degree if the delta is greater than a degree: if (sfDeltaAngle > 0.017 || sfDeltaAngle < -0.017) // There are 17 milliradians { // in a degree of angle if (sfDeltaAngle > 0) { sfDirection += 0.017 * 2.5 / sfSqrtDistance; // Turn angle in radians } // 2.5 is tuning if (sfDeltaAngle < 0) { sfDirection -= 0.017 * 2.5 / sfSqrtDistance; } } sfDeltaX = (float) (Math.cos(sfDirection) * sfIncrement * 0.3 / (sfSqrtDistance)); // 0.3 is tuning sfDeltaY = (float) (Math.sin(sfDirection) * sfIncrement * 0.3 / (sfSqrtDistance)); sfPx += sfDeltaX; sfPy += sfDeltaY; pose(sfPx, sfPy, normalizeAngle(sfDirection)); } else { // Compute the point midway between the nearest and second nearest: sfMidPointX = (rArray[iNearestRobotIndex].sfPx + rArray[iSecondNearest].sfPx) / 2; sfMidPointY = (rArray[iNearestRobotIndex].sfPy + rArray[iSecondNearest].sfPy) / 2; // Find the distance to the midpoint: sfDistanceToMidPoint = ((sfPx - sfMidPointX) * (sfPx - sfMidPointX)) + ((sfPy - sfMidPointY) * (sfPy - sfMidPointY)); // Move toward the midpoint if not close enough: if (sfDistanceToMidPoint > 1) { // Find the angle to the midpoint: sfAngleToMidPoint = (float) Math.atan2(sfMidPointY - sfPy, sfMidPointX - sfPx); // Find the delta angle to the midpoint: sfDeltaAngle = normalizeAngle(sfAngleToMidPoint - sfDirection); sfSqrtDistance = (float) Math.sqrt(sfDistanceToMidPoint); sfTurnFactor = Math.abs(sfDeltaAngle) * sfSqrtDistance * ((float) 0.6); // 0.6 is tuning if (sfTurnFactor > 17) { sfTurnFactor = 17; } // Change direction toward the delta angle by a factor if the delta is greater than a degree: if (sfDeltaAngle > 0.017 || sfDeltaAngle < -0.017) // There are 17 milliradians { // in a degree of angle if (sfDeltaAngle > 0) { sfDirection += 0.017 * sfTurnFactor; // sfTurnFactor degrees turn; } if (sfDeltaAngle < 0) { sfDirection -= 0.017 * sfTurnFactor; } } sfDeltaX = (float) (Math.cos(sfDirection) * sfIncrement * sfSqrtDistance * 0.9); // 0.9 is tuning sfDeltaY = (float) (Math.sin(sfDirection) * sfIncrement * sfSqrtDistance * 0.9); sfPx += sfDeltaX; sfPy += sfDeltaY; pose(sfPx, sfPy, normalizeAngle(sfDirection)); } } } // End of Robot.update() public void paint(Graphics g, Dimension d) { Polygon pgon = null; int i; for (i = 0; i < 5; i++) { iX[i] = 140 + Math.round(sfPaintScaleFactor * sfX[i]); // Set the polygon array for iY[i] = 720 - Math.round(sfPaintScaleFactor * sfY[i]); // painting } pgon = new Polygon(iX, iY, 5); g.setColor(cRobot); g.fillPolygon(pgon); g.setColor(Color.black); g.drawPolygon(pgon); } // End of Robot.paint() private float normalizeAngle(float a) // Normalize angle to +/- pi { // radians float r = a; if (a > Math.PI) r -= (float) (Math.PI * 2.0); if (a <= -Math.PI) r += (float) (Math.PI * 2.0); return r; } // End of Robot.normalizeAngle() } // End of Robot class, line 637