/* CMPT 460; Assignment 1 * Andrey Mirtchovski; SN == 358764 * * This Applet/standalone program implements Java2D and Vecmath to produce * images of moving balls in a window. * Using extremely simplistic collision-detection algorithm, the balls are * reacting to each other based on the Newton's Experimental Law of Motion. * After each collision the force vectors for each ball involved are * recalculated using the abovementioned law and 'Conservation of momentum' * (pretty much as explained in class). * This program accepts input for the following parameters (run with -h for * help menu; default values shown in brackets): * -n -- number of balls [5] * -f -- fps [50] * -m -- maximum speed of ball [10] * -s -- size of ball (diameter in pixels) [60] * -e -- coefficient of restitution [0.8] * -h -- help * * In order to achieve relative speed and to avoid flickering, this program * uses double buffering in its update() routine. * * Drawbacks: due to the fact that x/y locations of the balls are updated on * each move, and because they are incremented by the (positive or negative) * velocities in each direction, there exists the risc of viewing balls that * overlap, missing collisions for high-speed balls and miscalculating the * directions and force vectors after the collision. * There have been no special optimizations performed in order to avoid the * abovementioned drawbacks. The only thing taken care of is to move the * colliding balls away from each other in the opposite direction until they * stop colliding (hence the relatively small 'jump' they exhibit). * * Tested on: Solaris 7 (Sun Ultrasparc) * IRIX 6.5 (O2) * Windows NT/Me * * Status: Fully Operational (see 'drawbacks' section)... * */ import java.awt.*; import java.awt.geom.*; import java.awt.image.*; import java.awt.event.*; import java.applet.Applet; import java.util.*; import javax.swing.*; import javax.vecmath.*; public class BouncingBall extends JApplet implements Runnable { public class Ball extends Object { // This class defines a single Ball // all the elements of class ball are public, in order to keep // things simple. public int X = 0; // location along the X axis public int Y = 0; // location along the Y axis public int dx = 0; // speed along the X axis public int dy = 0; // speed along the Y axis public Color c = null; Ball(int topx, int topy, int max) { // initialize with random variables // determine direction and speed of ball this.X = rnd.nextInt(topx - bsize) + bsize; this.Y = rnd.nextInt(topy - bsize) + bsize; dx = rnd.nextInt(max); dx = rnd.nextBoolean() ? dx : -dx; dy = rnd.nextInt(max); dy = rnd.nextBoolean() ? dy : -dy; // set color to a random value c = new Color(rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256)); } public int dist(Ball b) { // calculate the distance to b int distx = Math.abs(b.X - X); distx *= distx; int disty = Math.abs(b.Y - Y); disty *= disty; double dist = Math.sqrt((double)(distx + disty)); return (int)Math.round(dist); } public boolean collide(Ball b) { return (dist(b) < bsize) ? true : false; } public void calcVectors(Ball b) { // This routine recalculates the force vectors for the current // ball ('this') and the argument ball ('b') // calculate the distances between ball's centre. // this forms the X and Y of the vector of collision int distx = b.X - X; int disty = b.Y - Y; // create force vectors for 'this', ball 'b' and the collision Vector2d v1 = new Vector2d((double) dx, (double)dy); Vector2d v2 = new Vector2d((double) b.dx, (double)b.dy); Vector2d v3 = new Vector2d((double) distx, (double)disty); // normalize the vector of collision in order to calculate the // projection v3.normalize(); // obtain the dot product for 'this' object double dot1 = v1.dot(v3); // calculate the projection vector for 'this' onto the vector of // collision Vector2d proj = new Vector2d(dot1 * v3.x, dot1 * v3.y); // find the vector perpendicular to the projection. // this vector does not change after the collision, but is used // in calculating the new force vector for 'this' Vector2d projPerp = new Vector2d(v1.x - proj.x, v1.y - proj.y); // turn the vector of collision's direction in order to // calculate the new vectors for 'b' v3.x *= -1; v3.y *= -1; v3.normalize(); double dot2 = v2.dot(v3); Vector2d projb = new Vector2d(dot2 * v3.x, dot2 * v3.y); Vector2d projbPerp = new Vector2d(v2.x - projb.x, v2.y - projb.y); // calculate the new projection vectors for 'this' and 'b' by // adding the 'swapped' projection vectors with the // perpendiculars. also take into consideration the coefficient // of elasticity dx = (int)Math.round(e * projb.x + projPerp.x); dy = (int)Math.round(e * projb.y + projPerp.y); b.dx = (int)Math.round(e * proj.x + projbPerp.x); b.dy = (int)Math.round(e * proj.y + projbPerp.y); // move the balls away from each other until they are not // colliding anymore. // this step is necessary because of the very simplistic // 'collision detection' algorithm and the fact that a ball's // location is updated by more than one pixel at each step while(collide(b)) { if(X < b.X) { X--; b.X++; } else { X++; b.X--; } if(Y < b.Y) { Y--; b.Y++; } else { Y++; b.Y--; } } } } // end of class Ball class BallControls extends JPanel implements ActionListener { BouncingBall b; JTextField tf1, tf2, tf3, tf4; public BallControls(BouncingBall bb) { this.b = bb; setBackground(Color.gray); JLabel l = new JLabel(" Enter: "); l.setForeground(Color.black); add(l); l = new JLabel(" e="); l.setForeground(Color.black); add(l); add(tf1 = new JTextField(String.valueOf(b.e))); tf1.setPreferredSize(new Dimension(30,20)); tf1.addActionListener(this); l = new JLabel(" how many="); l.setForeground(Color.black); add(l); add(tf2 = new JTextField(String.valueOf(b.numballs))); tf2.setPreferredSize(new Dimension(30,20)); tf2.addActionListener(this); l = new JLabel(" size="); l.setForeground(Color.black); add(l); add(tf3 = new JTextField(String.valueOf(b.bsize))); tf3.setPreferredSize(new Dimension(30,20)); tf3.addActionListener(this); l = new JLabel(" max speed="); l.setForeground(Color.black); add(l); add(tf4 = new JTextField(String.valueOf(b.max))); tf4.setPreferredSize(new Dimension(30,20)); tf4.addActionListener(this); } public void actionPerformed(ActionEvent e) { try { if (e.getSource().equals(tf1)) { b.e = Double.parseDouble(tf1.getText().trim()); } else if (e.getSource().equals(tf2)) { b.numballs = Integer.parseInt(tf2.getText().trim()); } else if (e.getSource().equals(tf3)) { b.bsize = Integer.parseInt(tf3.getText().trim()); } else if (e.getSource().equals(tf4)) { b.max = Integer.parseInt(tf4.getText().trim()); } b.balls = new LinkedList(); b.addBalls(); } catch (Exception ex) {} } } // End BallControls class static Random rnd = new Random(); // used in Ball static int topx = 400; // default dimensions of window static int topy = 400; static int fps = 30; int frameNumber = -1; int delay; boolean frozen = false; Thread animatorThread; LinkedList balls = new LinkedList(); // contains all the balls static int bsize = 60; // size of balls static int numballs = 5; // number of balls static double e = 0.8; // coefficient of elasticity static int max = 10; // max speed private BufferedImage bimg; public void init() { final BouncingBall bb = new BouncingBall(); JFrame f = new JFrame("Bouncing Balls by Andrey Mirtchovski"); f.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) {System.exit(0);} public void windowDeiconified(WindowEvent e) { bb.start(); } public void windowIconified(WindowEvent e) { bb.stop(); } }); f.getContentPane().add("North", new BallControls(this)); f.getContentPane().add("Center", this); delay = (fps > 500) ? (500 / fps) : 100; // create all the balls for this run addBalls(); f.pack(); f.setSize(new Dimension(topx,topy)); f.show(); } public void start() { if (frozen) { //Do nothing. The user has requested that we //stop changing the image. } else { //Start animating! if (animatorThread == null) { animatorThread = new Thread(this); } animatorThread.start(); } } public void stop() { //Stop the animating thread. animatorThread = null; } public void reset(int x, int y) { topx = x; topy = y; } public boolean mouseDown(Event e, int x, int y) { if (frozen) { frozen = false; start(); } else { frozen = true; stop(); } return true; } public void run() { //Just to be nice, lower this thread's priority //so it can't interfere with other processing going on. //Thread.currentThread().setPriority(Thread.MIN_PRIORITY); //Remember the starting time. long startTime = System.currentTimeMillis(); //This is the animation loop. while (Thread.currentThread() == animatorThread) { //Advance the animation frame. frameNumber++; //Display it. repaint(); //Delay depending on how far we are behind. try { startTime += delay; Thread.sleep(Math.max(0, startTime-System.currentTimeMillis())); } catch (InterruptedException e) { break; } } } public static void usage() { System.err.println("usage: BouncingBall [-f fps] [-n num] [-m max] [-s s] [-e e]"); System.err.println("\tfps -- refresh rate"); System.err.println("\tnum -- number of balls"); System.err.println("\tmax -- maximum speed"); System.err.println("\ts -- size of balls"); System.err.println("\te -- coefficient of restitution (0 <= e <= 1)"); System.err.println("\th -- this menu"); System.exit(1); } public void addBalls() { // addBalls() performs a check whether the ball just created // collides with any other, already existing ball for(int i = 0; i < numballs; i++) { Ball b = new Ball(topx, topy, max); for(int j = 0; j < balls.size(); j++) { if(b.collide((Ball)balls.get(j))) { j = 0; b = new Ball(topx, topy, max); } } balls.add(b); } } public Graphics2D createGraphics2D(int topx, int topy) { Graphics2D g2 = null; if (bimg == null || bimg.getWidth() != topx || bimg.getHeight() != topy) { bimg = (BufferedImage) createImage(topx, topy); reset(topx, topy); } g2 = bimg.createGraphics(); g2.setBackground(getBackground()); g2.clearRect(0, 0, topx, topy); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); return g2; } public void paint(Graphics g) { Dimension d = getSize(); Graphics2D g2 = createGraphics2D(d.width, d.height); update(topx, topy, g2); g2.dispose(); g.drawImage(bimg, 0, 0, this); } public void update(int topx, int topy, Graphics2D g2) { // takes care of drawing the balls and calls the necessary vector // calculation routines, if they have collided Ball b = null; Ball b2 = null; for(int i = 0; i < numballs; i++) { // cycle through all the balls, incrementing their position by // the respective velocity... b = (Ball)balls.get(i); b.X += b.dx; // check for out-of-bounds and bring back within the window if(b.X > topx-bsize) { b.X = topx - bsize ; b.dx = (b.dx > 0) ? -b.dx : b.dx; } if(b.X < 0) { b.X = 0; b.dx = (b.dx < 0) ? -b.dx : b.dx; } // same calculation performed for the Y axis b.Y += b.dy; if(b.Y > topy-bsize) { b.Y = topy - bsize ; b.dy = (b.dy > 0) ? -b.dy : b.dy; } if(b.Y < 0) { b.Y = 0; b.dy = (b.dy < 0) ? -b.dy : b.dy; } // cycle through all other balls checking for collisions // if collision found recalculate force vectors for(int j = 0; j < numballs; j++) { if (i == j) continue; b2 = (Ball)balls.get(j); if(b.collide(b2)) { b.calcVectors(b2); } } // draw the current ball Ellipse2D ellipse = new Ellipse2D.Double((double)b.X, (double)b.Y, bsize, bsize); g2.setPaint(b.c); g2.fill(ellipse); } } public static void main(String argv[]) { // extremely simplistic argument checking try { for(int i = 0; i < argv.length; i++) { if(argv[i].equals("-n")) numballs = new Integer(argv[++i]).intValue(); else if(argv[i].startsWith("-f")) fps = new Integer(argv[++i]).intValue(); else if(argv[i].startsWith("-m")) max = new Integer(argv[++i]).intValue(); else if(argv[i].startsWith("-s")) bsize = new Integer(argv[++i]).intValue(); else if(argv[i].startsWith("-e")) { e = new Double(argv[++i]).doubleValue(); if (e < 0 || e > 1) { System.err.println("Sorry, value for 'e' must be between 0 and 1"); usage(); } } else { usage(); } } } catch (Exception e) { usage(); } final BouncingBall bb = new BouncingBall(); bb.init(); /* final BouncingBall bb = new BouncingBall(); JFrame f = new JFrame("Bouncing Balls by Andrey Mirtchovski"); f.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) {System.exit(0);} public void windowDeiconified(WindowEvent e) { bb.start(); } public void windowIconified(WindowEvent e) { bb.stop(); } }); bb.init(bb); f.getContentPane().add("Center", bb); f.pack(); f.setSize(new Dimension(topx,topy)); f.show(); */ } }