/** BouncingBalls.java * @author Robert M. Keller * @version 1 * @date 8 December 1996 * * purpose: Multithreading demonstration using bouncing balls * * One or more Threads are generated by clicking the 'New' button. * Each Thread is in the form of a derived class here represented by Ball. * The balls update their positions and velocities independently of one * another. The applet thread knows which balls are active and draws them in * the window. Threads can be suspended and resumed by clicking the mouse * over the corresponding ball. * * Here is how a new thread gets created in this code: * * 1. User clicks 'New' button. * * 2. Method 'action' is called by the applet. * * 3. The event is recognized as coming from the 'New' button and * method 'newBall' is called. * * 4. new Ball(....) is called to create a Ball (and thus a Thread). * * 5. run() is called for the new Ball (Thread) thus created. * * 6. The applet is resumed and the new Thread is now running concurrently. * * Only 4 and 5 are essential. The rest are just how we do it here. * * Also, one Ball is always created at the very start by a call to newBall. * * This program uses the 'extends Thread' technique of thread creation. * The other method is the 'implements Runnable' technique. The difference * is described in a note at the end of the program. Also, there is an * alternate implementation using that technique called AltBouncingBalls. * * NOTE: This does not intend to demonstrate the best way to achieve * animation, especially in the case of synchronized objects. * It merely tries to show the concept of independent threads. **/ /* $Id: BouncingBalls.java,v 1.39 1996/12/09 08:11:57 keller Exp keller $ */ import java.applet.*; // applet classes import java.awt.*; // Abstract Window Toolkit classes import java.util.*; // utility classes import java.lang.Thread; // Thread class import java.lang.Math; public class BouncingBalls extends Applet implements Runnable { static public Color backgroundColor = Color.white, foregroundColor = Color.blue, borderColor = Color.red; static public int thickness = 5; // border thickness static public int diameter = 30; // ball diameter static int xMargin = 0, yMargin = 60; // margins in window int xMax, yMax; // limits to ball coordinates Vector balls; // a place to keep balls for drawing static public Font MainFont = new Font("Helvetica", Font.BOLD, 18); Image image; Graphics graphics; // graphics buffer int width, height; // image size Thread mythread; // thread of the applet static public double displayDelay = 10; // delay between updates (ms) static public int minDelay = 5; static public int maxDelay = 99; Slider delaySlider; // slider control static double deltaY0 = 0; // ball launch Y velocity static public double deltaX0 = 5; // ball launch X velocity static public int minDeltaX0 = 1; static public int maxDeltaX0 = 25; Slider deltaX0Slider; // slider control static int x0 = thickness; // ball launch positions static int y0 = thickness; double Gravity = 0.1; // gravity effect (additive) double Friction = 0.999; // friction effect (multiplicative) static double deltaMin = 0.5; // ball slower than this dies static public double xBounce = 0.7; // fraction of velocity left // following bounces. static public double yBounce = 80; // divide yBounce by 100. static public int minYbounce = 50; static public int maxYbounce = 99; Slider yBounceSlider; // slider control Button quitButton = new Button("Quit"); // button to quit the applet Button newButton = new Button("New"); // button to start new thread TextField status = new TextField(13); // field for status messages /** * Initialize the applet. **/ public void init() { balls = new Vector(); // keep Balls here width = size().width; // width of drawing field height = size().height-yMargin; // height int space = diameter + thickness; // temporary variable xMax = Integer.parseInt(getParameter("width")) - space; yMax = Integer.parseInt(getParameter("height")) - yMargin - space; setLayout(new FlowLayout(FlowLayout.LEFT)); // layout buttons from left setBackground(backgroundColor); // set the background color image = createImage(width, height); // create image buffer graphics = image.getGraphics(); graphics.setFont(MainFont); quitButton.setFont(MainFont); // make buttons newButton.setFont(MainFont); add(newButton); status.setFont(MainFont); status.setEditable(false); add(status); // make sliders deltaX0Slider = new Slider(this, "Vel", MainFont, deltaX0, minDeltaX0, maxDeltaX0, 2); yBounceSlider = new Slider(this, "Bounce", MainFont, yBounce, minYbounce, maxYbounce, 2); delaySlider = new Slider(this, "Delay", MainFont, displayDelay, minDelay, maxDelay, 2); add(quitButton); } /** * Run the applet. **/ public void run() { newBall(); while( true ) { cycle(); sleep(); } } /** * Do this every cycle of the applet. **/ void cycle() { graphics.clearRect(0, 0, width, height); // clear the image drawBorder(); // redraw the border drawBalls(); // draw the balls repaint(); // paint the image } /** * drawBorder draws a border around the ball bouncing area. **/ void drawBorder() { graphics.setColor(borderColor); graphics.fillRect(0, 0, width, thickness); graphics.fillRect(0, 0, thickness, height); graphics.fillRect(width-thickness, 0, thickness, height); graphics.fillRect(0, height-thickness, width, thickness); } /** * Create new Ball thread and start it. **/ void newBall() { Ball ball = new Ball(this); // Create ball and tell it about applet. balls.addElement(ball); // Remember the ball for drawing. ball.start(); // Start the thread. } /** * Remove Ball thread from the record (actual kill is done by Ball itself). **/ void killBall(Ball ball) { balls.removeElement(ball); } /** * drawBalls calls the draw method for each active ball. **/ void drawBalls() { graphics.setColor(foregroundColor); for( Enumeration e = balls.elements(); e.hasMoreElements(); ) { ((Ball)(e.nextElement())).draw(); } } /** * Suspend all balls. **/ void suspendBalls() { for( Enumeration e = balls.elements(); e.hasMoreElements(); ) { ((Ball)(e.nextElement())).Suspend(); } } /** * Resume all balls. **/ void resumeBalls() { for( Enumeration e = balls.elements(); e.hasMoreElements(); ) { ((Ball)(e.nextElement())).Resume(); } } /** * Find ball near position (x, y) and suspend it, * or resume it if it is suspended. **/ void findAndSuspend(int x, int y) { x -= (xMargin + thickness); // account for offset image y -= (yMargin + thickness); for( Enumeration e = balls.elements(); e.hasMoreElements(); ) { Ball ball = (Ball)(e.nextElement()); if( x >= ball.x && x < ball.x + diameter && // Found a ball y >= ball.y && y < ball.y + diameter ) // at this x, y { if( ball.suspended ) ball.Resume(); else ball.Suspend(); return; } } } /** * action handles events targeted for buttons, etc. **/ public boolean action(Event event, Object arg) { if( event.target == newButton ) { newBall(); // Create new ball. } else if( event.target == quitButton ) { destroy(); // Handle quit. } return super.action(event, arg); // Pass event to super. } /** * Set mode or perform action when user moves a slider **/ public boolean handleEvent(Event event) { if( event.target == deltaX0Slider.scroll ) { deltaX0 = deltaX0Slider.getValue(); // Vel slider return true; } if( event.target == yBounceSlider.scroll ) { yBounce = yBounceSlider.getValue(); // Bounce slider return true; } if( event.target == delaySlider.scroll ) { displayDelay = delaySlider.getValue(); // Delay slider return true; } return super.handleEvent(event); // Delegate all other actions to super. } /** * mouseDown is called when the mousebutton is pressed. **/ public boolean mouseDown(Event e, int x, int y) { findAndSuspend(x, y); return true; } /** * update is implicitly called by repaint(). It calls paint(Graphics). **/ public void update(Graphics g) { paint(g); } /** * paint(Graphics) is called by update(Graphics). **/ public void paint(Graphics g) { g.drawImage(image, 0, yMargin, null); } /** * start is called when the applet is entered, or re-entered. **/ public void start() { if( mythread == null ) // no main thread created yet { mythread = new Thread(this); // Create one mythread.start(); // and start it. } else { mythread.resume(); // Resume main thread. resumeBalls(); // Resume the balls. } } /** * stop is called when the applet is left. **/ public void stop() { mythread.suspend(); // Suspend the main thread. suspendBalls(); // Suspend the balls. } /** * sleep makes the applet sleep for argument milliseconds. **/ public static void sleep(int delay) { try { Thread.sleep(delay); } catch(InterruptedException e) { } } /** * sleep a standard amount **/ public static void sleep() { sleep((int)displayDelay); } /** * report reports on certain events. * This is declared synchronized so messages don't get mingled. **/ synchronized void report(String event) { status.setText(event); } } // class BouncingBalls /** * Ball class represents ball's state information **/ class Ball extends Thread { BouncingBalls app; // this ball's applet int diameter; // this ball's diameter double x, y; // this ball's coordinates double deltaX, deltaY; // this ball' velocities String myNumber; // ball's number as a string static int numberBalls = 0; // number of balls generated int xSignChange, ySignChange; // used for displaying bounce boolean suspended; // whether thread is suspended /** * ball constructor (implicitly constructs Thread, its parent) **/ public Ball(BouncingBalls app) { this.app = app; myNumber = Integer.toString(++numberBalls); // generate ball number app.report(myNumber + " created"); this.diameter = app.diameter; // set ball diameter x = app.x0; // set position to launch position y = app.y0; deltaX = app.deltaX0; // set velocity to launch velocities deltaY = app.deltaY0; xSignChange = 2; // used for bouncing (see 'move' method) ySignChange = 2; } /** * run is called by start() for the Ball thread. **/ public void run() { while( true ) { move(); // move the ball app.sleep(); // sleep } } /** * move moves the ball one time step. * * On each step, the x and y positions are incremented by adding * the respective deltas, which represent velocities. Then the * the velocities themselves are modified to take into account * gravity and friction (e.g. air and surface friction). * A bounce is detected when x or y is sufficiently close to a * wall or the floor or ceiling. Then the sign of the velocity * is changed multiplied by the bounce factor. However, the * ball may still be sufficiently close on the next time step as * well, so we wait one additional time step before checking again * whether the ball is close. This is implemented by the signChange * variables which count the number of consecutive closeness detections. **/ void move() { if( suspended ) return; // don't update if suspended x += deltaX; // increment offset y += deltaY; deltaY += app.Gravity; // gravity effect deltaY *= app.Friction; deltaX *= app.Friction; // friction effect if( Math.abs(deltaX) < app.deltaMin ) { // die; velocity too low die(); } if( x >= app.xMax || x <= app.thickness ) { // bounce off wall if( xSignChange++ > 1 ) // allow sufficient steps { // to clear edge deltaX = -deltaX * app.xBounce; xSignChange = 0; } } else xSignChange = 2; if( y >= app.yMax || y < app.thickness ) { // bounce off floor or ceiling if( ySignChange++ > 1 ) { deltaY = -deltaY * (app.yBounce/100.); // allow sufficient time ySignChange = 0; // to clear edge } } else ySignChange = 2; } /** * Draw this ball using the applet's graphics. **/ void draw() { int x = (int)Math.rint(this.x); int y = (int)Math.rint(this.y); int ystring = y+app.graphics.getFontMetrics().getHeight(); app.graphics.setColor(app.foregroundColor); app.graphics.fillOval(x, y, diameter, diameter); app.graphics.setColor(app.backgroundColor); app.graphics.drawString(myNumber, x+5, ystring); } /** * Kill this thread **/ void die() { app.killBall(this); // Let the applet know. app.report(myNumber + " died"); // Show on the screen. stop(); // Execution stops here. } /** * Suspend this ball * We can't over-ride 'suspend' and 'resume' since they are declared final. **/ void Suspend() { suspend(); // suspend thread app.report(myNumber + " suspended"); suspended = true; } /** * resume this ball **/ void Resume() { resume(); // resume thread app.report(myNumber + " resumed"); suspended = false; } } // class Ball /** * A Slider is a combination of a Label, a Scrollbar, and a TextField. **/ class Slider { double Value; // Value maintained by the slider Label label; // label for the slider Scrollbar scroll; // the slider itself TextField field; // field showing value of slider /** * Create a slider. **/ Slider(Applet app, // Applet in which to place slider String lab, // Label for the slider Font font, // Font for Label and TextField double initial, // Initial value int min, // minimum value of the slider int max, // maximum int fieldSize) // number of characters in TextField { label = new Label(lab); label.setFont(font); app.add(label); // Add the label. scroll = new Scrollbar(Scrollbar.VERTICAL, (int)initial, 100, min, max); scroll.setFont(font); app.add(scroll); // Add the scrollbar. this.Value = initial; // Initialize the value. field = new TextField(fieldSize); field.setText(new Double(initial).toString()); field.setFont(font); field.setEditable(false); app.add(field); // Add the TextField. } /** * Set the value of the slider programmatically. **/ void setValue(double Value) { this.Value = Value; field.setText(new Double(Value).toString()); } /** * Update the value of the slider when the scrollbar is adjusted * and return the value (must be called by handleEvent). **/ double getValue() { setValue(scroll.getValue()); return Value; } } // class Slider /* Notes: There are two methods of thread implementation: extends Thread implements Runnable This program shows the first method, which is less complicated, involving fewer steps. If were to instead use the second method, we would declare the Ball class as: class Ball implements Runnable Class Ball should include a reference to what will be its Thread: Thread thread; Then to create a new thread, we would have the applet call: void Ball() { Ball ball = new Ball(this); // Create ball and tell it about applet. Thread thread = new Thread(ball); // Create thread ball.thread = thread; // Tell ball about it balls.addElement(ball); // Remember the ball for drawing. thread.start(); // Start the thread. } Class Ball would still have to implement method 'run', which over-rides that in class Runnable. It would be pretty much the same as shown here, except that we call stop(), suspend(), resume() on the thread rather than on the Ball itself. */