Active Objects - Thursday, March 27th, 2003

Announcements

    1. Read over lecture notes including sample programs and text.
    2. Make sure you understand the previous week's lab before starting the following week's. If you understand it, you should be able to hide your solution and write the solution again on your own.
    3. Come to the instructors and TA's for help.

Revisit the Snowman and Bender

Demo: Snowman

Just a few things to point about about the Snowman program:

    1. use getImage() to load a GIF or JPEG file into an Image.
    2. create a VisibleImage on the canvas from the Image.

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 snowGen, 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. What values are possible for this parameter? It is computed by the line

  snowGen.nextValue()*2/screenWidth+2

Of course we could have used a separate random integer generator that provides only smaller numbers in this range, but the above expression also works.

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.

To recap, 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.

To see why we include the pause method call in the while loop of ActiveObject's, look at the behavior of a minor variant of the program where the only change is that we omit the pause in the while loop of class FallingSnow:

Demo: Falling Snow without pause

The difference is that all of the snowflakes are generated without pause, so they all essentially start at once (though some are slower to fall than others). The pause makes the animation much more obvious. (What would have happened if we omitted the pause in the FallingSnow class?)

Adding more complexity

Finally, it's time to take a look at the full Pong example:

Demo: Pong (the bouncing ball version)

The MovingBall class is included on the above page. Some things to notice:

    public static double getTime()
    

It does not need to specify an object and returns the current time in milliseconds.

Summary

We have now seen three different kinds of classes:

  1. Classes that extend WindowController. They require a begin() method to do initialization and may contain methods (such as onMouseClick(Location point)) to respond to mouse events.
  2. Classes that don't extend anything. They require a class constructor (with the same name as the class) to do initialization. They may contain any methods necessary. Examples include Tshirt, Die, etc.
  3. Classes that extend ActiveObject. They also require a constructor to do initialization. The last statement of the constructor calls method start(), though you do NOT provide a start method yourself. Instead you must provide a run() method which will eventually be called by start(). Any while loop in execute() must generally include a pause(double) call, both to provide better animation and to allow the processor to handle other processes that desire to run.