by Paul Buchheit
If you were awake this past year then you've probably heard of Java. If you were browsing the World Wide Web with a recent version of Netscape then you've probably even seen a Java applet or two. A Java applet is a Java program that interfaces with and endures the security restrictions of a web browser. Animated displays and games were some of the earliest and most popular uses for applets, although certainly not their only uses. You may have already noticed that, despite the popularity of Java animation, many applets don't do animation very well.
In order to illustrate beginning Java programming and animation techniques, I wrote a very simple slot machine in Java, its only moving part a lone, spinning wheel. To start, I will present a slot machine that uses one of the simplest and (unfortunately) most popular methods of animation. This slot machine won't look very good, but making it look better is not very difficult, and I'll present several methods for improving it. The source, images and actual incarnations of the slot machines described in this article are available at http://k2.cwru.edu/~ptb/lslot/. I recommend that you experiment with them as you read this.
This slot machine is composed of two images. The first image--the body of the slot machine--is really just a decorative so I have chosen our good friend and potential Linux mascot, Tux. The second image is really a strip of images that forms the face of the slot machine's wheel (Figure 2). We could load each of these images separately but that would probably slow down the loading process and complicate the code. These images are loaded inside the init() method through a call to getImage(), a method defined in the superclass Applet.
If you know C++ (or Java) you might notice that init() functions a lot like a constructor, i.e. it is used to initialize variables for a newly created object. There is a big difference however, in that a constructor is called when the class is instantiated, init() is called when the applet's host is ready to initialize the applet. Moving the getImage() code discussed above from init() to a constructor could cause it to break.
When the applet's host finally decides to display the applet it will call paint(). Looking at paint() you should notice that it gets passed a variable g of type Graphics, it is with this variable g that you will do your drawing. In fact, all the drawing that an applet ever does is through some instance of Graphics. The first line of paint(),
g.drawImage(body, 0, 0, this);
will draw the image body at position 0, 0. The fourth argument to drawImage(), this, specifies an ImageObserver, this is simply a reference to the current object.
An ImageObserver is an instance of any class that implements the java.awt.image.ImageObserver interface, meaning that it has been declared to implement ImageObserver and has a method imageUpdate(). In the following example, an instance of the class Foo would be a valid ImageObserver.
class Foo extends Object implements ImageObserver { ... public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h) { ... return true; } }
Interfaces provide a limited, yet safe and usable form of multiple inheritance. Fortunately, java.awt.Component, an ancestor of the superclass, Applet, is already an ImageObserver so no code needs to be written here.
If the image is not yet fully loaded the system will make note of the ImageObserver. Later on when more, but not necessarily all, of the data is ready, imageUpdate() will get called. By default, the imageUpdate() method for an applet will call repaint(), which causes the "visual loading" effect where the image gets painted as the data is loaded.
The next thing that paint() does is create a new Graphics specifically for drawing the wheel on the slot machine. This is accomplished by a call to createForWheel(), a method that I added. As mentioned, all drawing is done using a Graphics, and every Graphics has a rectangle somewhere on screen or in memory, to which it can draw. If a command such as drawImage() involves drawing outside this rectangle, the part that extends outside the rectangle is clipped. This is very useful if we only want to display a portion of an image.
The createForWheel() method creates the new Graphics by a call to the create() method of the Graphics that was passed to paint(). Four integers are passed to create() which specify the location and size of the new Graphics's rectangle--in this case, the position and size of the wheel. Now that we have this new Graphics we can do things such as:
drawImage(strip, 0, -55, this)
Even though strip is over 500 pixels tall and the specified coordinates put it in the upper left-hand corner, a nice 55 by 55 pixel square (the size specified when creating the Graphics) shows up at the position specified for the wheel that displays rows 55 through 110 of the image strip.
Once the new Graphics is no longer needed, its dispose() method should be called. This probably seems a little odd since Java has automatic garbage collection (meaning that memory does not have to be explicitly freed as in C or C++). The reason for calling dispose() anyway is that Garbage collection is not immediate and the Graphics could be in possession of limited system resources.
This slot machine, like most, does not spin its wheel all the time. In fact, its wheel only spins in response to a mouse button click and then only for a short time. Looking at the mouseDown() method you can see that it simply creates a new Thread called ``spinning''. When a thread is started it calls the run() method of the Runnable (another interface) object specified in the thread's constructor. In this case the object is this applet, this.
The slot machine's exciting spinning action is implemented in run(). The first thing that run() does is to ask getNewItem() where it's going. The getNewItem() method just returns a random number from 0 to 5 specifying the stopping item on the wheel. The run() method then calculates how far, in pixels, the wheel must travel to get there, including the number of items that should spin past the front before the wheel stops. After this run() simply loops until the wheel reaches its destination. Calculate the new position, repaint, sleep, repeat. Once finished run() just sets spinning to null so that clicking the mouse button again starts the wheel spinning, and run() returns.
Try the applet! Looks awful, doesn't it? Probably the most obvious defect is the ugly grey flashing that appears while the wheel is spinning. Fortunately, this problem is very easy to fix--every time repaint() is called the system asynchronously calls update(). update() is an inherited method (defined all the way back in the class java.awt.Component) that draws a rectangle the size of the applet using the background color and then calls paint(). This rectangle draw is the source of most of the flicker--since we are going to immediately draw over the whole region of the applet it is not only annoying but unnecessary. To fix the flicker, just insert the following method in the space between createForWheel() and the paint methods (see Listing 1):
public void update(Graphics g) { paint(g); }
Now run the applet--doesn't it look much better? However, if you look closely you might notice that the wheel has a slight black flicker, and the animation is a little rough. This problem is similar to the previous one--the body of the slot machine, which just has a black square where the wheel should be, gets drawn before the wheel graphic. so for an instant there isn't any wheel. One popular solution for this problem is double buffering: using an off-screen buffer to hold the image while it is being drawn. Now the part of the body hiding behind the wheel will never appear.
To add double buffering to our applet, the first thing that you must do is create a buffer, called (appropriately enough) buffer. Next add an Image instance variable called buffer into the class and insert into the init() method the line:
buffer = createImage(size().width, size().height);Now paint() must be modified so that it will first draw into the buffer and then draw the buffer itself onto the screen. This new paint() should look something like this:
public void paint(Graphics g) { Graphics bufG = buffer.getGraphics(); bufG.drawImage(body, 0, 0, this); Graphics clipG = createForWheel(bufG); drawWheel(clipG, currentWheelPos); clipG.dispose(); g.drawImage(buffer, 0, 0, this); }
Now run the applet--looks much nicer, no more flicker! But it seems awfully inefficient to redraw the entire area of the applet window when only a small part is changing, doesn't it? Another easy fix! Simply change the line:
repaint();
found in run() into:
repaint(wheelPosX, wheelPosY, wheelSize, wheelSize);
This new call tells the AWT system to update only the specified rectangle and leave the rest of the window alone.
It seems that, as before, we are drawing the body of the slot machine more often than needed, only this time it's to the buffer instead of the screen. It's possible to work up a scheme where only the wheel gets repainted to the buffer, fixing this complaint, but, as you may have already realized, there is a better way.
This buffer is silly, not only is it starting to get complicated but having some big Image in memory is a big waste, especially for more complex applets (such as a big, complex, slot machine). Buffer images have their place but buffering the whole applet is rarely a good idea. Why not just forget about using repaint and instead draw the spinning wheel right inside of run()? Good idea. Going back to the code that we had before adding in that buffer, modify run() as follows:
public void run() { // Gets something to spin to. int nextItem = getNewItem(); int pos = currentWheelPos; int finalPos = (itemsToSpin + nextItem) * wheelSize; Graphics g = createForWheel(getGraphics()); while((spinning != null) && (pos != finalPos)) { pos = findNextPos(pos, finalPos); currentWheelPos = pos % stripLen; drawWheel(g, currentWheelPos); getToolkit().sync(); try { Thread.sleep(delay); } catch(InterruptedException e) { } } g.dispose(); spinning = null; }
Now we are simply getting the Graphics that we need to draw the wheel and calling drawWheel() directly every time we move the wheel. The trick here is to call
getToolkit().sync();
when we want the drawing to appear. Without the call to sync() the system would wait until several drawing requests arrive, resulting in jumpy animation.
Finally the slot machine is finished! I think you can see that although the code is almost identical to that of the first slot machine, the resulting animation is much smoother.
For more complex animations you will probably want to use a combination of the methods presented here. For example, let's say you want to create a box with two balls bouncing around inside. In most cases you will simply have a drawImage() for each ball, but what happens if the two draws overlap? You may end up with flicker in the area of intersection. One solution would be to double buffer the draws whenever the two images intersect.
As usual, if you are having a hard time with some aspect of your applet you can (hopefully) find an applet that does something similar and look at the source. The largest collection of Java applets can be found at Gamelan (http://www.gamelan.com/), you should also find a link to the winners of the Java Cup International there. Another good place to find applets is the Java Applet Rating Service (JARS) at http://www.jars.com/. As the name implies, the Java Applet Rating Service rates applets based on a number of factors including quality of code (if the code is freely available).
Paul Buchheit is an inmate at Case Western Reserve University. When he's not busy sleeping, he's awake. He can be reached at ptb@po.cwru.edu.