import java.applet.*; import java.awt.*; import java.util.*; import java.awt.event.*; // This implements runnable because it can be threaded. Whenever a shot is // in progress, we create a new thread for the projectile object. public class Projectile implements Runnable { // the zero of x0 means it is the scale of a dimensionless variable. // init means it is the initial value of that variable. // We assume we start as x=y=0 and t=0. double x, y, x0, y0; double vx, vy, vx0, vy0; double velocityInit, angle; // These are the initial values of velocity and angle. double t, t0; double dt = 0.008; // This timestep is set by hand. // m dv/dt + k*v + m g = 0 (g<0) // dv/dt + kappa*v + gamma = 0 // kappa = k/m, gamma = g. I separate them b/c visual controls in the applet // will change k and m while the constants kappa and gamma remain fixed while // a shot is in progress. Otherwise, someone could change the shot in midair. double kappa, gamma; double maxHeight; // Incidental variable to record the max height reached during a shot. static final double gravity = -9.81; double k = 0.0; // Initial wind resistance. double m = 10.0; // Initial ball mass. int sleeptime = 20; // milliseconds the thread waits between calculations. int iMarkFrequency = 8; // How often we leave a trail. boolean hit; // Whether the ball is flying or has hit the ground. boolean please_stop; // Whether we want a running thread to consider stopping. Thread animator; // This represents a thread. It will be null when there is no shot. Vector Listeners; // When something (a graph) asks we inform them of changes // in the ball position, we add it to the list of listeners. public Projectile() { hit = true; please_stop = true; Listeners = new Vector(); velocityInit = 60.0; angle = 0.9*Math.PI/4; } public void setSleepTime(int sleep) { if ((sleep>0) && (sleep<1000)) { sleeptime = sleep; } else { System.out.println("The sleep time needs to be between 0 and 1000."); } } public void setMarkFrequency(int mark) { if (mark>0) { iMarkFrequency = mark; } else { System.out.println("The mark frequency should be greater than zero."); } } public void setNumSteps(int steps) { if ((steps>5) && (steps < 10000)) { dt = 1/((double)steps); } else { System.out.println("The number of steps should be between 5 and 10000."); } } // When a thread is created for this object, it sits forever in this method. // This method takes a projectile at t=0 and moves it until it crashes to y=0. public void run() { ProjectileListener tListen; boolean mark; // Run until the ball hits or someone asks us to stop. // please_stop is a global so someone else can change its value. // It acts as a flag to the thread. while (!please_stop && !hit) { mark = false; if ( (Math.round(t/dt) % iMarkFrequency) == 0) mark = true; // First tell the listeners where the ball is now. for (int i=0; imaxHeight) maxHeight = y; if (y<0.0 || x<0) hit = true; if (y<0.0) y = 0.0; // So we don't draw below the line. A little lie. try { Thread.sleep(sleeptime); } catch (InterruptedException e) { ; } } if (hit) { t = t-dt; // This mess is here just so we can tell the listeners we are done // firing and give them some info. It should be described below. double[] shotStats = new double[4]; shotStats[0] = x*x0; shotStats[1] = Math.sqrt(vx*vx*vx0*vx0+vy*vy*vy0*vy0); shotStats[2] = maxHeight*y0; shotStats[3] = t*t0; for (int i=0; i0.0) { ekt = Math.exp(-kappa*time); vy = (1+gk)*ekt-gk; y = (1+gk)*(1-ekt)/kappa-gk*time; vx = 1*ekt; x = (1-ekt)/kappa; } else { vy = 1-gamma*time; y = t-0.5*gamma*time*time; vx = 1; x = time; } } // This method doesn't use any information from the projectile object. // Since it doesn't access the object, we can make it a static method. // It does, however, calculate the position of a projectile you describe. // The equations are the same as those used in paint(); public static double[] calcPosition(double kap, double gam, double time, double vxScale, double vyScale, double tScale) { double gk, ekt, tt, xScale, yScale; double[] coord = new double[4]; gk = gam/kap; tt = time/tScale; xScale= vxScale*tScale; yScale = vyScale*tScale; if (kap>0.0000001) { ekt = Math.exp(-kap*tt); coord[3] = vyScale*((1+gk)*ekt-gk); coord[1] = yScale*((1+gk)*(1-ekt)/kap-gk*tt); coord[2] = vxScale*ekt; coord[0] = xScale*(1-ekt)/kap; } else { coord[3] = vyScale*(1-gam*tt); coord[1] = yScale*(tt-0.5*gam*tt*tt); coord[2] = vxScale; coord[0] = xScale*tt; } return coord; } // If someone wants us to tell them where the projectile is, they have to // have the methods described below, implement Projectile.ProjectileListener, // and add themselves as a listener. public interface ProjectileListener { public void setPosition(double x,double y,double vx,double vy,double time,boolean mark); // Initial velocity and angle. public void beginFiring(double velocity, double angle, double[] endStats); // shotStats[0] endDistance [1] end velocity [2] maxHeight [3] endTime // endStats[0] kappa [1] gamma // endStats[2] vx0 [3] vy0 [4] t0 -- all three scalings. public void endFiring(double[] shotStats); } public void addListener(ProjectileListener ting) { Listeners.addElement(ting); } // When the browser shows the applet on screen, it calls the start() // method of every suspended thread. It is because threads may be suspended // that we don't initialize the projectile at the start of the run method. // It may need to be stopped and called again. public void start() { if ((animator==null) && (hit == false)) { please_stop = false; animator = new Thread(this); animator.start(); } } public void setVelocity(double dVal) { if (dVal>0) velocityInit = dVal; } public void setAngle(double dVal) { if (dVal>0 && dVal<=90) angle = dVal; } public void setMass(double dVal) { if (dVal>0.1) { m = dVal; } } public void setResistance(double dVal) { if (dVal>=0.0) { k = dVal; } } // This is where the applet asks us to fire the bullet. public void fire() { // If we had a bullet mid flight, we ask it to stop by setting // please_stop. The method animator.join() will not return until // the thread has stopped. if (animator != null) { please_stop = true; try { animator.join(1000); // wait 1000 milliseconds } catch (Exception e) { ; } } if (animator==null) { hit = false; // First tell our listeners we are about to begin. ProjectileListener tListen; // These are scales for the dimensionless vx and vy. vx0 = velocityInit*Math.cos(angle); vy0 = velocityInit*Math.sin(angle); vx = 1; vy = 1; t = 0; // The scale for the time is the time it would take a Pi/4 shot // without air to land. t0 = 2*vy0/Math.abs(gravity); x0 = vx0*t0; y0 = vy0*t0; x = 0; y = 0; // Now we set the actual variables used during the computation. // This leaves the user free to play with the slider controls // changing mass, velocity, angle while the shot is flying. kappa = k*t0/m; gamma = -gravity*t0/vy0; maxHeight = 0; // Will record maximum height of flight. double[] endStats = new double[6]; endStats[0] = kappa; endStats[1] = gamma; endStats[2] = vx0; endStats[3] = vy0; endStats[4] = t0; for (int i=0; i