June 24, 2018
Hot Topics:

Fun with Java: Sprite Animation, Part 2

  • October 4, 2001
  • By Richard G. Baldwin
  • Send Email »
  • More Articles »

Java Programming, Lecture Notes #1452


Programming in Java doesn't have to be dull and boring.  In fact, it's possible to have a lot of fun while programming in Java.  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 second in of a group of lessons that will teach you how to write animation programs in Java.  The first (and previous) lesson in the group was entitled Fun with Java: Sprite Animation, Part 1.

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.

Colored spherical sea creatures

The first program, which is the program 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.

Many sea creatures can change color

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.

Animated multi-colored 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.

Get your GIF images here

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.

Classes and concepts discussed earlier

In the previous lesson, I introduced you to several classes and concepts that you must understand in order to understand animation in Java.  Included in that discussion were the following classes, which are particularly important to sprite animation:

  • Image
  • Toolkit
  • Graphics
  • MediaTracker
  • Random
I also discussed a number of concepts, including the following, which are particularly important to sprite animation:
  • offscreen graphics contexts
  • coordinates in Java graphics
  • translation origins
  • the drawImage method
  • animation repetition rates
  • pseudo-random numbers
Discussed beginning portions of controlling class

Also, in the previous lesson, I discussed the beginning portions of the controlling class for the program named Animate01(A complete listing of the program is provided near the end of this lesson.)

The plan for this lesson

In this lesson, I will teach you how to instantiate and use a MediaTracker object to track the loading status of Image objects.

I will teach you how to use the getImage method of the Toolkit class to create Image objects from GIF files, and how to register those Image objects on a MediaTracker object.

Finally, I will teach you how to use the waitForID method of the MediaTracker object to force the program to block and wait until the images in a group are either successfully loaded, or until a loading error occurs.

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 7 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.

Will discuss in fragments

As usual, I will discuss the program in fragments.  In addition to the controlling class named Animate01, the program contains several other important classes.  I began the discussion of the controlling class in the previous lesson.  I will continue discussing the controlling class in this lesson picking up with the constructor for the controlling class.

A MediaTracker object

Listing 1 shows the beginning of the constructor for the controlling class.  The first statement in Listing 1 instantiates a new object of the MediaTracker class.

  Animate01() {//constructor
    mediaTracker = 
                new MediaTracker(this);

Listing 1

What is a MediaTracker object?

As explained in some detail in the previous lesson, the MediaTracker class is a utility class, objects of which can be used to track the status of a number of media objects.  An object of the MediaTracker class can help you to cope with the delays involved in loading image files into the computer's memory.

The MediaTracker constructor

As of JDK 1.3, the MediaTracker class has only one constructor whose signature is shown below:

public MediaTracker(Component comp)

This constructor creates a media tracker object to track images for a given component.  The single parameter of type Component is the component on which the images will eventually be drawn.

Will draw images on this object

In this program, the images will be drawn on an object of the controlling class, which extends the Frame class.  Therefore, in the code in Listing 1, a reference to the object being constructed by the controlling class' constructor (this) is passed as a parameter to the constructor for the MediaTracker class.  The resulting MediaTracker object can be used to track images that will eventually be drawn on an object instantiated from the controlling class.

The background image

As shown in Figure 1, the animated spherical sea creatures will be drawn against a background image representing the inside of a fish tank.

The code in Listing 2 creates an object of type Image containing the picture described by the physical file named background02.gif.

    backGroundImage = 

Listing 2

(Instructions were provided earlier in this lesson for capturing a GIF file containing the background image from this HTML document.  Once you have captured the GIF file, in order to make it compatible with the code in Listing 2, you will need to rename the file background02.gif, and store it in the directory containing the class file produced by compiling the controlling class for this program.)

The getImage method of the Toolkit class

The Image object is created in Listing 2 by invoking the getImage method of the Toolkit class, passing the name of the GIF file as a parameter.

As of JDK 1.3, the Toolkit class provides two overloaded versions of the getImage method. One takes a URL as a parameter, while the other (the one used in this program) takes a file name represented by a String object as a parameter.

The getDefaultToolkit method

Before getting into the details of the getImage method, it is appropriate to make a few comments about the getDefaultToolkit method.

The getDefaultToolkit method creates and returns an object of some class that extends the Toolkit class.  This class defines the methods declared in the Toolkit class in such a way as to handle the peculiarities of the platform on which the Java program is running.

Many such methods are declared as abstract methods in the Toolkit class.  In this particular case, we are interested in the overridden version of the method named getImage.

Back to the getImage method

The signature of the getImage method, as declared in the Toolkit class, is as shown below (note that the method is abstract and must be fully defined in the object returned by the getDefaultToolkit method).

public abstract Image getImage(
                     String filename)

Creates Image object from an image file

As of JDK 1.3, the getImage method returns a reference to an object of type Image, which contains pixel data from the specified file.  The file format can be either GIF, JPEG or PNG.

The parameter to this overloaded version of the getImage method is a String object representing the name of a file containing pixel data in a recognized file format.  For the code in Listing 2, this is the file named background02.gif, stored in the same directory as the program file.

Coping with the loading delay

Even when the file is stored on a local hard drive, some finite amount of time is required to find the file, read it, and create the Image object.  Any attempt to use the Image object before that process is completed is very likely to result in problems.  That is where the MediaTracker object instantiated earlier comes into play.

Registering the Image object on the MediaTracker

The code in Listing 3 invokes the addImage method of the MediaTracker class to register the Image object referred to by the reference variable named backGroundImage on the MediaTracker object with an ID value of 0.

                   backGroundImage, 0);

Listing 3

(Note that there is another version of the addImage method that allows for scaling the image as it is being loaded.  That version is not used in this program.)

According to the Sun documentation, the version of the addImage method used in Listing 3

"Adds an image to the list of images being tracked by this media tracker. The image will eventually be rendered at its default (unscaled) size."
Getting Image object status

Having accomplished this, the MediaTracker object can then be queried to determine the loading status of the image.  A variety of methods are provided by the MediaTracker class to support such queries.

When an image is registered for tracking with a MediaTracker object, the second parameter of the addImage method provides an integer identifier value.  Many of the methods that can be used to query the MediaTracker object take the identifier as a parameter.  Those methods provide information about the image(s) having that identifier value to the exclusion of images having other identifier values.

Tracking images as a group

More than one image can be registered with the same identifier value.  In this case, all of the images having the same identifier value will be tracked as a group.  A query based on the common identifier value will return information about the group as a whole rather than returning information about the individual members of the group.

Seven different Image objects

This program uses seven different Image objects based on seven different GIF files.  One of the Image objects is used to create the background shown in Figure 1.  The other six Image objects are used to create the spherical sea creature shown in Figure 1.

An Image object for sea creatures

The code to create and register the Image object used for the background was shown in Listings 2 and 3.  The code to create and register the first of the Image objects used for sea creatures is shown in Listing 4.

    gifImages[0] = 
                      gifImages[0], 0);

Listing 4

This is essentially the same as the code in Listings 2 and 3 with the one exception that the reference to the Image object is stored in an element of an array of type Image[] instead of being stored in an ordinary reference variable.

The GIF file name

The code in Listing 4 also shows the name to be applied to one of the GIF files that you capture from this HTML document: redball.gif.

The remaining Image objects

The code for creating and registering the remaining five Image objects is shown in Listing 5.

    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);

Listing 5

The registration identifier

Note that the identifier value used to register all seven of the Image objects on the MediaTracker object is the same (0).  As a result, the MediaTracker object will track the loading of all seven Image objects as a group.

Listing 5 also shows the names to be applied to the five remaining GIF files that you capture from this HTML file.

Using the MediaTracker object

Listing 6 shows the use of the MediaTracker object to delay execution of the program until all seven of the images have been loaded and are ready for use.

    try {
    }catch (InterruptedException e) {
    }//end catch

Listing 6

The waitForID method

The waitForID method call in Listing 6 causes the program to block and wait for all of the images to complete loading (or for a loading error to occur).  Here is part of what Sun has to say about this method:

"Starts loading all images tracked by this media tracker with the specified identifier. This method waits until all the images with the specified identifier have finished loading.

If there is an error while loading or scaling an image, then that image is considered to have finished loading.  Use the isErrorAny and isErrorID methods to check for errors."

No guarantee of successful loading

The fact that this method returns doesn't guarantee that all of the images identified by the ID value of 0 have successfully loaded.  As indicated by Sun, the occurrence of a loading error will also cause this method to return.

Once the method returns, in order to be completely safe, the program should use one of the error checking methods listed in the quotation from Sun to confirm that an error did not occur.

Another version of the waitForID method

Note that in theory, this method could block and wait forever.  Another version of the waitForID method allows for the specification of a maximum length of time in milliseconds to block before returning, with or without a successful load.


In this lesson, I have explained how to instantiate and use a MediaTracker object to track the loading status of Image objects.

I have explained how to use the getImage method of the Toolkit class to create Image objects from GIF files, and how to register those Image objects on a MediaTracker object.

Finally, I explained how to use the waitForID method of the MediaTracker object to force the program to block and wait until the images in a group are either successfully loaded, or until a loading error occurs.

What's Next?

In the next lesson, I will continue explaining the constructor for the controlling class.  The first topic in that lesson will deal with the methodology used to cause the size of the Frame object to be based on the size of the Image object used as a background.

Complete Program Listing

A complete listing of the program is provided in Listing 7.
/*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 7

About the author

Richard Baldwin is a college professor 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.


Comment and Contribute


(Maximum characters: 1200). You have characters left.



Enterprise Development Update

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

By submitting your information, you agree that developer.com may send you developer offers via email, phone and text message, as well as email offers about other products and services that developer believes may be of interest to you. developer will process your information in accordance with the Quinstreet Privacy Policy.


We have made updates to our Privacy Policy to reflect the implementation of the General Data Protection Regulation.
Thanks for your registration, follow us on our social networks to keep up-to-date