February 28, 2021
Hot Topics:

Fun with Java: Sprite Animation, Part 5

  • By Richard G. Baldwin
  • Send Email »
  • More Articles »

Java Programming, Lecture Notes #1458


Why the repeated introduction?

If you are one of those orderly people who start reading a book at the beginning and reads through to the end, you are probably wondering why I keep repeating this long introduction.  The truth is that this introduction isn't meant for you.  Rather, it is meant for those people who start reading in the middle.

Having said that, this is one of the lessons in a miniseries that will concentrate on having fun while programming in Java.

This miniseries will include a variety of Java programming topics that fall in the category of fun programming.  This particular lesson is the fifth in of a group of lessons that teach you how to write animation programs in Java.

The first lesson in the group was entitled Fun with Java: Sprite Animation, Part 1(Here is your opportunity to go back and start reading at the beginning.) The previous lesson was entitled Fun with Java: Sprite Animation, Part 4.

Viewing tip

You may find it useful to open another copy of this lesson in a separate browser window.  That will make it easier for you to scroll back and forth among the different figures and listings while you are reading about them.

Supplementary material

I recommend that you also study the other lessons in my extensive collection of online Java tutorials.  You will find those lessons published at Gamelan.com.  However, as of the date of this writing, Gamelan doesn't maintain a consolidated index of my Java tutorial lessons, and sometimes they are difficult to locate there.  You will find a consolidated index at Baldwin's Java Programming Tutorials.


This is one of a group of lessons that will teach you how to write animation programs in Java.  These lessons will teach you how to write sprite animation, frame animation, and a combination of the two.

Animated spherical sea creatures

The first program, being discussed in this lesson, will show you how to use sprite animation to cause a group of colored spherical sea creatures to swim around in a fish tank.  A screen shot of the output produced by this program is shown in Figure 1.

Figure 1.  Animated spherical sea creatures in a fish tank.

A creature of many colors

Many sea creatures have the ability to change their color in very impressive ways.  The second program that I will discuss in subsequent lessons will simulate that process using a combination of sprite and frame animation.

Slithering sea worms

The third program, also to be discussed in a subsequent lesson, will use a combination of sprite animation, frame animation, and some other techniques to cause a group of multi-colored sea worms to slither around in the fish tank.  In addition to slithering, the sea worms will also change the color of different parts of their body, much like real sea creatures.

A screen shot of the output from the third program is shown in Figure 2.

Figure 2.  Animated sea worms in a fish tank.

Getting the required GIF images

Figure 3 shows the GIF image files that you will need to run these three programs.


Figure 3.  GIF image files that you will need.

You should be able to capture the images by right-clicking on them individually, and then saving them into files on your local disk.  Having done that, you will need to rename the files to match the names that are hard-coded into the programs.

Review of previous lesson

In the previous lesson, I explained the behavior of the run method of the animation thread as well as the makeSprite method of the controlling class.

I provided a preview of the SpriteManager class, which will be discussed in detail in a subsequent lesson.  I also provided a brief preview of the Sprite class, which will be discussed in detail in a subsequent lesson.

I discussed the repaint, update, and paint methods of the Component class.  I also discussed the timer loop used in this program, and suggested an alternative approach that makes use of a Timer object to fire Action events.

Also in the previous lesson, I provided a complete recap of everything that we have learned in the first four lessons of this series.

What are the plans for this lesson?

There are only two methods remaining to be discussed in the controlling class:  update and paint.  In this lesson, I will explain the behavior of the overridden update and paint methods.  As explained in the previous lesson, the update method is invoked by the operating system in response to a repaint request on the Frame.

Discussion and Sample Program

This program is so long that several lessons will be required to discuss it fully.  Rather than to make you wait until I complete all of those lessons to get your hands on the program, I have provided a copy of the entire program in Listing 6 near the end of the lesson.  That way, you can copy it into a source file on your local disk, compile it, run it, and start seeing the results.

Discuss in fragments

As usual, I will discuss the program in fragments.  As explained in the previous lesson, once during each iteration of the animation loop, the code updates the positions of all of the sprites and then invokes the repaint method on the Frame.  Then it sleeps for a specified period, wakes up, and does it all over again.

What is the behavior of the repaint method?

According to the Sun documentation, invoking the repaint method on the Frame causes a call to be made to the frame's update method as soon as possible.

What is the behavior of the update method?

Sun describes the default behavior of the update method as follows:

"The update method of Component does the following:
    • Clears this component by filling it with the background color.
    • Sets the color of the graphics context to be the foreground color of this component.
    • Calls this component's paint method to completely redraw this component."
Doesn't use default update method

However, this animation program does not use the default behavior of the update method.  Rather, this program overrides the update method to provide behavior that is appropriate for better-quality animation.

Three major changes

This program makes three major changes to the default behavior of the update method to improve the animation quality.

Eliminate flashing

First, the new behavior eliminates the clearing of the display area of the Frame object at the beginning of each redraw operation.  This eliminates a flashing effect that is often produced by that default operation.

Use double buffering

Second, the new behavior draws the scene on an offscreen graphics context and then transfers that scene onto the screen context at high speed. (This is often referred to as double buffering.)  This makes it impossible for the viewer to see the scene as it is being drawn, and usually provides a more pleasing result.

No need for the paint method

Third, since all of the required drawing is accomplished in the update method, there is no call to the paint method by the update method.  Although an overridden version of the paint method is provided, it doesn't do anything, and it isn't invoked by the animation process.

An offscreen graphics context

The code fragment in Listing 1 gets an offscreen graphics context to be used as described above.

  public void update(Graphics g) {
    if (offScreenGraphicsCtx == null) {
      offScreenImage = 
      offScreenGraphicsCtx = 
    }//end if

Listing 1

As you can see, there are two steps involved in getting a useful offscreen graphics context.

  1. Create an Image object
  2. Create a Graphics object

Create an Image object

The first step is to invoke the createImage method of the Component class to get an Image object of a specified size.  This method requires the width and height of the image as parameters and returns a reference to an object of type Image.

Create a Graphics object

The second step is to invoke the getGraphics method on the Image object returned by the first step.  This method returns a reference to an object of type Graphics.  Here is what Sun has to say about the method:

"Creates a graphics context for drawing to an off-screen image. This method can only be called for off-screen images."
Drawing offscreen

Once we have the offscreen graphics context, we can invoke any of the methods of the Graphics class to draw pictures on that context.

In this case, we don't draw the pictures within the update method directly.  Rather, we pass a reference to the offscreen context to the drawScene method of the SpriteManager object where the drawing is actually accomplished (you will see how the scene is drawn in a subsequent lesson where I discuss the SpriteManager class in detail).

The call to the drawScene method of the SpriteManager class is shown in Listing 2.


Listing 2

Drawing onscreen

When the drawScene method returns, the scene has been drawn onto the offscreen graphics context, and it is time to copy it to the screen context at a high rate of speed.  This is accomplished using the drawImage method of the Graphics class, as shown in Listing 3.

    if(offScreenImage != null){
           offScreenImage, 0, 0, this);
    }//end if
  }//end overridden update method

Listing 3

The drawImage method is overloaded

There are several overloaded versions of the drawImage method.  The version used here requires four parameters.  The first parameter is a reference to an offscreen graphics context.

The second and third parameters are the coordinate values of the screen image where the top left-hand corner of the offscreen image will be positioned.  In this case, the top left-hand corner of the offscreen image will be placed at the top left-hand corner of the screen image.

The last parameter is a reference to an ImageObserver object.  I have probably already confused you enough in an earlier lesson regarding the use of this as an ImageObserver object.  I won't add to that confusion by discussing it further here.  (Again, however, I plan to dedicate an entire future lesson to the concept of ImageObserver.)

Does not invoke the paint method

Note that the code in the overridden update method does not invoke the paint method, as is the case with the default version of the update method.  All of the required drawing is handled in the update method, and there is nothing further to be drawn by the paint method.  As a result, the overridden paint method shown in Listing 4 below is not used in the animation process.

  public void paint(Graphics g) {
    //Nothing required here.  All 
    // drawing is done in the update 
    // method.
  }//end overridden paint method
}//end class Animate01

Listing 4

That completes my discussion of all the methods of the controlling class.

The BackgroundImage class

Listing 5 contains all of the code for a utility class named BackgroundImage.  As I mentioned in an earlier lesson, this class was written simply to make it a little easier to deal with the background image.

class BackgroundImage{
  private Image image;
  private Component component;
  private Dimension size;

  public BackgroundImage(
                  Component component,
                  Image image) {
    this.component = component;
    size = component.getSize();
    this.image = image;
  }//end construtor
  public Dimension getSize(){
    return size;
  }//end getSize()

  public Image getImage(){
    return image;
  }//end getImage()

  public void setImage(Image image){
    this.image = image;
  }//end setImage()

  public void drawBackgroundImage(
                          Graphics g){
              image, 0, 0, component);
  }//end drawBackgroundImage()
}//end class BackgroundImage

Listing 5

A very simple class

As you can see from Listing 5, there isn't much to this class, so it doesn't deserve much in the way of a discussion.

Basically, an object instantiated from this class has the ability to

  • Store a reference to a background Image object,
  • Return a reference to the object,
  • Return the size of the object
  • Cause the background image to be drawn on a specified graphics context
Beyond that, there isn't much to be said for this class.


It has taken five lessons, but I have finally completed my discussion of the controlling class for this animation program.

In this lesson, I showed you how to override the update method of the Component class to improve the animation quality of the program over what would normally be achieved using the default version of the update method.

In the process, I showed you how to eliminate the flashing that often accompanies efforts to use the default version of the update method for animation purposes.  This flashing is caused by the fact that the default version of update draws an empty component (often white) at the beginning of each redraw cycle.

I also showed you how to get and use an offscreen drawing context to accomplish double buffering in the drawing process.  The use of double buffering makes it impossible for the user to see the scene as it is being drawn because the scene is first drawn offscreen and then transferred as a whole to the screen context.  Depending on the drawing speed, this can also produce a more pleasing result.

I also provided a very brief discussion of the utility class named BackgroundImage.

What's Next?

There are two more classes to cover before my discussion of this animation program is complete:  SpriteManager and Sprite.

I will begin my discussion of the SpriteManager class in the next lesson.

Complete Program Listing

A complete listing of the program is provided in Listing 6.
/*File Animate01.java
Copyright 2001, R.G.Baldwin

This program displays several animated
colored spherical creatures swimming 
around in an aquarium.  Each creature 
maintains generally the same course
with until it collides with another 
creature or with a wall.  However, 
each creature has the ability to 
occasionally make random changes in 
its course.

import java.awt.*;
import java.awt.event.*;
import java.util.*;

public class Animate01 extends Frame 
                  implements Runnable {
  private Image offScreenImage;
  private Image backGroundImage;
  private Image[] gifImages = 
                          new Image[6];
  //offscreen graphics context
  private Graphics 
  private Thread animationThread;
  private MediaTracker mediaTracker;
  private SpriteManager spriteManager;
  //Animation display rate, 12fps
  private int animationDelay = 83;
  private Random rand = 
                new Random(System.
  public static void main(
                        String[] args){
    new Animate01();
  }//end main

  Animate01() {//constructor
    // Load and track the images
    mediaTracker = 
                new MediaTracker(this);
    //Get and track the background 
    // image
    backGroundImage = 
                   backGroundImage, 0);
    //Get and track 6 images to use 
    // for sprites
    gifImages[0] = 
                      gifImages[0], 0);
    gifImages[1] = 
                      gifImages[1], 0);
    gifImages[2] = 
                      gifImages[2], 0);
    gifImages[3] = 
                      gifImages[3], 0);
    gifImages[4] = 
                      gifImages[4], 0);
    gifImages[5] = 
                      gifImages[5], 0);
    //Block and wait for all images to 
    // be loaded
    try {
    }catch (InterruptedException e) {
    }//end catch
    //Base the Frame size on the size 
    // of the background image.
    //These getter methods return -1 if
    // the size is not yet known.
    //Insets will be used later to 
    // limit the graphics area to the 
    // client area of the Frame.
    int width = 
    int height = 

    //While not likely, it may be 
    // possible that the size isn't
    // known yet.  Do the following 
    // just in case.
    //Wait until size is known
    while(width == -1 || height == -1){
                  "Waiting for image");
      width = backGroundImage.
      height = backGroundImage.
    }//end while loop
    //Display the frame
        "Copyright 2001, R.G.Baldwin");

    //Create and start animation thread
    animationThread = new Thread(this);
    //Anonymous inner class window 
    // listener to terminate the 
    // program.
                   new WindowAdapter(){
      public void windowClosing(
                        WindowEvent e){
  }//end constructor

  public void run() {
    //Create and add sprites to the 
    // sprite manager
    spriteManager = new SpriteManager(
             new BackgroundImage(
               this, backGroundImage));
    //Create 15 sprites from 6 gif 
    // files.
    for (int cnt = 0; cnt < 15; cnt++){
      Point position = spriteManager.
        getEmptyPosition(new Dimension(
        makeSprite(position, cnt % 6));
    }//end for loop

    //Loop, sleep, and update sprite 
    // positions once each 83 
    // milliseconds
    long time = 
    while (true) {//infinite loop
      try {
        time += animationDelay;
        Thread.sleep(Math.max(0,time - 
      }catch (InterruptedException e) {
      }//end catch
    }//end while loop
  }//end run method
  private Sprite makeSprite(
      Point position, int imageIndex) {
    return new Sprite(
          new Point(rand.nextInt() % 5,
                  rand.nextInt() % 5));
  }//end makeSprite()

  //Overridden graphics update method 
  // on the Frame
  public void update(Graphics g) {
    //Create the offscreen graphics 
    // context
    if (offScreenGraphicsCtx == null) {
      offScreenImage = 
      offScreenGraphicsCtx = 
    }//end if
    // Draw the sprites offscreen

    // Draw the scene onto the screen
    if(offScreenImage != null){
           offScreenImage, 0, 0, this);
    }//end if
  }//end overridden update method

  //Overridden paint method on the 
  // Frame
  public void paint(Graphics g) {
    //Nothing required here.  All 
    // drawing is done in the update 
    // method above.
  }//end overridden paint method
}//end class Animate01

class BackgroundImage{
  private Image image;
  private Component component;
  private Dimension size;

  public BackgroundImage(
                  Component component, 
                  Image image) {
    this.component = component;
    size = component.getSize();
    this.image = image;
  }//end construtor
  public Dimension getSize(){
    return size;
  }//end getSize()

  public Image getImage(){
    return image;
  }//end getImage()

  public void setImage(Image image){
    this.image = image;
  }//end setImage()

  public void drawBackgroundImage(
                          Graphics g) {
               image, 0, 0, component);
  }//end drawBackgroundImage()
}//end class BackgroundImage

class SpriteManager extends Vector {
  private BackgroundImage 

  public SpriteManager(
     BackgroundImage backgroundImage) {
    this.backgroundImage = 
  }//end constructor
  public Point getEmptyPosition(
                 Dimension spriteSize){
    Rectangle trialSpaceOccupied = 
      new Rectangle(0, 0, 
    Random rand = 
         new Random(
    boolean empty = false;
    int numTries = 0;

    // Search for an empty position
    while (!empty && numTries++ < 100){
      // Get a trial position
      trialSpaceOccupied.x = 
        Math.abs(rand.nextInt() %

      trialSpaceOccupied.y = 
        Math.abs(rand.nextInt() %

      // Iterate through existing 
      // sprites, checking if position 
      // is empty
      boolean collision = false;
      for(int cnt = 0;cnt < size();
        Rectangle testSpaceOccupied = 
        if (trialSpaceOccupied.
          collision = true;
        }//end if
      }//end for loop
      empty = !collision;
    }//end while loop
    return new Point(
  }//end getEmptyPosition()
  public void update() {
    Sprite sprite;
    //Iterate through sprite list
    for (int cnt = 0;cnt < size();
      sprite = (Sprite)elementAt(cnt);
      //Update a sprite's position

      //Test for collision. Positive 
      // result indicates a collision
      int hitIndex = 
      if (hitIndex >= 0){
        //a collision has occurred
      }//end if
    }//end for loop
  }//end update
  private int testForCollision(
                   Sprite testSprite) {
    //Check for collision with other 
    // sprites
    Sprite  sprite;
    for (int cnt = 0;cnt < size();
      sprite = (Sprite)elementAt(cnt);
      if (sprite == testSprite)
        //don't check self
      //Invoke testCollision method 
      // of Sprite class to perform
      // the actual test.
      if (testSprite.testCollision(
        //Return index of colliding 
        // sprite
        return cnt;
    }//end for loop
    return -1;//No collision detected
  }//end testForCollision()
  private void bounceOffSprite(
                    int oneHitIndex,
                    int otherHitIndex){
    //Swap motion vectors for 
    // bounce algorithm
    Sprite oneSprite = 
    Sprite otherSprite = 
    Point swap = 
  }//end bounceOffSprite()
  public void drawScene(Graphics g){
    //Draw the background and erase 
    // sprites from graphics area
    //Disable the following statement 
    // for an interesting effect.

    //Iterate through sprites, drawing
    // each sprite
    for (int cnt = 0;cnt < size();
  }//end drawScene()
  public void addSprite(Sprite sprite){
  }//end addSprite()
}//end class SpriteManager

class Sprite {
  private Component component;
  private Image image;
  private Rectangle spaceOccupied;
  private Point motionVector;
  private Rectangle bounds;
  private Random rand; 

  public Sprite(Component component,
                Image image,
                Point position,
                Point motionVector){

    //Seed a random number generator 
    // for this sprite with the sprite
    // position.
    rand = new Random(position.x);
    this.component = component;
    this.image = image;
    setSpaceOccupied(new Rectangle(
    this.motionVector = motionVector;
    //Compute edges of usable graphics
    // area in the Frame.
    int topBanner = (
    int bottomBorder = 
    int leftBorder = (
    int rightBorder = (
    bounds = new Rectangle(
         0 + leftBorder,
         0 + topBanner,
         component.getSize().width - 
            (leftBorder + rightBorder),
         component.getSize().height -
           (topBanner + bottomBorder));
  }//end constructor

  public Rectangle getSpaceOccupied(){
    return spaceOccupied;
  }//end getSpaceOccupied()
  void setSpaceOccupied(
              Rectangle spaceOccupied){
    this.spaceOccupied = spaceOccupied;
  public void setSpaceOccupied(
                       Point position){
               position.x, position.y);
  public Point getMotionVector(){
    return motionVector;
  }//end getMotionVector()
  public void setMotionVector(
                   Point motionVector){
    this.motionVector = motionVector;
  }//end setMotionVector()
  public void setBounds(
                     Rectangle bounds){
    this.bounds = bounds;
  }//end setBounds()
  public void updatePosition() {
    Point position = new Point(
     spaceOccupied.x, spaceOccupied.y);
    //Insert random behavior.  During 
    // each update, a sprite has about
    // one chance in 10 of making a 
    // random change to its 
    // motionVector.  When a change 
    // occurs, the motionVector
    // coordinate values are forced to
    // fall between -7 and 7.  This 
    // puts a cap on the maximum speed
    // for a sprite.
    if(rand.nextInt() % 10 == 0){
      Point randomOffset = 
         new Point(rand.nextInt() % 3,
                   rand.nextInt() % 3);
      motionVector.x += randomOffset.x;
      if(motionVector.x >= 7) 
                   motionVector.x -= 7;
      if(motionVector.x <= -7) 
                   motionVector.x += 7;
      motionVector.y += randomOffset.y;
      if(motionVector.y >= 7) 
                   motionVector.y -= 7;
      if(motionVector.y <= -7) 
                   motionVector.y += 7;
    }//end if
    //Move the sprite on the screen
       motionVector.x, motionVector.y);

    //Bounce off the walls
    boolean bounceRequired = false;
    Point tempMotionVector = new Point(

    //Handle walls in x-dimension
    if (position.x < bounds.x) {
      bounceRequired = true;
      position.x = bounds.x;
      //reverse direction in x
      tempMotionVector.x = 
    }else if ((
      position.x + spaceOccupied.width)
          > (bounds.x + bounds.width)){
      bounceRequired = true;
      position.x = bounds.x + 
                  bounds.width - 
      //reverse direction in x
      tempMotionVector.x = 
    }//end else if
    //Handle walls in y-dimension
    if (position.y < bounds.y){
      bounceRequired = true;
      position.y = bounds.y;
      tempMotionVector.y = 
    }else if ((position.y + 
         > (bounds.y + bounds.height)){
      bounceRequired = true;
      position.y = bounds.y + 
                 bounds.height - 
      tempMotionVector.y = 
    }//end else if
      //save new motionVector
    //update spaceOccupied
  }//end updatePosition()
  public void drawSpriteImage(
                           Graphics g){
  }//end drawSpriteImage()
  public boolean testCollision(
                    Sprite testSprite){
    //Check for collision with 
    // another sprite
    if (testSprite != this){
      return spaceOccupied.intersects(
    }//end if
    return false;
  }//end testCollision
}//end Sprite class

Listing 6

About the author

Richard Baldwin is a college professor (at Austin Community College in Austin, TX) and private consultant whose primary focus is a combination of Java and XML. In addition to the many platform-independent benefits of Java applications, he believes that a combination of Java and XML will become the primary driving force in the delivery of structured information on the Web.

Richard has participated in numerous consulting projects involving Java, XML, or a combination of the two.  He frequently provides onsite Java and/or XML training at the high-tech companies located in and around Austin, Texas.  He is the author of Baldwin's Java Programming Tutorials, which has gained a worldwide following among experienced and aspiring Java programmers. He has also published articles on Java Programming in Java Pro magazine.

Richard holds an MSEE degree from Southern Methodist University and has many years of experience in the application of computer technology to real-world problems.


This article was originally published on October 25, 2001

Enterprise Development Update

Don't miss an article. Subscribe to our newsletter below.

Thanks for your registration, follow us on our social networks to keep up-to-date