/* File: Thruster5.java Demonstration of thrust applied to rigid 2D objects. Handles collisions with walls and between objects. One of the www.MyPhysicsLab.com physics simulation applets. Copyright (c) 2001 Erik Neumann This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Contact Erik Neumann at erikn@MyPhysicsLab.com or 610 N. 65th St. Seattle WA 98103 ======================= HOW TO COMPILE ======================= Need to first compile: SimApplet.java for classes SimApplet, SimCanvas, SimLayout1, GenericAppletFrame,... to create jar file: jar cfm ../lab/Thruster5.jar ThrusterMainClass.txt *.class */ import java.awt.*; import java.awt.event.*; import java.applet.*; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.Enumeration; import java.io.*; import java.util.Iterator; import java.util.Vector; ///////////////////////////////////////////////////////////////////////////////// /* FuzzyButton is a non-focusable button (hence fuzzy). This is so that after clicking the button, the applet can still process key events. class FuzzyButton extends Button { public FuzzyButton(String name) { super(name); } public boolean isFocusable() { // Java 1.4 version return false; } } */ /////////////////////////////////////////////////////////////////////////// class ThrustLayout implements LayoutManager { public ThrustLayout() {} public void addLayoutComponent(String name, Component c) {} public void removeLayoutComponent(Component c) {} public Dimension preferredLayoutSize(Container target) { return new Dimension(500, 500); } public Dimension minimumLayoutSize(Container target) { return new Dimension(100,100); } public void layoutContainer(Container target) { // Canvas is assumed to be first component (index 0). // First pass: position the controls in upper part of window. // Lay them out left to right, and skip to next line when they // don't fit. //System.out.println("layoutContainer"); int canvasWidth = target.getSize().width; int i, space = 3; int lineVertical = space; int lineHorizontal = space; // cumulative width int maxHeight = 0; int startComponent = 1; // Which component is at start of current line. int n = target.getComponentCount(); for (i = startComponent; i < n; i++) { Component m = target.getComponent(i); if (m.isVisible()) { boolean wideLabel = m instanceof MyLabel; Dimension d = m.getPreferredSize(); // We assume that wide labels take up a whole line of the display. if (wideLabel) m.setSize(canvasWidth, d.height); // wide label gets entire width of canvas else m.setSize(d.width, d.height); // check if this component can fit on current line. if (wideLabel || (lineHorizontal + d.width > canvasWidth)) { // Can't fit on this line // Adjust items on this line to be vertically centered within the line int j; for (j = startComponent; j maxHeight) maxHeight = d.height; lineHorizontal += d.width + space; } } } // Now we know how much vertical space the controls need. // Set the canvas to take up the rest of the space. Component lastComp = target.getComponent(n-1); int lastY = lastComp.getLocation().y + lastComp.getSize().height + space; int canvasHeight = target.getSize().height - lastY; Component cnvs = target.getComponent(0); // this should be the canvas cnvs.invalidate(); // so that the offscreen buffer gets recreated cnvs.setLocation(0, 0); cnvs.setSize(canvasWidth, canvasHeight); // Move all the controls to below the canvas. for (i = 1; i < n; i++) { Component m = target.getComponent(i); Point loc = m.getLocation(); m.setLocation(loc.x, loc.y + canvasHeight); } // following line is necessary to fix update garbage when layout changes! target.update(target.getGraphics()); } }; ///////////////////////////////////////////////////////////////////////////// class ThrustText { public String m_text; private double m_num = 0; private boolean show_num = false; private Font myFont = null; private FontMetrics myFM = null; private int ascent = 20; private int descent = 10; private int leading = 5; private NumberFormat nf = null; public int line_height = 10; // WARNING: only accurate after draw() public double m_X1 = 0; public double m_Y1 = 0; public boolean centered = true; public ThrustText(String t) { m_text = t; } public void setNumber(double n) { show_num = true; m_num = n; } public void setFont(Graphics g) { if (myFont != null) return; myFont = new Font("Serif", Font.PLAIN, 14); myFM = g.getFontMetrics(myFont); ascent = myFM.getAscent(); descent = myFM.getDescent(); leading = myFM.getLeading(); nf = NumberFormat.getNumberInstance(); nf.setMaximumFractionDigits(5); if (line_height != ascent+descent+leading) { line_height = ascent+descent+leading; } } public void draw (Graphics g, CoordMap map) { int x1, y1; setFont(g); g.setFont(myFont); g.setColor(Color.black); if (centered) { y1 = map.screen_top + map.screen_height/2; int w = myFM.stringWidth(m_text); x1 = map.screen_left + map.screen_width/2 - w/2; } else { x1 = map.simToScreenX(m_X1); y1 = map.simToScreenY(m_Y1); } if (show_num) g.drawString(m_text + nf.format(m_num), x1, y1); else g.drawString(m_text, x1, y1); } } ///////////////////////////////////////////////////////////////////////////////// // A scrollbar where you can set the preferred width and height class MyScrollbar extends Scrollbar { int w,h; public MyScrollbar(int w, int h, int orient, int value, int vis, int min, int max) { // new Scrollbar(orientation, value, visibleAmount, minimum, maximum) super(orient, value, vis, min, max); this.w = w; this.h = h; } public Dimension getPreferredSize() { return new Dimension(w,h); } } ///////////////////////////////////////////////////////////////////////////////// // A Label where the preferred width and height is determined by the text & font. // Otherwise, the Label winds up being larger than necessary. class MyLabel extends Label { String sample = null; // sample text is used to figure max width of text public MyLabel(String text) { super(text); } public MyLabel(String text, int alignment) { super(text, alignment); } public MyLabel(String text, int alignment, String sample) { super(text, alignment); this.sample = sample; } public Dimension getPreferredSize() { Font myFont = new Font("SansSerif", Font.PLAIN, 12); this.setFont(myFont); FontMetrics myFM = this.getFontMetrics(myFont); int w,h; String txt; if (sample == null) txt = this.getText(); else txt = sample; w = 5+myFM.stringWidth(txt); // use sample to figure text width h = myFM.getAscent() + myFM.getDescent(); return new Dimension(w,h); } } ///////////////////////////////////////////////////////////////////////////////// // A slider consists of a label, a scrollbar, and a numeric display of the value. class MySlider extends Panel { double min, delta; public MyScrollbar scroll; MyLabel nameLabel; MyLabel myNumber; NumberFormat nf = NumberFormat.getNumberInstance(); public MySlider(AdjustmentListener applet, String name, double value, double min, double max, int increments, int digits) { this.min = min; delta = (max - min)/increments; nameLabel = new MyLabel(name, Label.CENTER); add(nameLabel); // new MyScrollbar(width, height, orientation, value, visibleAmount, minimum, maximum) scroll = new MyScrollbar(75, 15, Scrollbar.HORIZONTAL, (int)(0.5+(value-min)/delta), 10, 0, increments+10); add(scroll); scroll.addAdjustmentListener(applet); nf = NumberFormat.getNumberInstance(); nf.setMaximumFractionDigits(digits); nf.setMinimumFractionDigits(digits); myNumber = new MyLabel(nf.format(value), Label.LEFT, "88.88"); add(myNumber); FlowLayout lm = (FlowLayout)getLayout(); lm.setHgap(1); lm.setVgap(1); } public double getValue() { double value = min + (double)scroll.getValue()*delta; myNumber.setText(nf.format(value)); // update the text as a side effect return value; } /* public Insets getInsets() { return new Insets(1,1,1,1); }*/ /* // keep this around for understanding how panel layout works! public void paint(Graphics g) { Rectangle r = getBounds(); Rectangle c = g.getClipBounds(); System.out.println("Panel.paint "+r.x+" "+ r.y+" "+r.width+" "+r.height); if (c != null) System.out.println(" clip bounds "+c.x+" "+ c.y+" "+c.width+" "+c.height); Insets i = getInsets(); System.out.println(" insets "+i.top+" "+i.bottom+" "+i.right+" "+i.left); FlowLayout lm = (FlowLayout)getLayout(); System.out.println(" layout hgap vgap "+lm.getHgap()+" "+lm.getVgap()); g.setColor(Color.yellow); // NOTE: drawing takes place in local (panel) coordinates! g.fillRect(0, 0, r.width-1, r.height-1); super.paint(g); } */ } ///////////////////////////////////////////////////////////////////////////////// /* object coords are as follows. angle = zero as shown. tAngle = -pi/4 d-----c(width, height) | | | | coords of e = x + sin(angle)*cmy, y - cos(angle)*cmy | | coords of a = ex - cos(angle)*cmx, ey - sin(angle)*cmx | | coords of b = ax + cos(angle)*width, ay + sin(angle)*width | cm | coords of c = bx - sin(angle)*height, by + cos(angle)*height | /| coords of d = ax - sin(angle)*height, ay + cos(angle)*height | / | | t | | | | | a--e--b (0,0) */ class Thruster5Object { public double x,y; // position of the center of mass in the world public double angle; // rotation of object around center of mass private double width; // width of object private double height; // height of object public double cmx, cmy; // position of center of mass in object coords public double thrustX, thrustY; // position of thrust point in object coords public double[] tAngle; // angle of the thrust in object coords public boolean[] active; // which thrusters are firing public double tMagnitude; // thrust magnitude public double mass; public double ax,ay,bx,by,cx,cy,dx,dy,tx,ty; // positions of corners public Color color; public Thruster5Object() { x = 0; y = 0; angle = 0; setWidth(0.5); setHeight(3); thrustX = width/2; thrustY = 0.8*height; tMagnitude = 0.5; mass = 1; color = Color.black; tAngle = new double[4]; tAngle[0] = Math.PI/2; // left tAngle[1] = -Math.PI/2; // right tAngle[2] = 0; // up tAngle[3] = Math.PI; // down active = new boolean[4]; active[0] = active[1] = active[2] = active[3] = false; moveTo(x, y, angle); } public double getWidth() { return this.width; } public double getHeight() { return this.height; } public double getMinHeight() { // for potential energy calculation return (width 0) { Collision result = new Collision(); result.depth = dist; // depth of collision result.impactX = gx; // point of impact result.impactY = gy; result.normalObj = selfIndex; // object corresponding to the normal result.object = objIndex; // object whose corner is colliding // figure out normal to that edge, and rotate it back to world coords // (don't need to translate it) switch (edge) { case 0: px=0;py=-1;break; case 1: px=1;py=0;break; case 2: px=0;py=1;break; case 3: px=-1;py=0;break; } // normal (outward pointing) result.normalX = px*Math.cos(this.angle) - py*Math.sin(this.angle); result.normalY = px*Math.sin(this.angle) + py*Math.cos(this.angle); return result; } else return null; } } ///////////////////////////////////////////////////////////////////////////////// class Collision { public double depth; // depth of collision (positive = penetration) public double normalX; // normal (pointing outward from normalObj?) public double normalY; public double impactX; // point of impact public double impactY; public int normalObj; // object corresponding to the normal (negative = wall) public int object; // object whose corner is colliding public Collision() { } } ///////////////////////////////////////////////////////////////////////////////// public class Thruster5 extends SimApplet implements KeyListener, ActionListener, MouseListener, AdjustmentListener, ItemListener, MouseMotionListener { boolean debug = false; public static final int RIGHT_WALL = -1; public static final int BOTTOM_WALL = -2; public static final int LEFT_WALL = -3; public static final int TOP_WALL = -4; public static final int MAX_BODIES = 6; // maximum number of bodies public int numBods = 2; // number of bodies public Thruster5Object[] bods; // array of bodies public int numVars; // number of variables in vars[] public double[] vars; // array of variables public double[] old_vars; double gravity = 0.0; double damping = 0.0; double elasticity = 1.0; double thrust = 0.5; MySlider dampSlider; MySlider elasticSlider; MySlider gravitySlider; MySlider thrustSlider; MySlider massSlider; boolean showEnergy = false; // whether to show energy bar chart & labels Checkbox energyCheckbox; // "show energy" checkbox MyLabel preLabel; // label that displays pre-collision energy & momentum MyLabel postLabel; // label that displays post-collision energy & momentum private Font graphFont = null; // for numbers on the bar chart energy graph int graphAscent; // for numbers on the bar chart energy graph double graphFactor = 10; // determines how wide the bar chart is double graphDelta = 2; // spacing of the numbers in the bar chart Choice bodiesChoice; // popup menu for number of bodies ThrustCanvas cvs; // canvas used for drawing simulation into Button buttonStop; Button buttonStart; Button buttonReset; Vector collisions = new Vector(4*numBods); ThrustText message = null; // error message for when simulation is stuck double last_time = -9999; // last time that simulation step was done double sim_time = 0; // simulation time double lastTimeStep = 0; // amount of last time step (time delta) boolean m_Animating = true; protected static final double TOL = 0.0001; // time tolerance for finding collisions NumberFormat nf = NumberFormat.getNumberInstance(); int dragObj = -1; double mouseX, mouseY; boolean gameMode = false; int winningHits = 10; int greenHits = 0, blueHits = 0; // number of times green or blue object hit walls Label greenLabel; // displays number of wall hits Label blueLabel; ThrustText message2 = null; // "game over" message public synchronized void init() { if (debug) System.out.println("starting Thruster5"); if ("true".equalsIgnoreCase(getParameter("game"))) gameMode = true; String str = getParameter("objects"); if ((str != null) && (str != "")) { int objs = Integer.parseInt(str); if ((objs > 0) && (objs < MAX_BODIES)) numBods = objs; } requestFocus(); setBackground(Color.white); // otherwise controls appear over gray setLayout(new ThrustLayout()); // CoordMap(y_dir, x1, x2, y1, y2, align__x, align__y) double w = 5; map = new CoordMap(CoordMap.INCREASE_UP, -w, w, -w, w, CoordMap.ALIGN_MIDDLE, CoordMap.ALIGN_MIDDLE); add(cvs = new ThrustCanvas(this)); add(buttonStop = new Button("pause")); buttonStop.addActionListener(this); add(buttonStart = new Button("resume")); buttonStart.addActionListener(this); add(buttonReset = new Button("reset")); buttonReset.addActionListener(this); addKeyListener(this); //addMouseListener(this); if (gameMode) { add(greenLabel = new Label("green 0 ")); add(blueLabel = new Label("blue 0 ")); } if (!gameMode) { // popup menu for number of bodies bodiesChoice = new Choice(); int i; nf.setMinimumFractionDigits(0); for (i=0; i0 ? "s" : "")); } bodiesChoice.select(numBods-1); bodiesChoice.addItemListener(this); add(bodiesChoice); // energy checkbox: whether to show energy bar energyCheckbox = new Checkbox("Show Energy", showEnergy); energyCheckbox.addItemListener(this); add(energyCheckbox); } if (gameMode) damping = 0.2; else damping = 0; reset(); if (!gameMode) vars[1] = 1.5; // make first body move at start // Slider params: applet, name, value, min, max, number of increments, fraction digits add(dampSlider = new MySlider(this, "damping", damping, 0.0, 1.0, 100, 2)); add(elasticSlider = new MySlider(this, "elasticity", elasticity, 0.0, 1.0, 100, 2)); add(gravitySlider = new MySlider(this, "gravity", gravity, 0.0, 10.0, 100, 2)); add(thrustSlider = new MySlider(this, "thrust", thrust, 0.0, 5.0, 100, 2)); add(massSlider = new MySlider(this, "green mass", bods[0].mass, 0.1, 10.1, 100, 1)); // labels for pre- and post-collision energy preLabel = new MyLabel(" "); postLabel = new MyLabel(" "); } // Synchronized methods are guaranteed to run to completion before // any other synchronized method on the same object. // Here we synchronize to ensure that we don't change the number of // bodies while the animation routines are running. public synchronized void reset() { bods = new Thruster5Object[numBods]; int i; for (i=0; i0) { if (gameMode) bods[0].moveTo(2,0,Math.PI/4); else bods[0].moveTo(-2,0,Math.PI/2); bods[0].color = Color.green; } if (numBods>1) { if (gameMode) bods[1].moveTo(-2,0,-Math.PI/4); else bods[1].moveTo(2,1,0); //bods[1].setWidth(1); //bods[1].setHeight(3); //bods[1].thrustX = bods[1].cmx; //bods[1].thrustY = 0.8*bods[1].getHeight(); bods[1].color = Color.blue; } if (numBods>2) { bods[2].moveTo(1,0,0.1); bods[2].color = Color.red; } if (numBods>3) { bods[3].moveTo(-2.2, 1, 0.2+Math.PI/2); bods[3].color = Color.cyan; } if (numBods>4) { bods[4].moveTo(-2.4,-1, -0.2+Math.PI/2); bods[4].color = Color.magenta; } if (numBods>5) { bods[5].moveTo(-1.8,2, 0.3+Math.PI/2); bods[5].color = Color.orange; } /* variables: x, x', y, y', th, th' bods[0] 0, 1, 2, 3, 4, 5 bods[1] 6, 7, 8, 9, 10, 11 */ numVars = 6*numBods; vars = new double[numVars]; old_vars = new double[numVars]; for (i=0; i= 0) { g.setColor(Color.black); g.drawLine(map.simToScreenX(mouseX), map.simToScreenY(mouseY), map.simToScreenX(bods[dragObj].tx), map.simToScreenY(bods[dragObj].ty)); } if (message != null) message.draw(g, map); if (message2 != null) message2.draw(g, map); // draw rect around the simulation boundary g.setColor(Color.black); int left = map.leftBox(); int top = map.topBox(); int width = map.widthBox() -1; int height = map.heightBox() -1; //System.out.println("drawRect "+left+" "+top+" "+width+" "+height); g.drawRect(left,top,width,height); } public synchronized void threadUpdate() { modifyObjects(); int i = numBods; while (i-- > 0) bods[i].moveTo(vars[6*i+0], vars[6*i+2], vars[6*i+4]); cvs.repaint(); } /* Let th = angle of the body variables: x, x', y, y', th, th' bods[0] 0, 1, 2, 3, 4, 5 bods[1] 6, 7, 8, 9, 10, 11 Let R be the vector from CM (center of mass) to T (thrust point) components of R are Rx, Ry Let N be normalized R, so that N = R / |R| Let F be the thrust vector, with components Fx, Fy (and for mouse dragging, we add a spring force also). The force on the center of mass is (F.N)N... no its just F! CM moves according to (F.N)N = M A... no just F = M A So we have the two equations: Ax = Nx (F.N)/M Ay = Ny (F.N)/M ... no just Ax = Fx/M and Ay = Fx/M The moment of inertia about the CM is I = M (width^2 + height^2)/12 The torque at T about the CM is given by R x F = Rx Fy - Ry Fx The angular dynamics are given by R x F = th'' I So we have the equation th'' = (Rx Fy - Ry Fx)/I The method calcVectors calculates F,N,R given x,y,th */ // executes the i-th diffeq // i = which diffeq, t=time, x= array of variables public double evaluate(int i, double t, double[] x) { int j = i%6; // % is mod, so j tells what derivative is wanted: // 0=x, 1=x', 2=y, 3=y', 4=th, 5=th' int obj = i/6; // which object: 0, 1 int offset = 6*obj; int k; double result = 0; final double springConst = 1; switch (j) { case 0: return x[1+offset]; case 1: result = - damping*x[1+offset]/bods[obj].mass; for (k=0; k<4; k++) { // for each of the 4 thrusters if (bods[obj].active[k]) { double[] v = bods[obj].calcVectors(x[0+offset], x[2+offset], x[4+offset], k); // v[0] = Rx, v[1] = Ry, v[2] = Nx, v[3] = Ny, v[4] = Fx, v[5] = Fy result += v[4]/bods[obj].mass; // Ax = Fx/M } } if (obj == dragObj) { // add rubber band force double[] v = bods[obj].calcVectors(x[0+offset], x[2+offset], x[4+offset], 0); // x component of rubber band force double Fx = springConst*(mouseX - (x[0+offset] + v[0])); result += Fx/bods[obj].mass; // Ax = Fx/M } return result; case 2: return x[3+offset]; case 3: result = - damping*x[3+offset]/bods[obj].mass; result -= gravity; for (k=0; k<4; k++) { // for each thruster if (bods[obj].active[k]) { double[] v = bods[obj].calcVectors(x[0+offset], x[2+offset], x[4+offset], k); // v[0] = Rx, v[1] = Ry, v[2] = Nx, v[3] = Ny, v[4] = Fx, v[5] = Fy result += v[5]/bods[obj].mass; // Ay = Fy/M } } if (obj == dragObj) { // add rubber band force double[] v = bods[obj].calcVectors(x[0+offset], x[2+offset], x[4+offset], 0); // y component of rubber band force double Fy = springConst*(mouseY - (x[2+offset] + v[1])); result += Fy/bods[obj].mass; // Ay = Fy/M } return result; case 4: return x[5+offset]; case 5: result = - damping*x[5+offset]; for (k=0; k<4; k++) { // for each thruster if (bods[obj].active[k]) { double[] v = bods[obj].calcVectors(x[0+offset], x[2+offset], x[4+offset], k); // v[0] = Rx, v[1] = Ry, v[2] = Nx, v[3] = Ny, v[4] = Fx, v[5] = Fy // th'' = (Rx Fy - Ry Fx)/I result += (v[0]*v[5] - v[1]*v[4])/bods[obj].momentAboutCM(); } } if (obj == dragObj) { // add rubber band force double[] v = bods[obj].calcVectors(x[0+offset], x[2+offset], x[4+offset], 0); // x & y components of rubber band force double Fx = springConst*(mouseX - (x[0+offset] + v[0])); double Fy = springConst*(mouseY - (x[2+offset] + v[1])); // th'' = (Rx Fy - Ry Fx)/I result += (v[0]*Fy - v[1]*Fx)/bods[obj].momentAboutCM(); } return result; default: System.out.println("throw? problem in evaluate"); return 0; } } // A version of Runge-Kutta method using arrays // Calculates the values of the variables at time t+h // t = last time value // h = time increment // vars = array of variables // N = number of variables in x array public void solve(double t, double h) { int N = numVars; int i; double[] inp = new double[N]; double[] k1 = new double[N]; double[] k2 = new double[N]; double[] k3 = new double[N]; double[] k4 = new double[N]; for (i=0; i 0) && ((result == null) || (d > result.depth))) { result = new Collision(); result.depth = d; result.normalX = -1; result.normalY = 0; result.normalObj = RIGHT_WALL; } if (((d = map.sim_x1 - cornerX) > 0) && ((result == null) || (d > result.depth))) { result = new Collision(); result.depth = d; result.normalX = 1; result.normalY = 0; result.normalObj = LEFT_WALL; } if (((d = cornerY - map.sim_y2) > 0) && ((result == null) || (d > result.depth))) { result = new Collision(); result.depth = d; result.normalX = 0; result.normalY = -1; result.normalObj = TOP_WALL; } if (((d = map.sim_y1 - cornerY) > 0) && ((result == null) || (d > result.depth))) { result = new Collision(); result.depth = d; result.normalX = 0; result.normalY = 1; result.normalObj = BOTTOM_WALL; } // check for collision with each object for (i=0; i result.depth) result = c; } } } } // additional info for collision if (result != null) { result.impactX = cornerX; result.impactY = cornerY; result.object = obj; } return result; } // find all current collisions // for each point on a body, create a Collision object public void findAllCollisions(double t) { int i,j; // clear vector of collisions collisions.removeAllElements(); // NOTE Vector.clear() is only in Java 1.2 for (i=0; i 0) System.out.println("--------------------------------"); if (collisions.size() > 1) { System.out.println(collisions.size()+" collisions detected at time "+t); } if (collisions.size() > 0) { for (i=0; i= winningHits) message2 = new ThrustText("Green hit wall "+winningHits+" times -- Blue wins!"); } else { blueLabel.setText("blue "+nf.format(++blueHits)); if (blueHits >= winningHits) message2 = new ThrustText("Blue hit wall "+winningHits+" times -- Green wins!"); } } switch (c1.normalObj) { case RIGHT_WALL: case LEFT_WALL: velo[1+offset] += -(1+elasticity)*vars[1+offset]; break; case TOP_WALL: case BOTTOM_WALL: velo[3+offset] += -(1+elasticity)*vars[3+offset]; break; } } else { // object-object collision // find the midpoint between the two impact points if (debug) System.out.println("object-object collision found"); c1.impactX = (c1.impactX + c2.impactX)/2; c1.impactY = (c1.impactY + c2.impactY)/2; addImpact(c1, velo); } // We could deal with other combinations here, but need to figure out how. // * Side collision between two objects // * Object hitting two walls in a corner // * Object rotates so corner A hits another object, corner C hits wall if (debug) System.out.println("special impact "+c1.object+" "+c1.normalObj); // delete these collisions from the original vector // NOTE: collisions.removeAll(m) is only in Java 1.2 while (m.size() > 0) { collisions.removeElement(m.lastElement()); m.removeElement(m.lastElement()); } } } public void addImpact(Collision cd, double[] velo) { // Calculate impact resulting from the given collision. // Modifies the passed-in array of changes to velocities for the bodies. double nx = cd.normalX; // n = normal vector pointing towards body double ny = cd.normalY; /* cross product in the plane: unit vectors i,j,k (ax,ay,0) x (bx,by,0) = k(ax by - ay bx) */ int objA,objB,offsetA,offsetB; double rax,ray,rbx,rby,Ia,Ib,ma,mb; double vax,vay,wa,vbx,vby,wb; double d,dx,dy,j; if (cd.normalObj < 0) { // wall collision //System.out.println("wall collision"); objB = cd.object; offsetB = 6*objB; if (gameMode) { this.nf.setMinimumFractionDigits(0); if (objB==0) { greenLabel.setText("green "+nf.format(++greenHits)); if (greenHits >= winningHits) message2 = new ThrustText("Green hit wall "+winningHits+" times -- Blue wins!"); } else { blueLabel.setText("blue "+nf.format(++blueHits)); if (blueHits >= winningHits) message2 = new ThrustText("Blue hit wall "+winningHits+" times -- Green wins!"); } } // normal is pointing in towards body B, so it is correct here. rbx = cd.impactX - bods[objB].x; // r = vector from cm to point of impact, p rby = cd.impactY - bods[objB].y; Ib = bods[objB].momentAboutCM(); mb = bods[objB].mass; vbx = vars[1+offsetB]; vby = vars[3+offsetB]; wb = vars[5+offsetB]; /* vb = old linear velocity of cm = (vbx, vby) mb = mass n = normal vector pointing towards body (length 1 here) vb2 = new linear velocity of cm = (vbx2,vby2) wb = old angular velocity = vars[5] rb = vector from cm to point of impact Ib = moment of inertia of body about cm wb2 = new angular velocity velocity of collision point = vb + w x rb -(1 + elasticity) (v1 + w1 x r).n j = ------------------------- n.n + (rp x n)^2 --- -------- M I */ // cross product r x n = (rx, ry, 0) x (nx, ny, 0) = (0, 0, rx*ny - ry*nx) j = rbx*ny - rby*nx; j = (j*j)/Ib + (1/mb); // cross product: w1 x r = (0,0,w) x (rx, ry, 0) = (-w*ry, w*rx, 0) j = -(1 + elasticity)*((vbx-rby*wb)*nx + (vby+rbx*wb)*ny) / j; double vx,vy,w; vx = nx*j/mb; vy = ny*j/mb; w = j*(rbx*ny - rby*nx)/Ib; //System.out.println("Impact add vx="+vx+" vy="+vy+" w="+w); // v2 = v1 + (j/M)n = new linear velocity velo[1+offsetB] += nx*j/mb; velo[3+offsetB] += ny*j/mb; // w2 = w1 + j(r x n)/I = new angular velocity velo[5+offsetB] += j*(rbx*ny - rby*nx)/Ib; //System.out.println("Velo is vx="+velo[1+offsetB]+" vy="+velo[3+offsetB]+" w="+velo[5+offsetB]); } else { // object-object collision // The vertex of body A is colliding into an edge of body B. // The normal points out from body B, perpendicular to the edge. objA = cd.object; objB = cd.normalObj; offsetA = 6*objA; offsetB = 6*objB; rax = cd.impactX - bods[objA].x; // ra = vector from A's cm to point of impact, p ray = cd.impactY - bods[objA].y; rbx = cd.impactX - bods[objB].x; // rb = vector from B's cm to point of impact rby = cd.impactY - bods[objB].y; Ia = bods[objA].momentAboutCM(); Ib = bods[objB].momentAboutCM(); ma = bods[objA].mass; mb = bods[objB].mass; nx = -nx; // reverse n so it points out from body A into body B ny = -ny; vax = vars[1+offsetA]; vay = vars[3+offsetA]; wa = vars[5+offsetA]; vbx = vars[1+offsetB]; vby = vars[3+offsetB]; wb = vars[5+offsetB]; /* ma = mass of body A n = normal vector pointing out from body A (length 1 here) j = impulse scalar jn = impulse vector va = old linear velocity of cm for body A va2 = new linear velocity of cm wa = old angular velocity for body A wa2 = new angular velocity ra = vector from body A cm to point of impact = (rax, ray) Ia = moment of inertia of body A about center of mass vab = relative velocity of contact points (vpa, vpb) on bodies vab = (vpa - vpb) vpa = va + wa x ra = velocity of contact point vab = va + wa x ra - vb - wb x rb -(1 + elasticity) vab.n j = ------------------------------------- 1 1 (ra x n)^2 (rb x n)^2 (--- + ---) + --------- + --------- Ma Mb Ia Ib Note that we use -j for body B. */ // cross product r x n = (rx, ry, 0) x (nx, ny, 0) = (0, 0, rx*ny - ry*nx) d = rax*ny - ray*nx; j = d*d/Ia; d = -rby*nx + rbx*ny; j += d*d/Ib; j += (1/bods[objA].mass) + (1/bods[objB].mass); // vab.n = (va + wa x ra - vb - wb x rb) . n // cross product: w x r = (0,0,w) x (rx, ry, 0) = (-w*ry, w*rx, 0) dx = vax + wa*(-ray) - vbx - wb*(-rby); dy = vay + wa*(rax) - vby - wb*(rbx); j = -(1+elasticity)*(dx*nx + dy*ny)/j; // v2 = v1 + j n / m = new linear velocity velo[1+offsetA] += j*nx/ma; velo[3+offsetA] += j*ny/ma; velo[1+offsetB] += -j*nx/mb; velo[3+offsetB] += -j*ny/mb; // w2 = w1 + j(r x n)/I = new angular velocity velo[5+offsetA] += j*(-ray*nx + rax*ny)/Ia; velo[5+offsetB] += -j*(-rby*nx + rbx*ny)/Ib; } } public void modifyObjects() { if (m_Animating) { double now = (double)System.currentTimeMillis()/1000; /* figure out how much time has passed since last simulation step */ /* last_time is used to figure how much real-time has passed since last simulation step */ /* sim_time is a cumulative time counter in "simulation" time */ double h; if (last_time < 0) { sim_time = 0; h = 0.05; // assume that a small time has passed at start } else { h = now - last_time; if (h == 0) { return; // does this ever happen? } // Deal with long delays here... This causes time slippage & animation will stutter // It will look like the animation "paused" during the delay, but I think its // better than having the animation do a huge discontinuous jump. if (h > 0.25) h = 0.25; } // record time of this simulation step last_time = now; int i = numVars; while (i-- > 0) old_vars[i] = vars[i]; // save variables solve(sim_time, h); // step forward by time h findAllCollisions(sim_time + h); if (collisions.size() > 0) { // use bisection method to find time of collision // See Numerical Analysis, 6th Edition, Burden & Faires, page 49. i = numVars; while (i-- > 0) // restore variables to beginning of period vars[i] = old_vars[i]; int ctr = 0; double a = sim_time; double b = sim_time + h; double p; Vector saveCollisions = collisions; collisions = new Vector(4*numBods); // Binary search to time of collision. // Do this by finding time a & b, such that no collision at time a, // and at least one collision at time b, and (b-a) 0) { // situation is like this: // 0 + + number of collisions // a------- p -------- b time // so p becomes the new 'b' b = p; i = numVars; while (i-- > 0) vars[i] = old_vars[i]; // reset vars to time a saveCollisions = collisions; collisions = new Vector(4*numBods); } else { // situation is like this: // 0 0 + number of collisions // a------- p -------- b time // so p becomes the new 'a' a = p; i = numVars; while (i-- > 0) old_vars[i] = vars[i]; // save variables at new time a } } if (ctr >= 12) System.out.println("*** COULD NOT RESOLVE COLLISION ***"); /* System.out.println("%%%%%%%%%%%%%%% start of collision %%%%%%%%%%%%%%%%%"); System.out.println("Time="+sim_time+" vx="+vars[1]+" vy="+vars[3]+" vw="+vars[5]); System.out.println("x="+vars[0]+" y="+vars[2]+" w="+vars[4]); */ printEnergy(0, "pre-collision "); if (debug && saveCollisions.size() > 1) { System.out.println(saveCollisions.size()+" collisions detected"); } // Note that this collision corresponds to time b, but we are at // time a. double[] velo = new double[numVars]; // NOTE: ??? avoid allocation by keeping this around? for (i=0; i (double)maxWidth) || (total*graphFactor < 0.2*(double)maxWidth)) { if (total*graphFactor > (double)maxWidth) graphFactor = 0.75*(double)maxWidth/total; else graphFactor = 0.75*(double)maxWidth/total; double power = Math.pow(10,Math.floor(Math.log(total)/Math.log(10))); double logTot = total/power; // logTot should be in the range from 1.0 to 9.999 // choose a nice delta for the numbers on the chart if (logTot >= 8) graphDelta = 2; else if (logTot >= 5) graphDelta = 1; else if (logTot >= 3) graphDelta = 0.5; else if (logTot >= 2) graphDelta = 0.4; else graphDelta = 0.2; graphDelta *= power; //System.out.println("rescale "+total+" "+logTot+" "+power+" "+graphDelta); } // draw a bar chart of the various energy types. g.setColor(Color.darkGray); g.fillRect(w, top + TOP_MARGIN, w2 = (int)(0.5+pe*graphFactor), HEIGHT); g.setColor(Color.lightGray); w += w2; g.fillRect(w, top + TOP_MARGIN, w2 = (int)(0.5+re*graphFactor), HEIGHT); g.setColor(Color.gray); w += w2; g.fillRect(w, top + TOP_MARGIN, w2 = (int)(0.5+te*graphFactor), HEIGHT); if (graphFont == null) { graphFont = new Font("SansSerif", Font.PLAIN, 10); } g.setFont(graphFont); FontMetrics graphFM = g.getFontMetrics(); graphAscent = graphFM.getAscent(); // draw in the numeric scale for the bar chart. nf.setMaximumFractionDigits(4); nf.setMinimumFractionDigits(0); g.setColor(Color.black); double scale = 0; do { int x = left + LEFT_MARGIN+(int)(scale*graphFactor); int y = top + TOP_MARGIN; g.drawLine(x, y+HEIGHT/2, x, y+HEIGHT+2); String s = nf.format(scale); int textWidth = graphFM.stringWidth(s); g.drawString(s, x -textWidth/2, y+HEIGHT+graphAscent+3); scale += graphDelta; } while (scale < total); } public void adjustmentValueChanged(AdjustmentEvent e) { if (e.getAdjustable() == dampSlider.scroll) { damping = dampSlider.getValue(); } else if (e.getAdjustable() == elasticSlider.scroll) { elasticity = elasticSlider.getValue(); } else if (e.getAdjustable() == gravitySlider.scroll) { gravity = gravitySlider.getValue(); } else if (e.getAdjustable() == thrustSlider.scroll) { thrust = thrustSlider.getValue(); int i; for (i=0; i