Solution for Programmming Exercise 7.4
This page contains a sample solution to one of the exercises from Introduction to Programming Using Java.
Exercise 7.4:
For this problem, you will need to use an array of objects. The objects belong to the class MovingBall, which I have already written. You can find the source code for this class in the file MovingBall.java. A MovingBall represents a circle that has an associated color, radius, direction, and speed. It is restricted to moving in a rectangle in the (x,y) plane. It will "bounce back" when it hits one of the sides of this rectangle. A MovingBall does not actually move by itself. It's just a collection of data. You have to call instance methods to tell it to update its position and to draw itself. The constructor for the MovingBall class takes the form
new MovingBall(xmin, xmax, ymin, ymax)
where the parameters are integers that specify the limits on the x and y coordinates of the ball. In this exercise, you will want balls to bounce off the sides of the applet, so you will create them with the constructor call
new MovingBall(0, getWidth(), 0, getHeight())
The constructor creates a ball that initially is colored red, has a radius of 5 pixels, is located at the center of its range, has a random speed between 4 and 12, and is headed in a random direction. There is one problem here: You can't use this constructor until the width and height of the component are known. It would be OK to use it in the init() method of an applet, but not in the constructor of an applet or panel class. If you are using a panel class to display the ball, one slightly messy solution is to create the MovingBall objects in the panel's paintComponent() method the first time that method is called. You can be sure that the size of the panel has been determined before paintComponent() is called. This is what I did in my own solution to this exercise.
If ball is a variable of type MovingBall, then the following methods are available:
- ball.draw(g) -- draw the ball in a graphics context. The parameter, g, must be of type Graphics. (The drawing color in g will be changed to the color of the ball.)
- ball.travel() -- change the (x,y)-coordinates of the ball by an amount equal to its speed. The ball has a certain direction of motion, and the ball is moved in that direction. Ordinarily, you will call this once for each frame of an animation, so the speed is given in terms of "pixels per frame". Calling this routine does not move the ball on the screen. It just changes the values of some instance variables in the object. The next time the object's draw() method is called, the ball will be drawn in the new position.
- ball.headTowards(x,y) -- change the direction of motion of the ball so that it is headed towards the point (x,y). This does not affect the speed.
These are the methods that you will need for this exercise. There are also methods for setting various properties of the ball, such as ball.setColor(color) for changing the color and ball.setRadius(radius) for changing its size. See the source code for more information.
For this exercise, you should create an applet that shows an animation of balls bouncing around on a black background. Use a Timer to drive the animation. (See Subsection 6.5.1.) Use an array of type MovingBall[] to hold the data for the balls. In addition, your program should listen for mouse and mouse motion events. When the user presses the mouse or drags the mouse, call each of the ball's headTowards() methods to make the balls head towards the mouse's location. My solution uses 50 balls and a time delay of 50 milliseconds for the timer.
Here is my solution. Try clicking and dragging on the applet:
The solution to this exercise is not very long, although it is rather complicated conceptually and it might take time to get used to working with arrays of objects.
My program uses a nested class named Display, defined as a subclass of JPanel as a drawing surface where the moving balls are drawn. The main class is a subclass of JApplet, but it just uses an object of type Display as its content pane. Here, I only discuss the programming of the Display class.
An instance variable of type MovingBall[] is needed to hold the data for the balls. This instance variable can be declared as
MovingBall[] balls;
As discussed in the exercise, it is not possible to create the ball objects in the constructor of the Display class, so I create the array and the ball objects that it contains in the paintComponent(). This should only be done once, the first time paintComponent() is called. To achieve this, I test whether balls == null at the begining of the paintComponent() method. If not, then the objects have already been created; if so, then this is the first time paintComponent() is being called, and the objects must be created. The array object is created with a statement of the form "balls = new MovingBall[ballCount];" where ballCount is the number of balls. However, this just gives an array filled with null values. There aren't any balls yet. Each of the balls must be created with a call to the constructor from the MovingBall class:
if (balls == null) { balls = new MovingBall[ ballCount ]; // Create the array for (int i = 0; i < balls.length; i++) { // Create each of the ball objects. The parameters specify // that the balls are restricted to moving within the bounds // of the panel. balls[i] = new MovingBall(0, getWidth(), 0, getHeight()); } }
The paintComponent() method must draw the balls. I decided to put the code for moving the balls into the paintComponent() method as well. This means that each ball will move in its current direction of motion by a small amount each time paintComponent() is called. The i-th ball can be moved by calling its travel() method with the command "balls[i].travel()". It can be drawn in the graphics context g by calling its draw() method with the command balls[i].draw(g);". To apply these commands to every ball in the array, we need a for loop
for (int i = 0; i < balls.length; i++) { balls[i].travel(); balls[i].draw(g); }
An alternative to this would be the for-each loop:
for ( MovingBall ball : balls ) { ball.travel(); ball.draw(g); }
Similarly, in the mousePressed() and mouseDragged() routine, we need a for loop (or for-each loop) to tell each ball to head towards the location of the mouse, (evt.getX(),evt.getY()):
for (int i = 0; i < balls.length; i++) { balls[i].headTowards(evt.getX(),evt.getY()); }
My program uses anonymous inner classes for the mouse listener and mouse motion listener. We also need a timer to drive the animation. Since the balls move every time paintComponent() is called, the response to an action event from the time is simply to call repaint(), which will in turn cause paintComponent() to be called. The action listener for the timer is also defined by an anonymous inner class:
Timer timer = new Timer(millisecondsPerFrame, new ActionListener() { // This timer will drive the animation by calling repaint() // at periodic intervals. public void actionPerformed(ActionEvent evt) { repaint(); } }); timer.start();
That's really all there is to it. You might want to try variations like giving the balls random colors or sizes. This can be done when the ball objects are created in the paintComponent() method. In my program, I decided to use applet parameters to make it possible to customize the applet by specifying the number of balls and by setting the speed at which the animation plays. Recall that applet parameters are specified in the <applet> tag on the web page. The following applet tag specifies and applet fewer balls, moving more slowly, than the default:
<applet code="BallisticBalls.class" archive="BallisticBalls.jar" width=400 height=300> <param name="frameTime" value="25"> <param name="ballCount" value="100"> </applet>
The param with name "ballCount" specifies the number of balls in the applet. The param with name "frameTime" specifies the number of milliseconds to use for each frame of the animation. You can look at the source code for the applet, below, to see how I use these params. The method getIntParam() demonstrates how to get an integer value from an applet param.
import java.awt.*; import java.awt.event.*; import javax.swing.*; /** * This applet shows an animation of red balls moving on a black * background. The balls "bounce" off the sides of the applet. * The number of balls can be set as the value of an applet * param with name "ballCount". The default number is 25. * The number of milliseconds per frame can be set as the value of an * applet parameter with name "frameTime". The default is * 50 milliseconds. * * If the user clicks on the applet, or drags the mouse on * the applet, all the balls head towards the mouse location. * * The "balls" are represented by objects of type MovingBall, which * is defined in the file MovingBall.java. * * This class also contains a main() routine that allows the classprogram * to be run as a stand-alone application. */ public class BallisticBalls extends JApplet { /** * main() routine simply opens a window that uses an object of * type Display as its content pane, where Display is a static * nested class inside this class. */ public static void main(String[] args) { JFrame window = new JFrame("Ballistic Balls"); Display content = new Display(50,50); window.setContentPane(content); window.pack(); window.setLocation(100,100); window.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); window.setResizable(false); window.setVisible(true); } /** * The init() method of the applet uses an object of type Display * as the content pane of the applet. It also process the * applet params named "ballCount" and "frameTime", if present, * and uses their values to configure the Display. */ public void init() { int millisecondsPerFrame; // Time between frames in animation. int ballCount; // Number of MovingBalls that are used. try { // Try to read the value of millisecondsPerFrame from // an applet parameter named "frameTime". If it is not // present or is not a legal value, an error will occur, // and the default value of 50 will be used. String str = getParameter("frameTime"); millisecondsPerFrame = Integer.parseInt(str); if (millisecondsPerFrame <= 0) millisecondsPerFrame = 50; } catch (NumberFormatException e) { millisecondsPerFrame = 50; // Use default value. } try { // Try to read the value of ballCount from // an applet parameter named "ballCount". If it is not // present or is not a legal value, an error will occur, // and the default value of 50 will be used. String str = getParameter("ballCount"); ballCount = Integer.parseInt(str); if (ballCount <= 0) ballCount = 50; } catch (NumberFormatException e) { ballCount = 50; // Use default value. } setContentPane( new Display(ballCount, millisecondsPerFrame) ); } // end init(); /** * The nested class Display does all the work of the program. * It represents the drawing area in which the balls move. */ private static class Display extends JPanel { MovingBall[] balls; // An array to hold the balls. This will be // null until the first time paintComponent() // is called. int ballCount; // Number of balls requested in the constructor. /** * Constructor sets the background color (black) and preferred size (400-by-400) * of the panel. It sets up mouse listeners and creates and starts a timer * that will drive the motion of the balls. * @param ballCount the number of balls that should be used * @param millisecondsPerFrame the time between frames; this becomes * the delay time of the timer that drives the animation. */ Display(int ballCount, int millisecondsPerFrame) { setBackground(Color.BLACK); setPreferredSize( new Dimension(400,400) ); this.ballCount = ballCount; addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent evt) { // The user has clicked on the panel. Tell all the // balls to head towards the location of the mouse. for (int i = 0; i < balls.length; i++) balls[i].headTowards(evt.getX(), evt.getY()); } }); addMouseMotionListener(new MouseMotionAdapter() { public void mouseDragged(MouseEvent evt) { // The user has dragged the mouse on the panel. Tell all // the balls to head towards the location of the mouse. for (int i = 0; i < balls.length; i++) balls[i].headTowards(evt.getX(), evt.getY()); } }); Timer timer = new Timer(millisecondsPerFrame, new ActionListener() { // This timer will drive the animation by calling repaint() // at periodic intervals. public void actionPerformed(ActionEvent evt) { repaint(); } }); timer.start(); } // end constructor /** * The paint component method moves all the balls along their trajectories * (by calling ball.travel() for each ball) and draws all the balls (by * calling ball.draw(g) for each ball). Thus, the balls move each time * paintComponent() is called, and to drive the animation, it is only necessary * to call repaint() over-and-over. */ public void paintComponent(Graphics g) { super.paintComponent(g); if (balls == null) { balls = new MovingBall[ ballCount ]; // Create the array for (int i = 0; i < balls.length; i++) { // Create each of the ball objects. The parameters specify // that the balls are restricted to moving within the bounds // of the panel. NOTE: This is done in paintComponent() // because the size of the panel has not yet been set when the // constructor is called. balls[i] = new MovingBall(0, getWidth(), 0, getHeight()); } } /* Tell each ball to move. It moves an amount depending on its current direction and speed, and it will "bounce" off the side of the applet if necessary. Then the ball is told to draw itself in the graphics context g. */ for (int i = 0; i < balls.length; i++) { balls[i].travel(); balls[i].draw(g); } } // end paintComponent() } // end nested class Display } // end class BallisticBalls