Lecture 9

Announcements

Continuing Action

So far, our programs have exhibited behavior only in direct reaction to user actions with the mouse. In particular, each method resulted in a short burst of action and then stopped.

Sometimes we would like to initiate an action, but then have the action continue for a while. The following program gives an example of such behavior. Click to start the snow falling. Don't worry about the program details yet - that will come later.

Demo: Falling Snow

Programming languages allow us to support such behavior with looping constructs. Looping constructs provide ways of repeating behavior as long as desired. Sometimes we want these actions to be repeated a certain number of times, while other times we want them to occur as long as a certain condition is true.

We begin with a simpler example. Consider a program that draws railroad tracks, as in the following demo:

Demo: Railroad Built by Clicking

Using the Java we have seen so far, we have two options:

    public void begin()
    {
        . . .
        tiePosition = 5;
    }
    
    public void onMouseClick( Location point )
    {
        if ( tiePosition < SCREEN_WIDTH ) {
            new FilledRect(tiePosition, TIE_TOP, TIE_WIDTH, TIE_LENGTH,
                           canvas);
            tiePosition = tiePosition + TIE_WIDTH + TIE_SPACING;
        }
    }

Notice that the method stops drawing new ties once the variable tiePosition represents an x-value that would be off the screen.

It is painful to have to click repeatedly to get the ties drawn. Instead we would like the computer to continue drawing ties while they are still on the screen. Java provides the while statement, or while loop, to perform repeated actions. Java includes other looping constructs that we will see later in the semester.

The syntax of a while statement is:

    while (condition)
    {
        ...
    }
The statements between the open and closed curly brackets are known as the body of the loop.

A common way the while loop is used is as follows:

    while (condition)
    {
        do something
        change some variable so that next time you do
                        something a bit differently
     }

Now we can draw all of our railroad ties using a while loop, right in the begin method:

    public void begin()
    {
        ...
        tiePosition = 5;
        while ( tiePosition < SCREEN_WIDTH ) {
            new FilledRect(tiePosition, TIE_TOP, TIE_WIDTH, TIE_HEIGHT,
                           canvas);
            tiePosition = tiePosition + TIE_WIDTH + TIE_SPACING;
        }
    }

The complete Java program to draw the railroad includes drawing of the rails and setting appropriate colors. The Java source code and the applet to run are available here:

Demo: Railroad Built by While

As above, the condition controlling the while loop will usually involve the variable that's changing. If nothing in the condition changes, then the loop will never terminate. Recall the typical shampoo bottle directions:

    wet hair
    apply shampoo
    lather
    rinse
    repeat
If a robot followed those directions, it would continue washing its (or perhaps George Jetson's) hair forever! This is known as an infinite loop. We avoid this, in general, by encuring that our loops have a precise stopping condition. While we might be able to look at an algorithm and say "hey, we should stop now", Java will not (and in fact cannot, in general) determine if a loop will not stop.

Active Objects

We have now seen how to get one set of commands to be executed repeatedly. But some of the programs we have seen have lots of different things happening simultaneously.

All of the classes we have defined so far have described "passive" objects. They only do things when they are told to (i.e., because someone invokes one of their methods).

We can also create "active" objects in Java. They can contain instructions (in a special method called run) that run even when the user doesn't do anything with the mouse.

If you put a loop that goes on for a long time in an onMouse... method, the WindowController can't respond well to additional events because it is busy in the mouse-handling method. Instead, we will put such loops in the "run" methods of ActiveObject's.

To create an ActiveObject one must:

  1. define a class that "extends ActiveObject"
  2. define its constructor and say "start()" at the end.
  3. define a "public void run()" method.

The class ActiveObject is part of the ObjectDraw library. It includes a number of instance variables and methods that are used to keep track of objects which can execute in parallel with each other. The method start does some housekeeping which results in the creation of a new "thread of control" (or just "thread"), which then begins running the run method. Thus evaluating start (or equivalently, this.start()), eventually leads to the creation of a thread that executes the run method. When the method terminates, the thread dies, and the object is no longer "active."

Our first example is of a falling ball in a window with a Pong paddle. This is a pretty boring pong game, since the paddle can't actually hit the ball, but it does demonstrate that the paddle can move at the same time that the ball falls. There is always one thread that handles the mouse motion methods (e.g., onMouseMove), so this program will have two threads operating concurrently: one to move the paddle and the other to move the ball.

Demo: Pathetic Pong

Consider the ActiveObject in this program - the FallingBall. We see that it does all the things we said an ActiveObject is supposed to do. Its class header tells Java that this class extends ActiveObject. We have several constants and an instance variable to hold the FilledOval that will be the actual ball.

The constructor includes the same functionality as things like the Tshirt examples - it constructs the objects that make up an instance of this class (in this case, just a FilledOval). The difference is that it must end with a start() statement.

The run method contains a simple while loop that defines the ongoing "activity" of a FallingBall:

    public void run() {
        while (ball.getY() < BOTTOM ) {
            ball.move(0, Y_SPEED);
            pause(DELAY_TIME);
        }
        ball.removeFromCanvas();
    }

The condition ball.getY() < BOTTOM is true as long as the ball is on the screen. Notice that the body of the while loop contains the statement pause(DELAY_TIME). When this is executed, the thread pauses execution for DELAY_TIME milliseconds (thousandths of a second) before going around to the next iteration of the loop to move the ball by another Y_SPEED pixels down the screen. If the pause statement were not in the loop the animation would take place so fast that we would not be able to see it. For each of our applications we will play with the value of DELAY_TIME until we get a speed of animation that looks the best. For this particular application, we chose a value of 33. If the value of DELAY_TIME were 66, the ball would fall half as fast (there would be twice as much delay between movements), while a value of 11 would have the ball falling 3 times as fast.

There is another, more technical, reason for including a pause statement in the loop. Most personal computers only have a single processor. Thus if two threads are active, they are both being run by the same processor. In order to make it look as through both are being run simultaneously, they take turns. Different computer operating systems have different ways of taking turns. Some automatically trade off after a certain time interval (usually every few milliseconds), while others wait for one thread to pause before releasing control to the other thread. We will always include pauses in every loop in the run method of an ActiveObject in order to ensure that they alternate turns fairly.

Once the ball has finished falling off the screen there is no need keep it around. We call removeFromCanvas, which is different from the hide method in that there is no way to recover the object once it has been removed, to remove the FilledOval from the canvas. The run method then terminates, "deactivating" our FallingBall.

This is everything you'll need to know for the Boxball lab.

Drawable Images

Demo: Snowman

In the program above, we drag a picture of a snowman around the screen. The picture comes from a "gif" file named snowman.gif.

The image is certainly too complex to draw using our ObjectDraw primitives. Fortunately, we can read an image from a file and save it as an object with type Image. Image is a built-in Java class from the library java.awt. Hence you need to make sure that any program using Image imports java.awt.Image or java.awt.*.

The first line of the begin method of the Snowman class shows how to do this when given a "gif" file (a particular format for holding images on-line):

    snowManPic = getImage("snowman.gif");
where snowManPic is an instance variable declared to have type Image. Downloading a "gif" file can often be slow, so we usually will want to create an image from the "gif" file at the beginning of a program and save it until we need it. If you download "gif" files in the middle of your program, you may cause a long delay while the computer brings it in from a file on a local disk or fileserver.

While objects of class Image can hold a picture, they can't do much else. We would like to create an object that behaves like our other graphics objects (e.g., FramedRect) so that it can be displayed and moved around on our canvas.

The class VisibleImage from the ObjectDraw library allows you to treat an image roughly as you would a rectangle. In fact, imagine a VisibleImage to be a rectangle with a picture embedded in it. You can do most things you can do with a rectangle, except that there's a neat picture on top.

To create a new VisibleImage:

    new VisibleImage( anImage, xLocation, yLocation, canvas);
For example, new VisibleImage(snowManPic, 10, 10, canvas); would create an object of type VisibleImage from the image in snowManPic and place it on canvas at location (10,10), with size equal to the size of the image it contains.

If you associate a name with your VisibleImage, you can manipulate it using some familiar methods:

    VisibleImage snowMan;
And then later:
    snowMan = new VisibleImage(snowManPic, 10, 10, canvas);

    snowMan.setWidth(124);
    snowMan.setHeight(144);

Our original snow man image is large: 619x718 pixels, but we shrunk him down to a more reasonable size.

What do you think happens if we say:

    snowMan.setColor(Color.green);
Nothing! It's not an error, but nothing is done for you either! Because the picture already has its own colors, it wouldn't make sense to change it to a solid color. Similarly, the value returned by snowMan.getColor() is always Color.black, no matter what colors are in the image! Think about replacing snowman.gif with a more colorful image to see why. A file bender.gif is included with the Snowman demo.

The rest of the code for the Snowman class is essentially identical to our earlier programs which allowed us to drag around squares and T-shirts.

Images and Active Objects - putting it all together

Back to the falling snow example, now that we have seen active objects and loading graphics.

Demo: Falling Snow

First consider class Snow, which extends WindowController. While it includes code for loading the images of the snowflakes and draws the background picture, the only indication that something interesting is going on is in the method onMouseClick. On each click, a new object of type Cloud is created. It is the Cloud object that is responsible for all those snowflakes. The snowflake image is passed as a parameter to the Cloud constructor.

First look back at the code for class FallingBall. A falling snowflake will be very similar, except that the object falling will be a VisibleImage rather than a FilledOval. But there's more to it than this.

We created a FallingBall every time the user clicked the mouse. When the user clicks the mouse now, the process of generating and dropping snowflakes begins. There's another class here: the Cloud, which we've also made an active object. A cloud is an active object that continuously generates snowflakes. Each snowflake is an active object that, when constructed, floats down the screen.

Look at the code for class Cloud on the Falling Snow demo.

Let's jump right to the run method of the class. It starts by declaring a local variable, snowCount, initialized to 0. The rest of the method is a while loop which increments snowCount, constructs a FallingSnow (more on FallingSnow soon!), and then pauses for 900 milliseconds before repeating. The while loop's body will be run 150 times before stopping.

The constructor for Cloud saves all four parameters as instance variables. The reason for this is that all four will be needed for the calls to the FallingSnow constructor in the run method. Remember that values of formal parameters go away at the end of the method or constructor in which they are declared. If we need to save values after a method or constructor terminates, then we need to save them in instance variables.

The constructor also creates snowGen, a random number generator used to determine where each snowflake will be dropped and how fast it travels down the screen.

Finally, the constructor calls start(), as required to activate our active object.

Going back to method run, we will see below that the constructor for FallingSnow takes parameters which are a DrawingCanvas on which to draw the image, the Image of the snowflake, a double that determines how far from the left edge of the screen the snowflake will be located, another double to indicate how fast the snowflake falls, and finally an integer indicating the height of the window.

Three of the actual parameters: canvas, snowflakePic, and screenHeight are values of instance variables that were provided values by the constructor of Cloud. Cloud simply remembers and passes along these values to FallingSnow and never uses them in any other way. The other two actual parameters are random values generated by leafGen, which provides values between 0 and the width of the screen. The next to last parameter, representing how far the snowflake falls each time through a loop, is calculated from such a random number, but is divided by screenWidth in order to reduce the speed to a smaller number. Of course we could have used a separate random integer generator that provides only smaller numbers, but the above expression also seems to work well.

Let's now take a look at the FallingSnow class, another active object.

The constructor remembers the speed, screen height, and canvas in instance variables so that they can be used later in the run method, and then creates a VisibleImage from the Image of a snowflake. Once the image has been embedded in a VisibleImage, we can move it around on the screen. In fact, since we created the image at the coordinates (0,0) - the upper left corner of the screen, we immediately move it to its correct x-coordinate and set its y-coordinate so that the bottom of the snowflake is off the top of the screen.

Why not just create the snowflake at the right position instead of creating it at (0,0) then moving it? Unfortunately, we cannot determine the dimensions of a VisibleImage until it has been created. That is, we cannot get this information from the Image used in constructing it. Thus we had to first construct the snowflake before we could see how high it was, and thus, how far up the screen it needed to be located so that it would not be seen! We could have created the VisibleImage with x-coordinate x, but since we knew we were going to have to move it anyway, we just created it at (0,0) and then moved it both across and up.

As usual, the last line in the constructor is the command start().

The run method of FallingSnow is quite simple. It is a simple loop that pauses and then moves the snowflake. It terminates when the snowflake is off the screen. It then removes the snowflake from the canvas.

When executing, this program contains several passive objects and may contain hundreds of active objects, all running at once. There is an object corresponding to the main class, Snow, that loads the snowflake pictures and draws the scene. It responds to mouse clicks by creating an object of class Cloud. The creation of a Cloud results in the creation of 150 objects of type FallingSnow.

Always remember, to create an ActiveObject you:

  1. define a class that "extends ActiveObject"
  2. define its constructor and say "start()" at the end.
  3. define at least a "public void run()" method.