Lab 2 -- Ball World
Lab 2 -- Ball World
The purpose of this lab is to write a simple animation program. We'll create a window containing a set of bouncing balls. The lab will illustrate the use of graphics in Java and explore the power of inheritance and polymorphism.The lab is structured around the "Model View Controller" paradigm, which I will describe briefly below. The idea is to separate to program components responsible for the actual computation from those related to the user interface. You'll have a controller class called Ballworld and a view class called BWFrame. The model is comprised of two classes, Ball and BallDispatcher. The ball dispatcher is responsible for managing a set of balls and for responding to the timer which triggers the animation. There is also a Ball class which will be instantiated once for each ball you create for the animation.
The Model View Controller paradigm
A GUI application can be divided into three components:- A model, which is the application with no user interface,
- A view, which is the user interface, and
- A controller, which connects the view to the model.
The controller has the model and view as its data fields. Its constructor creates the model and view objects and installs the action listeners in the view.
That's the framework we'll be using for the lab. Now we can go ahead and describe the application.
Constructing an animation
Consider the little "flip-book" cartoons you may have made as a child. To make something appear to move, one draws a picture of the object, and then another with the object slightly displaced. And then another picture with the object displaced again and so on. The faster we flip through the pages, the faster the animation progresses.We don't have a stack of papers here. We have a single screen. But we can draw really fast on that single piece of electronic paper. So let's see what we'd have to do to achieve the same effect:
- Erase the screen.
- Draw the object on the screen.
- Move the object to a new location, without redrawing it.
- Wait a specified period of time.
- Do steps 1-4 again.
How can this repeatable pattern be implemented? Java provides an object called a Timer which acts like a countdown timer. When the timer is started, it gets an initial time value, which it counts down. When the timer hits zero ("ticks"), it generates an Action Event, then resets itself. Another object (or several) can be designated as its ActionListener. When the timer ticks, the actionPerformed method in the listener is called.
So, what do we need to create an animation?
- Create a Timer that ticks at some rate.
- Whenever the Timer ticks,
- Erase the screen.
- Draw an object (or objects) on the screen.
- Move the object(s) to a new location, without redrawing it (them).
The pieces of the puzzle
What sort of objects do we need? Let's look at the objects and what they need to do:- A frame object
- Has a place to draw on.
- Can erase itself
- A ball object
- Can draw itself
- Can move itself
- A Timer object
- Can be set for a specific tempo
- Ticks at regular intervals, triggering an ActionEvent each time
- A dispatcher object
- Responds to timer ticks
- Keeps a list of all the balls
- On each tick, tells each ball to draw itself and move
Observables and observers
How does the dispatcher notify the ball(s) when it's time to move and repaint? We model this part of the program with what is called the Observer design pattern. Consider the following situation: You go to your favorite restaurant for dinner, but there are no tables available when you arrive. So you ask that your name be placed on a waiting list. When your table is ready, your name is called and you can be seated.In this case, you are an Observer (observing "table ready" events) and the restaurant is the Observable. The Observable object has some behavior that the Observer is interested in. So, the Observer registers with the Observable; in effect, requesting to be notified when some action occurs in the Observable. When the event occurs, the Observable notifies the Observer. (In fact, there can be many Observers; each one is notified when an Observable event occurs.)
To implement this in Java, we use an Observer class and an Observable class. The Observable class contains a "register" method which allows Observers to register their interest in the Observable. The Observer class contains a "notify" method that allows the Observable to tell the Observer that an event has occurred.
In Ballworld, the BallDispatcher is the Observable and the Balls are Observers. Each time a new Ball is created, it must register with the BallDispatcher. The BallDispatcher adds the ball to a list of Observers. (We'll use the Java class ArrayList to implement this list.) On each timer tick, the BallDispatcher notifies all the Balls that they must now redraw themselves on the screen.
In summary, the structure of the program's classes looks like this:
I'll provide you with the classes Ballworld, BWFrame, Ball, and a shell of the Ball Dispatcher. You'll need to add a few lines to Ballworld and Ball, and fill in most of the code in Ball Dispatcher. You can download the code I am providing through this link: lab2.jar . Create a subdirectory "lab2" in your 160 directory, download the jar file, and expand it using the jar command, as you did in the last lab.
You now have the shell of a working Ballworld program in a directory called lab2. If you compile and run the program, you should see the Ball World frame:
What you see is called a JFrame, which is a member of a class defined in the Java swing package. Swing (javax.swing).is a set of classes designed for creating GUI interfaces. GUI components can be placed in the frame. In this case, the structure of the frame is based on a Border Layout, which allows GUI components to be placed in one of five positions: North, South, East, West, and Center. The Ball World frame contains a JPanel in the South position. The JPanel contains three JButtons. The rest of the frame is a Canvas in the Center position. A Canvas is a GUI Component which is used to display graphics.
The Close Window button should work, but the other two buttons are not yet connected to any action.
Part two. Fill in the missing code in BallDispatcher.
Make the following changes to BallDispatcher.java:
1. The BallDispatcher needs a reference to the Canvas object that it notifies when it is time to repaint the screen. Declare a private instance variable called "canvas" of type Canvas at the top of the class definition for BallDispatcher.
2. The BallDispatcher needs a Timer object to trigger the animation. Timer has a constructor which accepts as arguments (a) the delay time between timer ticks in milliseconds (50 should work), and (b) an ActionListener. In this case, the ActionListener will be the BallDispatcher itself, so write
private javax.swing.Timer timer = new javax.swing.Timer(50,this);Also, add the phrase "implements ActionListener" to the header line of the class definition. (ActionListener is an interface defined in the package java.awt.event.)
3. In its role as an Observable, the BallDispatcther needs to keep track of its Observers; that is, the objects which want to be notified when a timer tick occurs. So, declare a variable of type ArrayList (provided in the java.util package), instantiated with its no-argument constructor:
private ArrayList observers = new ArrayList();4. Write the constructor for BallDispatcher. It should have one argument, a Canvas, which will be passed to it from Ballworld. The constructor should copy its Canvas argument to the corresponding instance variable:
this.owner = owner;Then, it starts the Timer by calling its start method:
timer.start();5. BallDispatcher needs an actionPerformed method, which will be called on each timer tick. (The code I supplied just has a stub routine here.) actionPerformed should result in a redraw of the screen. In Java's graphics model, this is done by repainting the Canvas, so the actionPerformed is a single line, invoking the repaint method of the Canvas:
canvas.repaint();6. BallDispatcher needs methods to manage the ArrayList of Balls. They are:
- register (which an Observer uses to register its request to be notified of future events).
- clear (to empty the set of Observers), and
- notifyAll (to notify all the Observers of an event).
a. register has one argument, a Ball to be registered. Use ArrayList's "add" method to add the Ball to the list of observers.
b. clear has no arguments. It can clear the existing observers by simply creating a new ArrayList and storing it in the observers variable. (The old observers don't have to be explicitly deleted; they will be cleaned up by Java's garbage collector.)
c. notifyAll has one argument, a Graphics object g which must be passed along to the Balls so they know where to draw themselves. Its job is to iterate through the observers, calling the notify method of each one. You'll need a while loop to step through the observers, using an Iterator.
Iterator is another java utility class, this one designed to make it possible to step through a linear sequence of objects, without knowing the details of how the sequence is implemented. We'll use two of Iterator's methods:
boolean hasNext(); // return true if there are more elements in the list being iteratedA simple loop, stepping through an ArrayList is structured as follows:
Object next(); // returns the next element of the list being iterated
Iterator i = arrayList.iterator();For the notifyAll method, you can use this loop structure, with the following changes:
while(i.hasNext()){
Object obj = i.next();
obj.process(); // we have an object, now apply some processing step to it
}
The object returned by i.next() is actually a Ball. In order to call Ball's methods on it, it must be cast to a Ball. So write
Ball ball = (Ball) i.next();The processing step is to call the ball's notify method, with the Canvas and Graphics objects as parameters:
ball.notify(canvas,g);
Part three. Fill in the rest of the pieces.
We now have a BallDispatcher. We just need a few more changes to connect the pieces of the program.
1. Open Ball.java. You should see:
- The constructor uses Java's random number generator to set initial values for the ball's postion, velocity (both x and y components), diameter, and color.
- The notify method is the method which will be called when the timer ticks. It has just two steps: the Ball draws itself onto a Graphics object, and then updates its position with a move operation.
- The move method indicates how to compute the Ball's position after each Timer tick. First, the x and y components of the position are incremented by adding the x and y position of the velocity. The rest of the code is to implement the "bounce off the walls" behavior of the ball. If any part of the ball would fall outside the current frame, the position is adjusted and the velocity is reversed.
- The draw method calls two of the methods of the Graphics object, one to set the ball's color and the other to actually draw it as a filled-in oval. (The two diameter parameters indicate the horizontal and vertical axes of the oval.)
- BWFrame is a subclass of JFrame, a component defined in the java swing package.
- The frame represents a GUI window. It is a container for GUI components. In this case you can see that the frame contains a canvas, a JPanel (a region within the frame that can hold GUI components), and three JButtons.
- BWFrame's constructor initializes its components. The comments indicate what each line of code is doing. For example, the JButtons must be instantiated and then added to the JPanel.
- BWFrame has two methods, addAddListener and addClearListener, which allow action listeners for the buttons to be installed. The code for the action listeners is in Ballworld.java.
a. Create a BWCanvas object and store a reference to it in the canvas variable.
b. Create a BWFrame object and store a reference to it in the frame variable. Pass the canvas to the frame's constructor.
Add another line to create a BallDispatcher and store a reference to it in the ballDispatcher variable. Pass the canvas to the ballDispatcher's constructor.
4. Ballworld.java contains a private class called BWCanvas, which is a subclass of Canvas. The paint method in BWCanvas (which is overriding a method in its base class) is used to indicate actions to be taken when a Graphics object is drawn onto the Canvas. In this case, we want to tell the BallDispatcher to notify all the Balls. Write a line of code to do this.
5. Ballworld.java contains the ActionListeners for the "Add Ball" button and the "Delete all Balls" buttons. The ActionListener interface contains one method declaration called "actionPerformed", which is executed when an action event occurs. (In this case, the action events are the button clicks.) You are responsible for filling in the code of the actionPerformed methods. For the "Add Ball" actionPerformed method, create a new Ball, passing to its constructor the width and height of the canvas, and register the Ball with the ball dispatcher. For the "Delete all Balls" actionPerformed method, tell the ball dispatcher to clear the Balls list.
6. The action listeners must be instantiated and installed. Create a new AddButtonListener object and pass it to the addAddListener method of the frame. Do the same for the ClearButtonListener.
7. Now try running the program. The "Add Ball" and "Delete all Balls" buttons should be working now. When the program is working, move on to part four.
Part four. Make a better Ball.
We now have a program in which balls move in straight lines in a rectangular frame on the screen, bouncing back into the frame when they hit walls. The framework we have set up (and the object-oriented concepts of inheritance and polymorphism) will make it easy to add some different types of ball to the program. We'll illustrate this by creating some curve balls.
A CurveBall will be a special type of Ball (think inheritance ) that behaves like a Ball, but moves differently. Write a CurveBall class as a subclass of Ball, as follows:
1. Create a new .java file called CurveBall.java. Use the "extends" clause to indicate that CurveBall is a subclass of Ball. CurveBall inherits the instance variables and methods of Ball.
2. Constructors are not inherited, so we have to write one for CurveBall. It should have the same two arguments as Ball's constructor. All it needs to do is to call Ball's constructor, with the statement
super(width, height);
3. Add an instance variable of type double called "curvature" to indicate the curvature in the ball's motion. You can initialize it to a constant value (something in the range of 15 to 20 should work), or assign a random value to it in the constructor.
4. The notify and draw methods are inherited from Ball. CurveBall does not need to override these methods.
5. The move method does need to be overridden, to implement curved motion. Curved motion can be implemented by modifying the velocity components (not just the position) each time a ball moves. Write a move method (with the same parameters as Ball's.) which does the following:
a. Call Ball's move method. This will cause the ball's position to be updated.
b. Modify the velocity components as follows:
xvelocity -= yvelocity/curvature;(Note: xvelocity and yvelocity must be declared as doubles for this to work properly.)
yvelocity += xvelocity/curvature;
6. One last detail: CurveBall uses Java classes from java.awt and java.util, so you need import statements to import those packages. The import statements belong at the beginning of the file.
Part five. Now that we have defined a new Ball class, can we use it in an upgraded version of Ballworld? According to the principle of polymorphism, Ballworld should be able to handle any kind of Ball, and of course, CurveBalls are a kind of Ball, so it should work. We just need to modify the user interface so that the user can create some curve balls. We'll do that in this section, by adding a JButton to add a curve ball.
1. Open BWFrame.java. See how the JButton is created for creating a ball. Add a similar segment of code to create a JButton for creating a curve ball. The button should be added to the panel at the bottom of the GUI window.
You will also see a method "addAddListener". Write a similar method "addAddCurveBallListener", which will allow an action listener to be installed for the curve ball button.
2. Now go to Ballworld.java. Find the code for the AddButtonListener class. Copy this code and modify it for the AddCurveBall button. Then in the Ballworld constructor, write a line of code which will instantiate a curve ball button listener and pass it to the frame.
Okay, now save your work and try out the program. See how the straight balls and the curve balls coexist peacefully!
Part six. Roll your own!!
Now that you see the technique of using inheritance to create a new Ball type, it's time to write your own extension of the Ball class and install it into the Ballworld program.
Here are a couple of suggestions:
Color changing ball:
Make a ball that changes color as it moves. Every time you move the ball, set its Color to a random color, just as it was assigned a random color in the Ball constructor.
"Breathing" ball:
Make a ball whose radius changes from big to small to big (within some fixed range) as it moves.
Random walk balls:
Make a ball that wanders aimlessly on the screen. Every time you update, generate random values to add to the xvelocity and yvelocity property values (use the "+=" operator). Keep the velocity values bounded (<15) so that they don't shoot off the screen too fast.
Rectangular random walk balls:
Make a random walking ball that can only walk horizontally or vertically. On each update, set either its xvelocity or its yvelocity to a random value, and the other to zero. (The choice of horizontal or vertical motion should also be random.)
You can use the static method Math.random() to generate random numbers (as I did in my code). Each time it is called, Math.random() returns a double value between 0.0 and 1.0.
Well, that's enough for one week. Submit your Ballworld directory using handin. It should include the normal Ball, CurveBall, and at least two other types of Ball.
Note:
To produce a smoother animationon the linux machines in the lab, try using DABallCanvas.java instead of Canvas. (Your BWCanvas should be a subclass of DABallCanvas.)