Skip to main content.

Web Based Programming Tutorials

Homepage | Forum - Join the forum to discuss anything related to programming! | Programming Resources

Tricks of the Java Programming Gurus

Chapter 13 -- Animation Techniques

Chapter 13

Animation Techniques


CONTENTS


Animation is perhaps one of the most popular uses of the Java language thus far. Even if few people have realized the full potential of using Java to solve problems on the Web, most can see the benefits of using Java to animate Web content. Java is indeed the ideal technology to bring animation to the Web. In this chapter, you learn all about animation as it applies to Java, including the different types of fundamental animation techniques.

Throughout this chapter, you learn about animation by developing real applets that demonstrate the animation techniques discussed. You also learn optimization tips to minimize flicker and get the best performance out of Java animations. The chapter concludes with a fully functioning set of sprite classes for creating Java applets with multiple, interactive animated objects.

What Is Animation?

What is animation? To put it simply, animation is the illusion of movement. When you watch television, you see lots of things moving around. You are really being tricked into believing that you are seeing movement. In the case of television, the illusion of movement is created by displaying a rapid succession of images with slight changes in the content. The human eye perceives these changes as movement because of its low visual acuity. The human eye can be tricked into perceiving movement with as low as 12 frames of movement per second. It should come as no surprise that frames per second (fps) is the standard unit of measure for animation. It should also be no surprise that computers use the same animation technique as television sets to trick us into seeing movement.

Although 12 fps is enough technically to make animation work, the animations sometimes look jerky. Most professional animations therefore use a higher frame rate. Television uses 30 fps, and motion pictures use about 24 fps. Although the number of frames per second is a good measure of the animation quality, it isn't always the bottom line. Professional animators have the ability to create their animations with a particular frame rate in mind so that they can alleviate some of the jerkiness at slower speeds.

When you program animation in Java, you typically have the ability to manipulate the frame rate a fair amount. The obvious limitation on frame rate is the speed at which the computer can generate and display the animation frames. There is usually some give and take between establishing a frame rate low enough to yield a smooth animation, while not bogging down the processor and slowing the system. You learn more about all that later. For now, keep in mind that when programming animation in Java, you are acting as a magician creating the illusion of movement for the users of your applet.

Types of Animation

Before jumping into writing Java code, you need some background on the different types of animation. Armed with this knowledge, you can then pick and choose which approach suits your animation needs best.

There are many different types of animation, all useful in different instances. However, for implementing animation in Java, animation can be broken down into two basic types: frame-based animation and cast-based animation.

Frame-Based Animation

Frame-based animation is the simpler of the animation techniques. It involves simulating movement by displaying a sequence of static frames. A movie is a perfect example of frame-based animation; each frame of the film is a frame of animation. When the frames are shown in rapid succession, they create the illusion of movement. In frame-based animation, there is no concept of an object distinguishable from the background; everything is reproduced on each frame. This is an important point, because it distinguishes frame-based animation from cast-based animation.

The number of images used in the Count applets in the last chapter would make a good frame-based animation. By treating each image as an animation frame and displaying them all over time, you can create counting animations. As a matter of fact, you do this exact thing a little later in this chapter.

Cast-Based Animation

Cast-based animation, which also is called sprite animation, is a very popular form of animation and has seen a lot of usage in games. Cast-based animation involves objects that move independently of the background. At this point, you may be a little confused by the use of the word "object" when referring to parts of an image. In this case, an object is something that logically can be thought of as a separate entity from the background of an image. For example, in the animation of a forest, the trees might be part of the background, but a deer would be a separate object moving independently of the background.

Each object in a cast-based animation is referred to as a sprite, and can have a changing position. Almost every video game uses sprites to some degree. For example, every object in the classic Asteroids game is a sprite moving independently of the other objects. Sprites generally are assigned a position and a velocity, which determine how they move.

Note
Speaking of Asteroids, Chapter 14, "Writing 2D Games," takes you through developing a complete Asteroids game in Java.

Going back to the example involving the number images, if you want to create an animation with numbers floating around on the screen, you would be better off using cast-based animation. Remember, frame-based animation is useful for counting (changing the number itself). However, cast-based animation is better when the number has to be able to change position; the number in this case is acting as a sprite.

Transparency

Because bitmapped images are rectangular by nature, a problem arises when sprite images aren't rectangular in shape-which is usually the case. The problem is that the areas of the rectangular image surrounding the sprite hide the background when the sprite is displayed. The solution is transparency, which enables you to specify that a particular color in the sprite is not to be displayed. This color is known as the transparent color.

Lucky for you, transparency is already supported in Java by way of the GIF 89a image format. In the GIF 89a image format, you specify a color of the GIF image that serves as the transparent color. When the image is drawn, pixels matching the transparent color are skipped over and left undrawn, leaving the background pixels unchanged.

Z-Order

The depth of sprites on the screen is referred to as Z-order. It is called Z-order because it works like another dimension, a z axis. You can think of sprites moving around on the screen in the x,y axis. Similarly, the z axis can be thought of as another axis projected out of the screen that determines how the sprites overlap each other; it determines their depth within the screen. Even though you're now thinking in terms of three axes, Z-order can't really be considered 3D, because it only specifies how objects hide each other.

Collision Detection

There is one final topic to cover regarding sprite animation: collision detection. Collision detection is simply the method of determining whether sprites have collided with each other. Although collision detection doesn't directly play a role in creating the illusion of movement, it is nevertheless tightly linked to sprite animation.

Collision detection defines how sprites physically interact with each other. In an Asteroids game, for example, if the ship sprite collides with an asteroid sprite, the ship is destroyed. Similarly, a molecular animation might show atoms bouncing off each other; the atom sprites bounce in response to a collision detection. Because a lot of animations have many sprites moving around, collision detection can get very tricky.

There are many approaches to handling collision detection. The simplest approach is to compare the bounding rectangles of each sprite with the bounding rectangles of all the other sprites. This method is very efficient, but if you have objects that are nonrectangular, there will be a certain degree of error when the objects brush by each other. This is because the corners might overlap and indicate a collision when really only the transparent areas are intersecting. The more irregular the shape of the sprites, the more error there usually is. Figure 13.1 shows how simple rectangle collision works.

Figure 13.1 : Collision detection using simple rectangle collision.

In Figure 13.1, the areas determining the collision detection are shaded. You can see how simple rectangle collision detection isn't all that accurate. An improvement on this technique is to shrink the collision rectangles a little, which reduces the corner error. This method improves things a little, but might cause error in the other direction and enable the sprites to overlap in some cases without signaling a collision. Figure 13.2 shows how shrinking the collision rectangles can improve the error on simple rectangle collision detection. You use this approach later in this chapter when you develop a sprite class in Java.

Figure 13.2 : Collision detection using shrunken rectangle collision.

Another solution is to detect collision based on the sprite image data and to see whether transparent parts of the image or the image itself are overlapping. In this case, you get a collision only if the actual sprite images are overlapping. This is the ideal technique for detecting collision because it is exact and enables objects of any shape to move by each other without error. Figure 13.3 shows collision detection using the sprite image data.

Figure 13.3 : Collision detection using sprite image data.

Unfortunately, this technique requires far more overhead than rectangle collision detection and sometimes can be a major bottleneck in performance. Considering the fact that getting decent animation performance is already a challenge in Java, it's safe to forget about this approach for the time being.

Implementing Frame Animation

The most common animation used in Java applets is simple frame animation. This type of animation involves displaying a series of image frames that create the effect of motion and draw attention to certain parts of a Web page. For this reason, you first learn how to implement frame animation before moving on to the more complicated sprite animation. The Counter1 applet shown in Figure 13.4 shows a very basic implementation of frame animation.

Figure 13.4 : The Counter 1 basic frame animation applet.

In Counter1, a series of ten number images are used to animate a count from zero to ten. The source code for Counter1 is shown in Listing 13.1.


Listing 13.1. The Counter1 sample applet.
// Counter1 Class
// Counter1.java

// Imports
import java.applet.*;
import java.awt.*;

public class Counter1 extends Applet implements Runnable {
  Image[]       numbers = new Image[10];
  Thread        animate;
  MediaTracker  tracker;
  int           frame = 0;

  public void init() {
    // Load and track the images
    tracker = new MediaTracker(this);
    for (int i = 0; i < 10; i++) {
      numbers[i] = getImage(getDocumentBase(), "Res/" + i + ".gif");
      tracker.addImage(numbers[i], 0);
    }
  }

  public void start() {
    if (animate == null) {
      animate = new Thread(this);
      animate.start();
    }
  }

  public void stop() {
    if (animate != null) {
      animate.stop();
      animate = null;
    }
  }

  public void run() {
    try {
      tracker.waitForID(0);
    }
    catch (InterruptedException e) {
      return;
    }

    while (true) {
      if (++frame > 9)
        frame = 0;
      repaint();
    }
  }

  public void paint(Graphics g) {
    if ((tracker.statusID(0, true) & MediaTracker.ERRORED) != 0) {
      // Draw the error rectangle
      g.setColor(Color.red);
      g.fillRect(0, 0, size().width, size().height);
      return;
    }
    if ((tracker.statusID(0, true) & MediaTracker.COMPLETE) != 0) {
      // Draw the frame image
      g.drawImage(numbers[frame], 0, 0, this);
    }
    else {
      // Draw the loading message
      Font        font = new Font("Helvetica", Font.PLAIN, 16);
      FontMetrics fm = g.getFontMetrics(font);
      String      str = new String("Loading images...");
      g.setFont(font);
      g.drawString(str, (size().width - fm.stringWidth(str)) / 2,
        ((size().height - fm.getHeight()) / 2) + fm.getAscent());
    }
  }
}

Even though Counter1 is a basic animation example, you're probably thinking it contains a lot of code. The reason is that it takes a decent amount of code to get even a simple animation up and running. Just take it a step at a time and you'll see that it's not so bad.

The number images used in the animation are stored in the member variable numbers, which is an array of Image. There are also member variables for an animation thread, a media tracker, and the current frame of animation. An animation thread is necessary because animations perform much better within their own thread of execution. The media tracker, as you learned in the previous chapter, is used to determine when all the images have been loaded.

The init method loads all the images and registers them with the media tracker. The start and stop methods are standard thread handler methods. The run method first waits for the images to finish loading by calling the waitForID method of the MediaTracker object. Once the images have finished loading, an infinite while loop is entered that handles incrementing the animation frame and forcing the applet to repaint itself. By forcing a repaint, you are causing the applet to draw the next frame of animation.

The frames are actually drawn in the paint method, which looks a lot like the paint method from the Count2 applet in the previous chapter. The only significant difference is the line of code that actually draws the frame image, which follows:

g.drawImage(numbers[frame], 0, 0, this);

Notice that the correct frame is drawn by indexing into the image array with the current frame. It's as simple as that!

Although the Counter1 applet may seem much simpler after closer inspection, it is lacking in many ways. The most obvious problem with it is that there is no control over the speed of the animation (frame rate). Animations can hardly be effective if they're zipping by too fast to keep up with. Another problem with Counter1 is the obvious flicker when the animation frames are drawn. Although the flicker may be fairly tolerable with this animation, because the frame images themselves are fairly small, it would be much worse with larger images. It's safe to say that this problem should be solved.

Actually, both of these problems will be dealt with in a variety of ways. The next few sections of this chapter deal with improving this applet by solving these problems incrementally. The end result is a powerful, high-performance frame animation applet that you can use in your own Web pages.

Establishing a Frame Rate

Arguably, the biggest problem with Counter1 is the lack of control over the speed of the animation. The Counter2 applet fixes this problem quite nicely. I'd love to show you a nice figure displaying the difference between the two applets, but unfortunately frame rate is difficult to communicate on a printed page. You'll have to resort to the CD-ROM and run the applets yourself to see the difference.

Even so, by learning the programmatic differences between the two applets, you should form a good understanding of how Counter2 solves the frame rate problem. The first change made in Counter2 is the addition of an integer member variable, delay. This member variable determines the delay, in milliseconds, between each successive animation frame. The inverse of this delay value is the frame rate of the animation. The delay member variable is initialized in Counter2 as follows:

int delay = 200; // 5 fps

You can tell by the comment that the inverse of 200 milliseconds is 5 fps. So, a value of 200 for delay yields a frame rate of 5 frames per second. That's pretty slow by most animation standards, but you want to be able to count the numbers as they go by, so it's a good frame rate for this example.

The code that actually uses the delay member variable to establish the frame rate is located in the run method. Listing 13.2 contains the source code for the run method in Counter2.


Listing 13.2. The run() method in the Counter2 sample applet.
public void run() {
  try {
    tracker.waitForID(0);
  }
  catch (InterruptedException e) {
    return;
  }

  // Update everything
  long t = System.currentTimeMillis();
  while (Thread.currentThread() == animate) {
    if (++frame > 9)
      frame = 0;
    repaint();
    try {
      t += delay;
      Thread.sleep(Math.max(0, t - System.currentTimeMillis()));
    }
    catch (InterruptedException e) {
      break;
    }
  }
}

The first interesting line of code in the run method is the call to currentTimeMillis. This method returns the current system time in milliseconds. You aren't really concerned with what absolute time this method is returning you, because you are going to use it here only to measure relative time. First, the frame is incremented and the repaint method called as in Counter1.

The delay value is then added to the current time. At this point, you have updated the frame and calculated a time value that is delay milliseconds into the future. The next step is to tell the animation thread to sleep an amount of time equal to the difference between the future time value you just calculated and the present time. The sleep method is used to make a thread sleep for a number of milliseconds, as determined by the value passed in its only parameter. You may be thinking you could just pass delay to sleep and things would be fine. This approach technically would work, but it would have a certain amount of error, because a finite amount of time passes between updating the frame and putting the thread to sleep. Without accounting for this time, the actual delay between frames wouldn't be equal to the value of delay. The solution is to check the time before and after the frame is updated and reflect the difference in the delay passed to the sleep method.

With that, the frame rate is under control. You simply change the value of the delay member variable to alter the frame rate. You should try running the applet at different frame rates to see the effects. You'll quickly learn that the frame rate will max out at a certain value, in which case increasing it won't help anymore. At this point, the applet is eating all the processor time with the animation thread.

Eliminating Flicker

Now that the frame rate issue is behind you, it's time to tackle the remaining problem plaguing the Counter2 applet: flicker. Unlike the frame rate problem, there are two different ways to approach the flicker problem. The first is very simple, but is less effective and applies only to a limited range of animations. The second is more complicated, but is very powerful and absolutely essential in creating quality animations. You're going to learn about both of these approaches.

Overriding the update() Method

The simplest solution to eliminating the flicker problem in animations is to override the update method in your applet. To see how an overridden version of update might help, take a look at the source code for the standard update method, as contained in the Java 1.0 release:

public void update(Graphics g) {
  g.setColor(getBackground());
  g.fillRect(0, 0, width, height);
  g.setColor(getForeground());
  paint(g);
}

Notice that update performs an update of the graphics context by first erasing it and then calling the paint method. It's the erasing part that causes the flicker. With every frame of animation, there is an erase followed by a paint. When this process occurs repeatedly and rapidly, as in animations, the erase results in a visible flicker. If you could just paint without erasing, the flicker would be eliminated. That's exactly what you need to do.

The Counter3 applet is functionally equivalent to the Counter2 applet except for the addition of an overridden update method. The update method in Counter3 looks like this:

public void update(Graphics g) {
  paint(g);
}

This update method is a pared-down version of the original method that only calls paint. By eliminating the erase part of the update, you put an end to the flicker problem. In this case, however, there is a side effect. Check out Counter3 in action in Figure 13.5 to see what I mean.

Figure 13.5 : The Counter3 frame animation applet.

It's pretty obvious that the background is not being erased because you can see the remains of the Loading images... message behind the animation. This brings up the primary limitation of this solution to the flicker problem: it only works when your animation takes up the entire applet window. Otherwise, the parts of the applet window outside the animation never get erased.

Another limitation not readily apparent in this example is that this solution applies only to animations that use images. What about animations that are based on AWT graphics primitives, such as lines and polygons? In this case, you want the background to be erased between each frame so that the old lines and polygons aren't left around. What then?

Double Buffering

Double buffering is the cure-all for many problems associated with animation. By using double buffering, you eliminate flicker and allow speedy animations involving both images and AWT graphics primitives. Double buffering is the process of maintaining an extra, offscreen buffer image onto which you draw the next frame of animation. Rather than drawing directly to the applet window, you draw to the intermediate, offscreen buffer. When it's time to update the animation frame, you simply draw the entire offscreen buffer image to the applet window and then start the process over by drawing the next frame to the buffer. Figure 13.6 contains a diagram showing how double buffering works.

Figure 13.6 : The basics of double buffered animation.

The Counter4 applet is an improved Counter3 with full double buffering support. Although double buffering is certainly more complex than overriding the update method with a single call to paint, it's still not too bad. As a matter of fact, the majority of the changes in Counter4 are in the update method, which is shown in Listing 13.3. Before you look at that, check out the two member variables that have been added to the Counter4 applet:

Image         offImage;
Graphics      offGrfx;

The offImage member variable contains the offscreen buffer image used for drawing intermediate animation frames. The offGrfx member variable contains the graphics context associated with the offscreen buffer image.


Listing 13.3. The update() method in the Counter4 sample applet.
public void update(Graphics g) {
  // Create the offscreen graphics context
  Dimension dim = size();
  if (offGrfx == null) {
    offImage = createImage(dim.width, dim.height);
    offGrfx = offImage.getGraphics();
  }

  // Erase the previous image
  offGrfx.setColor(getBackground());
  offGrfx.fillRect(0, 0, dim.width, dim.height);
  offGrfx.setColor(Color.black);

  // Draw the frame image
  offGrfx.drawImage(numbers[frame], 0, 0, this);

  // Draw the image onto the screen
  g.drawImage(offImage, 0, 0, null);
}

The update method in Counter4 handles almost all the details of supporting double buffering. First, the size of the applet window is determined with a call to the size method. The offscreen buffer is then created as an Image object whose dimensions match those of the applet window. It is important to make the offscreen buffer the exact size of the applet window. The graphics context associated with the buffer is then retrieved using the getGraphics method of Image.

Because you are now working on an offscreen image, it's safe to erase it without worrying about flicker. As a matter of fact, erasing the offscreen buffer is an important step in the double-buffered approach. After erasing the buffer, the animation frame is drawn to the buffer, just as it was drawn to the applet window's graphics context in the paint method in Counter3. The offscreen buffer is now ready to be drawn to the applet window. This is simply a matter of calling drawImage and passing the offscreen buffer image.

Notice that the paint method isn't even called from update. This a further optimization to eliminate the overhead of calling paint and going through the checks to see whether the images have loaded successfully. At the point that update gets called, you already know the images have finished loading. However, this doesn't mean you can ignore paint; you must still implement paint because it gets called at other points in the AWT framework. Counter4's version of paint is very similar to Counter3's paint, with the only difference being the line that draws the offscreen buffer:

g.drawImage(offImage, 0, 0, null);

This is the same line of code found at the end of update, which shouldn't be too surprising to you by now.

Working with Tiled Image Frames

The last modification you're going to learn about in regard to the Counter applets is that of using a single tiled image rather than individual images for the animation frames.

Note
A tiled image is an image containing multiple sub-images called tiles. A good way to visualize a tiled image is to think of a reel of film for a movie; the film can be thought of as a big tiled image with lots of image tiles. The movie is animated by displaying the image tiles in rapid succession.

In all the Counter applets until now, the animation frames have come from individual images. Counter5 is a modified Counter4 that gets its frame images from a single image containing tiled subimages. The image Numbers.gif is used in Counter5 (see Figure 13.7).

Figure 13.7 : The Numbers.gif tiled animation image used in Counter5.

As you can see, the individual number images are tiled horizontally from left to right in Numbers.gif. To see how Counter5 manages to draw each frame using this image, check out Listing 13.4, which contains the update method.


Listing 13.4. The update() method in the Counter5 sample applet.
public void update(Graphics g) {
  // Create the offscreen graphics context
  Dimension dim = size();
  if (offGrfx == null) {
    offImage = createImage(dim.width, dim.height);
    offGrfx = offImage.getGraphics();
  }

  // Erase the previous image
  offGrfx.setColor(getBackground());
  offGrfx.fillRect(0, 0, dim.width, dim.height);
  offGrfx.setColor(Color.black);

  // Draw the frame image
  int w = numbers.getWidth(this) / 10,
      h = numbers.getHeight(this);
  offGrfx.clipRect(0, 0, w, h);
  offGrfx.drawImage(numbers, -(frame * w), 0, this);

  // Draw the image onto the screen
  g.drawImage(offImage, 0, 0, null);
}

The only part of update that is changed is the part where the frame image is drawn to the offscreen buffer. The width and height of the frame to be drawn are first obtained. Notice that the width of a single frame is calculated by getting the width of the entire image and dividing it by the number of tiles (in this case 10). Then, the offscreen graphics context is clipped around the rectangle where the frame is to be drawn. This clipping is crucial, because it limits all drawing to the specified rectangle, which is the rectangle for the single frame of animation. The entire image is then drawn to the offscreen buffer at a location specifically calculated so that the correct frame will appear in the clipped region of the offscreen buffer. To better understand what is going on, take a look at Figure 13.8.

Figure 13.8 : Using a clipping region to draw a single frame of a tiled image.

The best way to understand what is happening is to imagine the offscreen buffer as a piece of paper. The clipping rectangle is a rectangular section of the paper that has been removed. So, you have a piece of paper with a rectangular section that you can see through. Now, imagine the tiled image as another piece of paper that you are going to hold up behind the first piece. By lining up a tiled frame on the image piece of paper with the cutout on the first piece, you are able to view that frame by itself. Pretty tricky!

It is faster to transmit a single tiled image than it is to transmit a series of individual images. Because any potential gain in transmission speed has to be made whenever possible, the tiled image approach is often valuable. The only problem with it is that it won't work for sprite animation, which you learn about next.

At this point, you have a very powerful and easy-to-use animation applet, Counter5, to use as a template for your own animation applets. Counter5 contains everything you need to include high-performance, frame-based animations in your Web pages. If, however, your needs go beyond frame-based animation, read on to learn all about implementing sprite animation.

Implementing Sprite Animation

As you learned earlier in this chapter, sprite animation involves the movement of individual graphic objects called sprites. Unlike simple frame animation, sprite animation involves considerably more overhead. More specifically, it is necessary not only to develop a sprite class, but also a sprite management class for keeping up with all the sprites. This is necessary because sprites need to be able to interact with each other through a common interface.

In this section, you learn how to implement sprite animation in Java by creating two sprite classes: Sprite and SpriteVector. The Sprite class models a single sprite and contains all the information and methods necessary to get a single sprite up and running. However, the real power of sprite animation is harnessed by combining the Sprite class with the SpriteVector class, which is a container class that keeps up with multiple sprites.

The Sprite Class

Although sprites can be implemented simply as movable images, a more powerful sprite includes support for frame animation. A frame-animated sprite is basically a sprite with multiple frame images. The Sprite class you develop here supports frame animation, which comes in the form of an array of images that can be displayed in succession. Using this approach, you end up with a Sprite class that supports both fundamental types of animation.

Enough general talk about the Sprite class-you're probably ready to get into the details of how to implement it. However, before jumping into the Java code, take a moment to think about what information a Sprite class needs. The following list contains the key information the Sprite class needs to include:

The array of frame images is necessary to carry out the frame animations. Even though the support is there for multiple animation frames, a sprite requires only a single image. The current frame keeps up with the current frame of animation. In a typical frame-animated sprite, the current frame gets incremented to the next frame when the sprite is updated. The x,y position stores the position of the sprite in the applet window. The Z-order represents the depth of the sprite in relation to other sprites. Ultimately, the Z-order of a sprite determines its drawing order (more on that a little later). Finally, the velocity of a sprite represents the speed and direction of the sprite.

Now that you understand the basic information required by the Sprite class, it's time to get into the specific Java implementation. Take a look at Listing 13.5, which contains the source code for the Sprite class.


Listing 13.5. The Sprite class.
// Sprite Class
// Sprite.java

// Imports
import java.awt.*;
import java.awt.image.*;

public class Sprite {
  Component component;
  Image[]   image;
  int       frame,
            frameInc,
            frameDelay,
            frameTrigger;
  Rectangle position,
            collision;
  int       zOrder;
  Point     velocity;

  Sprite(Component comp, Image img, Point pos, Point vel, int z) {
    component = comp;
    image = new Image[1];
    image[0] = img;
    frame = 0;
    frameInc = 0;
    frameDelay = frameTrigger = 0;
    velocity = vel;
    zOrder = z;
    setPosition(pos);
  }

  Sprite(Component comp, Image[] img, int f, int fi, int fd,
    Point pos, Point vel, int z) {
    component = comp;
    image = img;
    frame = f;
    frameInc = fi;
    frameDelay = frameTrigger = fd;
    velocity = vel;
    zOrder = z;
    setPosition(pos);
  }

  Image[] getImage() {
    return image;
  }

  int getFrameInc() {
    return frameInc;
  }

  void setFrameInc(int fi) {
    frameInc = fi;
  }

  int getFrame() {
    return frame;
  }

  void incFrame() {
    if ((frameDelay > 0) && (--frameTrigger <= 0))
    {
      // Reset the frame trigger
      frameTrigger = frameDelay;

      // Increment the frame
      frame += frameInc;
      if (frame >= image.length)
        frame = 0;
      else if (frame < 0)
        frame = image.length - 1;
    }
  }

  Rectangle getPositionRect() {
    return position;
  }

  void setPosition(Rectangle pos) {
    position = pos;
    calcCollisionRect();
  }

  void setPosition(Point pos) {
    position = new Rectangle(pos.x, pos.y,
      image[0].getWidth(component), image[0].getHeight(component));
    calcCollisionRect();
  }

  Rectangle getCollisionRect() {
    return collision;
  }

  int getZOrder() {
    return zOrder;
  }

  Point getVelocity() {
    return velocity;
  }

  void setVelocity(Point vel)
  {
    velocity = vel;
  }

  void update() {
    // Update the position and collision rects
    int w = component.size().width,
        h = component.size().height;
    position.translate(velocity.x, velocity.y);
    if ((position.x + position.width) < 0)
      position.x = w;
    else if (position.x > w)
      position.x = -position.width;
    if ((position.y + position.height) < 0)
      position.y = h;
    else if (position.y > h)
      position.y = -position.height;
    calcCollisionRect();

    // Increment the frame
    incFrame();
  }

  void draw(Graphics g) {
    // Draw the current frame
    g.drawImage(image[frame], position.x, position.y, component);
  }

  protected boolean testCollision(Sprite test) {
    // Check for collision with another sprite
    if (this != test)
      if (collision.intersects(test.getCollisionRect()))
        return true;
    return false;
  }

  protected void calcCollisionRect() {
    // Calculate the collision rect
    collision = new Rectangle(position.x + 4, position.y + 4,
      position.width - 8, position.height - 8);
  }
}

It looks like a lot of code for a simple Sprite class, but take it a method at a time and it's not too bad. First, notice from the member variables that the appropriate sprite information is maintained by the Sprite class. You may also notice a few member variables that aren't related to the core sprite information discussed earlier. The Component member variable is necessary because an ImageObserver object is necessary to retrieve information about an image. What does Component have to do with ImageObserver? The Component class implements the ImageObserver interface. Furthermore, the Applet class is derived from Component. A Sprite object gets its image information from the Java applet itself, which is used to initialize the component member variable.

The frameInc member variable is used to provide a means to change the way the animation frames are updated. For example, there may be instances where you want the frames to be displayed in the reverse order. You can easily do this by setting frameInc to -1 (its typical value is 1). The frameDelay and frameTrigger member variables are used to provide a means of varying the speed of the frame animation. You see how the speed of animation is controlled in a moment when you learn about the incFrame method.

The last member variable in question is collision, which is a Rectangle object. This member variable is used to support shrunken rectangle collision detection, where a smaller rectangle is used in collision detection tests. You see how collision is used a little later when you learn about the testCollision and calcCollisionRect methods.

The Sprite class has two constructors. The first constructor creates a Sprite without frame animations, meaning that it uses a single image to represent the sprite. This constructor takes an image, position, velocity, and Z-order as parameters. The second constructor takes an array of images and some additional information about the frame animations. The additional information includes the current frame, frame increment, and frame delay.

Sprite contains a few access methods, which are simply interfaces to get and set certain member variables. These methods consist of one or two lines of code and are pretty self-explanatory. Let's move on to the juicier methods!

The incFrame method is the first method with any real substance. incFrame is used to increment the current animation frame. It first checks the frameDelay and frameTrigger member variables to see whether the frame should indeed be incremented. This check is what enables you to vary the speed of animation, which is done by changing the value of frameDelay. Larger values for frameDelay result in a slower animation. The current frame is incremented by adding frameInc to frame. frame is then checked to make sure its value is within the bounds of the image array.

The setPosition methods set the position of the sprite. Even though the sprite position is stored as a rectangle, the setPosition methods enable you to specify the sprite position as either a rectangle or a point. In the latter version, the rectangle is calculated based on the dimensions of the sprite image. After the sprite position rectangle is calculated, the collision rectangle is set with a call to calcCollisionRect.

The method that does most of the work in Sprite is the update method. update handles the task of updating the position and animation frame of the sprite. The position of the sprite is updated by translating the position rectangle based on the velocity. You can think of the position rectangle as being slid a distance determined by the velocity. The position of the sprite is then checked against the dimensions of the applet window to see whether it needs to be wrapped around to the other side. Finally, the frame is updated with a call to incFrame.

The draw method simply draws the current frame to the Graphics object that is passed in. Notice that the drawImage method requires the image, x,y position, and component (ImageObserver) to carry this out.

The testCollision method is used to check for collisions between sprites. The sprite to test is passed in the test parameter. The test simply involves checking to see whether the collision rectangles intersect. If so, testCollision returns true. testCollision isn't all that useful within the context of a single sprite, but it will come in very handy when you put together the SpriteVector class a little later in this chapter.

The last method of interest in Sprite is calcCollisionRect, which calculates the collision rectangle from the position rectangle. In this case, the collision rectangle is simply calculated as a smaller version of the position rectangle. However, you could tailor this rectangle to match the images of specific sprites more closely. In this case, you would derive a new sprite class and then override the calcCollisionRect method. A further enhancement could even include an array of collision rectangles that correspond to each animation frame. With this enhancement, you could tighten up the error inherent in rectangle collision detection.

The SpriteVector Class

Now you have a Sprite class with some pretty neat features, but you are still missing a key ingredient-the capability of managing multiple sprites and allowing them to interact with each other. The SpriteVector class, shown in Listing 13.6, is exactly what you need.


Listing 13.6. The SpriteVector class.
// SpriteVector Class
// SpriteVector.java

// Imports
import java.awt.*;
import java.util.*;

public class SpriteVector extends Vector {
  Component component;
  Image     background;

  SpriteVector() {
    super(50, 10);
  }

  SpriteVector(Component comp, Image bg) {
    super(50, 10);
    component = comp;
    background = bg;
  }

  Image getBackground() {
    return background;
  }

  void setBackground(Image back) {
    background = back;
  }

  void update() {
    Sprite    s, hit;
    Rectangle old;
    int       size = size();

    // Iterate through sprites, updating each
    for (int i = 0; i < size; i++) {
      s = (Sprite)elementAt(i);
      old = s.getPositionRect();
      s.update();
      hit = testCollision(s);
      if (hit != null) {
        s.setPosition(old);
        collision(s, hit);
      }
    }
  }

  void draw(Graphics g) {
    if (background != null)
      // Draw background image
      g.drawImage(background, 0, 0, component);
    else {
      // Erase background
      Dimension dim = component.size();
      g.setColor(component.getBackground());
      g.fillRect(0, 0, dim.width, dim.height);
      g.setColor(Color.black);
    }

    // Iterate through sprites, drawing each
    int size = size();
    for (int i = 0; i < size; i++)
      ((Sprite)elementAt(i)).draw(g);
  }

  int add(Sprite s) {
    // Use a binary search to find the right location to insert the
    // new sprite (based on z-order)
    int   l = 0, r = size(), x = 0;
    int   z = s.getZOrder(),
          zTest = z + 1;
    while (r > l) {
      x = (l + r) / 2;
      zTest = ((Sprite)elementAt(x)).getZOrder();
      if (z < zTest)
        r = x;
      else
        l = x + 1;
      if (z == zTest)
        break;
    }
    if (z >= zTest)
      x++;

    insertElementAt(s, x);
    return x;
  }

  Sprite testCollision(Sprite test) {
    // Check for collision with other sprites
    int     size = size();
    Sprite  s;
    for (int i = 0; i < size; i++)
    {
      s = (Sprite)elementAt(i);
      if (s == test)  // don't check itself
        continue;
      if (test.testCollision(s))
        return s;
    }
    return null;
  }

  protected void collision(Sprite s, Sprite hit) {
    // Swap velocities (bounce)
    Point swap = s.getVelocity();
    s.setVelocity(hit.getVelocity());
    hit.setVelocity(swap);
  }
}

SpriteVector has only two member variables, which consist of a background Image object and a Component object for working with the image. There are two constructors for SpriteVector: one with no background and one that supports a background image. The background image serves as a backdrop behind the sprites and can be used to jazz up the animation with little effort.

The SpriteVector class is derived from Vector, which is a container class (similar to an array) that can grow. You may have noticed that both constructors call the Vector superclass constructor and set the default storage capacity and amount to increment the storage capacity should the Vector need to grow.

As in Sprite, update is the key method in SpriteVector because it handles updating all the sprites. This update method iterates through the sprites, calling update on each one. It then calls testCollision to see whether a collision has occurred between sprites. If a collision has occurred, the old position of the collided sprite is restored and the collision method called.

The draw method handles drawing all the sprites, as well as drawing the background if one exists. The background member variable is first checked to see whether the background image should be drawn. If not, the background color of the applet window is used to erase the graphics context. The sprites are then drawn by iterating through the list and calling the draw method for each.

The add method is probably the trickiest method in the SpriteVector class. The add method handles adding new sprites to the sprite list. The catch is that the sprite list must always be sorted according to Z-order. Why? Remember that Z-order is the depth at which sprites appear on the screen. The illusion of depth is established by the order in which the sprites are drawn. This works because sprites drawn later are drawn on top of other sprites, and therefore they appear to be at a higher depth. Sorting the sprite list by Z-order and then drawing them in that order is an effective way to provide the illusion of depth. The add method uses a binary search to find the right spot to add new sprites so that the sprite list remains sorted by Z-order.

The testCollision method is used to test for collisions between a sprite and the rest of the sprites in the sprite list. The sprite to be tested is passed in the test parameter. The sprites are then iterated through and the testCollision method called for each. If a collision is detected, the Sprite object that has been hit is returned from testCollision.

Finally, the collision method is used to handle collisions between two sprites. The action here is to simply swap the velocities of the collided Sprite objects, which results in a bouncing effect. This method is where you could provide specific collision actions. For example, in a game you might want some sprites to explode upon collision.

That wraps up the SpriteVector class. You now not only have a powerful Sprite class, but also a SpriteVector class for managing and providing interactivity between sprites. All that's left is putting these classes to work in a real applet.

Testing the Sprite Classes

You didn't come this far with the sprite stuff not to see some action. Figure 13.9 shows a screen shot of the SpriteTest applet, which shows off the sprite classes you've toiled so hard over.

Figure 13.9 : The SpriteTest sample applet.

The SpriteTest applet uses a SpriteVector object to manage five Sprite objects, two of which use frame animation. Listing 13.7 contains the source code for the SpriteTest applet.


Listing 13.7. The SpriteTest sample applet.
// SpriteTest Class
// SpriteTest.java

// Imports
import java.applet.*;
import java.awt.*;

public class SpriteTest extends Applet implements Runnable {
  Image         offImage, back, ball;
  Image[]       numbers = new Image[10];
  Graphics      offGrfx;
  Thread        animate;
  MediaTracker  tracker;
  SpriteVector  sv;
  int           delay = 83; // 12 fps

  public void init() {
    // Load and track the images
    tracker = new MediaTracker(this);
    back = getImage(getDocumentBase(), "Res/Back.gif");
    tracker.addImage(back, 0);
    ball = getImage(getDocumentBase(), "Res/Ball.gif");
    tracker.addImage(ball, 0);
    for (int i = 0; i < 10; i++) {
      numbers[i] = getImage(getDocumentBase(), "Res/" + i + ".gif");
      tracker.addImage(numbers[i], 0);
    }
  }

  public void start() {
    if (animate == null) {
      animate = new Thread(this);
      animate.start();
    }
  }

  public void stop() {
    if (animate != null) {
      animate.stop();
      animate = null;
    }
  }

  public void run() {
    try {
      tracker.waitForID(0);
    }
    catch (InterruptedException e) {
      return;
    }

    // Create and add the sprites
    sv = new SpriteVector(this, back);
    sv.add(new Sprite(this, numbers, 0, 1, 5, new Point(0, 0),
      new Point(1, 3), 1));
    sv.add(new Sprite(this, numbers, 0, 1, 20, new Point(0, 100),
      new Point(-1, 5), 2));
    sv.add(new Sprite(this, ball, new Point(100, 100),
      new Point(-3, 2), 3));
    sv.add(new Sprite(this, ball, new Point(50, 50),
      new Point(1, -2), 4));
    sv.add(new Sprite(this, ball, new Point(100, 0),
      new Point(4, -3), 5));

    // Update everything
    long t = System.currentTimeMillis();
    while (Thread.currentThread() == animate) {
      sv.update();
      repaint();
      try {
        t += delay;
        Thread.sleep(Math.max(0, t - System.currentTimeMillis()));
      }
      catch (InterruptedException e) {
        break;
      }
    }
  }

  public void update(Graphics g) {
    // Create the offscreen graphics context
    Dimension dim = size();
    if (offGrfx == null) {
      offImage = createImage(dim.width, dim.height);
      offGrfx = offImage.getGraphics();
    }

    // Draw the sprites
    sv.draw(offGrfx);

    // Draw the image onto the screen
    g.drawImage(offImage, 0, 0, null);
  }

  public void paint(Graphics g) {
    if ((tracker.statusID(0, true) & MediaTracker.ERRORED) != 0) {
      // Draw the error rectangle
      g.setColor(Color.red);
      g.fillRect(0, 0, size().width, size().height);
      return;
    }
    if ((tracker.statusID(0, true) & MediaTracker.COMPLETE) != 0) {
      // Draw the offscreen image
      g.drawImage(offImage, 0, 0, null);
    }
    else {
      // Draw the loading message
      Font        font = new Font("Helvetica", Font.PLAIN, 18);
      FontMetrics fm = g.getFontMetrics(font);
      String      str = new String("Loading images...");
      g.setFont(font);
      g.drawString(str, (size().width - fm.stringWidth(str)) / 2,
        ((size().height - fm.getHeight()) / 2) + fm.getAscent());
    }
  }
}

You may notice a lot of similarities between SpriteTest and the Counter5 sample applet developed earlier in this chapter. SpriteTest is very similar to Counter5 because a lot of the same animation support code is required by the sprite classes. Let's look at the aspects of SpriteTest that facilitate the usage of the Sprite and SpriteVector classes; you've already covered the rest.

The first thing to notice is the SpriteVector member variable sv. There are also some extra member variables for a background image and a ball sprite image. The only other change with member variables is the value of the delay member variable. It is set to 83, which results in a frame rate of 12 fps. This faster frame rate is required for more fluid animation, such as sprite animation.

The SpriteVector is created in the run method using the constructor that supports a background image. Five different Sprite objects are then created and added to the sprite vector. The first two sprites use the number images as their animation frames. Notice that these two sprites are created with different frame delay values. You can see the difference when you run the applet because one of the sprites "counts" faster than the other. The run method also updates the sprite vector by calling the update method.

The update method for SpriteTest looks almost like the one in Counter5. The only difference is the call to the SpriteVector's draw method, which draws the background and all the sprites.

Using the sprite classes is as easy as that! You've now seen for yourself how the sprite classes encapsulate all the functionality required to manage both cast- and frame-based animation, as well as providing support for interactivity among sprites via collision detection.

Summary

Although it covered a lot of material, this chapter added a significant array of tools and techniques to your bag of Java tricks. You learned all about animation, including the two major types of animation: frame-based and cast-based. Following up this theory, you saw a frame-based animation applet evolve from a simple example to a powerful and reusable animation template.

Although the frame-based animation example applets are interesting and useful, you learned that sprite animation is where the fun really begins. You saw firsthand how to develop a powerful duo of sprite classes for implementing sprite animation. You then put them to work in a sample applet that involved very little additional overhead.

More than anything, you learned in this chapter that Java animation is both powerful and easy to implement. Using what you learned here, you should be able to add many cool animations to your own Web creations.