November 22, 2014
Hot Topics:

Fun with Java: Sprite Animation, Part 1

  • September 27, 2001
  • By Richard G. Baldwin
  • Send Email »
  • More Articles »

Java Programming, Lecture Notes #1450


Preface

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 the first lesson in a miniseries that will concentrate on having fun while programming in Java.

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.

Preview

Animation is fun

When it comes to having fun while programming, it's hard to beat a good old fashioned program that provides visual feedback and stimulation.  And in that category, it's hard to beat an animation program.

This is the first of several 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.  Once you know how to do animation, there are lots of ways to put that knowledge to use.  For example, you could use that newfound knowledge to write some neat game programs.  Or, you could take your newfound knowledge and use it to explore the world of Artificial Life.

Descriptions of upcoming programs

The first program that I will discuss in this and the next few lessons will show you how to write a program in which you animate a group of colored spherical sea creatures swimming 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.

Uses sprite animation

This program will use sprite animation to cause the spherical creatures to swim around.  Of course, the screen shot doesn't do justice to the effect that you will see when you run the program in its animated form.

Using frame animation to change colors

If you watch The Discovery Channel or The Learning Channel very much, you will already know that many sea creatures have the ability to change their color in very impressive ways.  The second program that I will discuss will simulate that process.  It will use sprite animation to cause the spherical creatures to swim, and will also use frame animation to cause them to change their color at the same time.  Since a screen shot can't show the creatures changing colors, a screen shot of the second program would look very similar to the screen shot in Figure 1 above.  Therefore, I didn't provide a screen shot of the second program.

How about some sea worms?

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

Figure 2.  Animated sea worms in a fish tank.

This program 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 the real sea creatures that have this amazing ability to change the colors on their bodies do.

The required GIF files

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 various images from Figure 3 by right-clicking on them individually, and then saving them into files on your local disk.

Rename the captured files

Having done that, you will need to rename the files to match the names that are hard-coded into the programs (or change the names in the programs to match the names of your files).

Important classes

In this lesson, I will introduce you to several classes and concepts that you must understand in order to understand animation in Java.

Included in the classes that I will discuss will be the following, which are particularly important to sprite animation:

  • Image
  • Toolkit
  • Graphics
  • MediaTracker
  • Random
Important concepts

I will also discuss 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
Preview of control structure

Here is a preview of the control structure that I will use for this animation program.

The controlling class extends the Frame class and implements the Runnable interface.  Thus, an object of the controlling class is used to provide the visual manifestation of the program as a visual Frame object.  An object of the controlling class is also suitable for using as an animation thread, which controls the overall behavior of the animation process.  In other words, an object of the controlling class acts both as the director of the play, and the stage upon which the play is performed.

The main method of the controlling class instantiates an object of the controlling class, thus causing the constructor for the controlling class to be executed.

Objects of type Image

The constructor for the controlling class causes seven Image objects to be created.  Each Image object is based on the pixel contents of a GIF file.

One of the Image objects is used to produce the background scenery against which the animation is played out.  The other six Image objects are used to provide the visual manifestation of the sprites.

Each Image object provides the visual manifestation for more than one sprite.  Therefore, some of the sprites look alike (twins in some cases and triplets in others).

Set the Frame size

After the Image objects have been created, the size of the Image object used for the background scenery is used by the constructor to set the size of the Frame.  Then the Frame is made visible.

Start the animation thread

Finally, the constructor creates the animation thread and starts it running.  From this point forward, the run method of the controlling class controls the animation behavior of the program.

The run method

The run method begins by creating and populating a SpriteManager object.  An object of the SpriteManager class is capable of managing a collection of sprites, causing them to update their positions on demand, and dealing with collisions between the sprites.

The SpriteManager object

The SpriteManager object is populated with fifteen separate Sprite objects.  Each sprite has a visual manifestation based on one of the six Image objects.  Each sprite also has an initial position based on a random number and has a motion vector whose components are also based on random numbers.  The motion vector is used to determine the next position of the sprite when the sprite is told by the SpriteManager to change its position.

The animation loop

Then the run method enters an infinite loop, iterating approximately twelve times per second.  At the beginning of each iteration, the SpriteManager is told to update the positions of all of the sprites in its collection.  It does so, dealing with collisions in the process.

The run method sends a message to the operating system asking it to repaint the Frame object on the screen.

The upDate method

When the operating system honors the request to repaint, it invokes the upDate method on the Frame object, (which normally does some initialization and then invokes the paint method).

The update method is overridden in this program to cause the new scene to be drawn in its entirety, showing each of the sprites in its new position superimposed upon the background image.  Note that in this case, the update method does not invoke the paint method, because there is nothing for the paint method to do.

An offscreen image

When drawing the scene, the update method first draws the scene on an offscreen graphics context, and then causes the scene to be transferred from that context to the screen context.  This is done to improve the animation quality of the program.

Discussion and Sample Program

That's enough of the preliminaries.  It's time to get down to business and start discussing code.

A fairly long program

This is a fairly long program.  It is so long, in fact, that several lessons will be required to discuss it fully.  However, 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 immediately.

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 will discuss the controlling class in this lesson and defer my discussion of the other classes until future lessons.  In fact, the controlling class itself is quite long, so I will partition the discussion of the controlling class into several consecutive lessons as well.

Acknowledgment

Before getting into the details, I want to acknowledge that some of the techniques used in this program, such as the animation timer and the collision detector, were taken from the book entitled Teach Yourself Internet Game Programming with Java in 21 days, by Michael Morrison.

The copy of the book that I have is the first edition (I don't know if there are later editions) and is somewhat dated by now (for example, it uses the original JDK 1.0 event model).  However, even though Java has been updated significantly since the publication of the book, some techniques discussed in the book are still appropriate for use.

In addition, the book provides a good discussion of the benefits of Object-Oriented Programming.  That information is beneficial to anyone embarking on a career as a Java programmer.

The controlling class

The beginning of the class definition for the controlling class named Animate01 is shown in Listing 1.
 

public class Animate01 extends Frame 
                   implements Runnable{
  private Image offScreenImage;
  private Image backGroundImage;
  private Image[] gifImages = 
                          new Image[6];

Listing 1

Extends the Frame class

As you can see, the controlling class extends the Frame class (extending JFrame would work just as well provided that you take the Swing content pane into account).  This causes an object instantiated from the controlling class to be suitable as a drawing surface for the animation.  Thus, the animation images are presented directly on the surface of the Frame as shown in Figure 1 and Figure 2.

Implements the Runnable interface

The controlling class also implements the Runnable interface.  This makes it suitable for use as a Thread object.  We will see later that the animation loop is actually implemented inside the run method of the controlling class.

The Image Class

The code in Listing 1 declares three reference variables.  The first two are reference variables of the type Image.  The third is a reference variable that refers to an array object containing six references to objects of type Image.  From this, you might surmise that an understanding of the Image class is important to this type of animation, and if so, you are correct.

What does Sun have to say about the Image class?

Here is part of what Sun has to say about the Image class:

"The abstract class Image is the superclass of all classes that represent graphical images. The image must be obtained in a platform-specific manner."
Because Image is an abstract class, we can't directly instantiate objects of the class.  We will see later that we obtain our objects of type Image using a roundabout approach involving the Toolkit class.  (I will have more to say about that later.)

The Toolkit class

For the time being, suffice it to say the Toolkit class makes it possible to gain access to system-dependent resources using system-independent code.

(Other examples of the use of the Toolkit class have to do with the system event queue, and access to system printers, which I discuss at length in other lessons.)

Getting Image objects

We will get our Image objects by invoking one of the overloaded getImage methods of the Toolkit class.  Once we get an Image object, we really won't know the name of the class from which it was instantiated.  Furthermore, we won't care about the name of the class from which it was instantiated.  We will know simply that we can treat it as type Image and let polymorphic behavior take care of us.

(Hopefully, you already know all about polymorphic behavior.  If not, I discuss it in detail in several other lessons, including the lessons on the Collections Framework.)

Using Image objects

The Image class (and the classes that extend it) define (or override) a number of useful methods that we will use throughout the program.  This will include the methods named getGraphics, getWidth, and getHeight.

The Graphics class

The code in Listing 2 declares two more reference variables.  Of particular interest at this point is the reference variable of type Graphics.  This particular variable will be used to refer to an object that will serve as offscreen graphics context.
 

  private Graphics 
                  offScreenGraphicsCtx;
  private Thread animationThread;

Listing 2

What is an offscreen graphics context?

Put simply, in this program, an offscreen graphics context is an area of memory that serves as a stand-in for the computer screen.

We use the methods of the Graphics class to draw pictures in that memory without disturbing the pictures currently showing on the computer screen.

Why use an offscreen graphics context?

Then we can blast the pictures from the offscreen graphics context to the actual computer screen very rapidly.

This is an important capability for animation.  A noticeable amount of time is often required to create a picture.  Because this approach doesn't disturb the visible image during the time required to create the picture, it usually results in smoother animation than can be achieved by creating and drawing the pictures directly on the computer screen.  It eliminates the flashing and other distractions that can occur when the material is being displayed as it is being created.

What does Sun have to say about the Graphics class?

The sun documentation has quite a lot to say about the Graphics class in general.  Here is a brief sampling:

"The Graphics class is the abstract base class for all graphics contexts that allow an application to draw onto components that are realized on various devices, as well as onto offscreen images."
For example, printing in Java involves the use of methods of the Graphics class to draw pictures on the paper in the printer.  It doesn't matter whether those pictures represent landscapes or letters; they are pictures nonetheless.  In that sense, the printer paper can be thought of as a graphics context.

Our graphics contexts

In this program, we will be particularly interested in two graphics contexts:

  • The computer screen.
  • An offscreen image.
More info from Sun

Here is more of what Sun has to say about the Graphics class:

"A Graphics object encapsulates state information needed for the basic rendering operations that Java supports. This state information includes the following properties ..."
Sun goes on to list several properties, which won't be too important to us in this lesson.

Location, width, and height

In this lesson, we will frequently be working with the location, width, and height of images.  This requires some knowledge of how coordinate positions are treated.  In this regard, Sun says:

"All coordinates that appear as arguments to the methods of this Graphics object are considered relative to the translation origin of this Graphics object prior to the invocation of the method."
What is a translation origin?

By default, the plotting origin of a graphics surface is the upper left-hand corner of the surface on which the plotting is being performed.  That origin can be translated to a different spot (the translation origin), but none of the code in this lesson does that.

Positive horizontal coordinates progress from left to right across the graphics surface (relative to the origin).  Positive vertical coordinates progress from top to bottom down the surface (relative to the origin).

(The translation origin for the images produced by this program is the upper-left corner of the Frame object.)

The drawImage methods

The Graphics class, (and its subclass named Graphics2D) provide dozens of methods that can be used to draw pictures on a graphics context.  However, most of those methods have to do with drawing lines, circles, polygons, etc.

Only about eight methods are provided for drawing images, and most of those methods are overloaded versions of the method named drawImage.  The drawImage method will surely become our friend in this and the next few lessons.

The Thread class

The other reference variable declared in the code in Listing 2 is of type Thread.  Hopefully you already know all about Java threads.  If not, I have published several lessons explaining the use of threads on my web site, and you should probably refer to them before getting too far into this program.

The MediaTracker class

The variable declaration in Listing 3 exposes one of the more abstract issues involved in this program, the MediaTracker class.

The primary purpose of the MediaTracker class is to help you deal with time delays that may occur when loading image data into memory.  If the images are being loaded via the Internet, those time delays can be quite long.  Even if the images are being loaded from a local hard drive, the delays can be long enough to be troublesome.
 

  private MediaTracker mediaTracker;

Listing 3

In other words, when you are using images, you need to know the load status of each image before you try to use it.  If it hasn't finished loading, you must be careful what you try to do with it.

What does Sun have to say about MediaTracker?

Here is part of what the Sun documentation for JDK 1.3 has to say about the MediaTracker class:

"The MediaTracker class is a utility class to track the status of a number of media objects. Media objects could include audio clips as well as images, though currently only images are supported.

To use a media tracker, create an instance of MediaTracker and call its addImage method for each image to be tracked.

In addition, each image can be assigned a unique identifier. This identifier controls the priority order in which the images are fetched. It can also be used to identify unique subsets of the images that can be waited on independently. Images with a lower ID are loaded in preference to those with a higher ID number."

How do you use a MediaTracker object?

Once you have registered an image with a MediaTracker object (using the addImage method and identifying the image with a specific ID value), there are several methods that you can invoke on the MediaTracker object to learn the current status of the image.

Some of the methods allow you to manipulate the images in other ways, such as unregistering an image using the removeImage method.

The MediaTracker methods

Here is a partial list of the available methods (note that, as usual, some of the methods have several overloaded versions).

  • checkAll
  • checkID
  • getErrorsAny
  • getErrorsID
  • isErrorAny
  • isErrorID
  • removeImage
  • statusAll
  • statusID
  • waitForAll
  • waitForID
The names of these methods are fairly descriptive, so you should be able to surmise what most of them do.

I will use some of these methods in this program to track the loading of GIF images that are used for the background graphic and the sprites.

The SpriteManager class

Listing 4 shows the declaration of three additional instance variables.
 

  private SpriteManager spriteManager;
  //Animation display rate, 12fps
  private int animationDelay = 83;
  private Random rand = 
                new Random(System.
                  currentTimeMillis());

Listing 4

The SpriteManager class is defined in this program.  As the name implies, an object of this class is used to manage the sprites involved in the animation process.  This class will be discussed in detail in a subsequent lesson.

Animation repetition rate

The variable named animationDelay is used to control the repetition rate of the animation process.

As in the movies, or on TV, animation is achieved by presenting a series of pictures on the screen.  Each picture represents a slightly different version of an object being animated.

(When I was a child, I used to create stick-man movies by drawing different versions of a stick-man doing acrobatics on the edges of the pages in a book.  By rapidly flipping through the pages with my thumb and forefinger, I could animate the stick-man and cause him to do his acrobatics.)

What is the required repetition rate?

The pictures need to be presented at a sufficiently fast rate to fool the brain and give the illusion of continuous motion.  On the other hand, presenting the pictures too rapidly simply wastes computer resources because the animation quality is not significantly improved.

Is twelve repetitions per second adequate?

The animationDelay variable in Listing 4 is initialized to a value of 83 milliseconds.  This is used by the program to insert 83 milliseconds between repetitions of the animated sprites.  This works out to approximately 12 repetitions per second.  Many authors agree that this rate is a good compromise between too slow and too fast.  However, only you can be the final judge of that.

Changing the repetition rate

To the extent that you computer can handle it, it isn't difficult to increase the repetition rate.  Decrease the initialization value for the animationDelay variable to increase the repetition rate, or increase the value to decrease the repetition rate.

Divide the animationDelay value into 1 to get the repetition rate.  Note, however, that if you make the animationDelay value too small, you computer won't be able to achieve the repetition rate specified by your new value for animationDelay.  In that case, the computer will simply be displaying new pictures as fast as it can create them.

Pseudo-random numbers

As we go through the program, you will see a number of instances where a random number is needed for some purpose.  The third reference variable in Listing 4 contains a reference to an object of the class Rand.  Here is part of what the Sun documentation has to say about the Rand class:

"An instance of this class is used to generate a stream of pseudo-random numbers. The class uses a 48-bit seed, ...

If two instances of Random are created with the same seed, and the same sequence of method calls is made for each, they will generate and return identical sequences of numbers."

The converse is also true

Although it isn't explicitly stated in the Sun documentation, the converse of the second paragraph above is also true.  In particular, if two instances of Random are created with different seeds, and the same sequence of method calls is made for each, they will generate and return different sequences of numbers.

In this program, I didn't want identical sequences of numbers.  Therefore, in the code shown in Listing 4, the Random object was constructed using the current time in milliseconds (relative to midnight on January 1, 1970) as the seed.  Using this approach, unless two Random objects are created within the same millisecond, they will produce different sequences of numbers.

In some cases, using time as a seed is inadequate.  Other instances of Random are created at other places in the program using seed values based on something other than time.

What can you do with a Random object?

Once you have an object of the Random class, a number of methods are available that allow you to extract random numbers from the object.

For example, the method named nextInt returns the next pseudo random, uniformly distributed int value from a random number generator's sequence.  This method will be used frequently, in conjunction with the modulus operator (%) to obtain random numbers that are uniformly distributed between the positive and negative values of a particular whole number (between -8 and +8, for example).

The main method

The code shown in Listing 5 is the main method for this application.  This code simply creates a new instance of the controlling class.
 

  public static void main(
                        String[] args){
    new Animate01();
  }//end main

Listing 5

This code, working in conjunction with the constructor and the run method of the animation thread starts the program running.

Summary

In this lesson, I have introduced you to several classes and concepts that you must understand in order to understand animation in Java.

I have introduced and discussed a number of classes used by the program.  Included were the following, which are particularly important to sprite animation:

  • Image
  • Toolkit
  • Graphics
  • MediaTracker
  • Random
I have 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

What's Next?

The next lesson in this series will pick up with a discussion of the constructor for the Animate01 class.

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 
                  offScreenGraphicsCtx;
  private Thread animationThread;
  private MediaTracker mediaTracker;
  private SpriteManager spriteManager;
  //Animation display rate, 12fps
  private int animationDelay = 83;
  private Random rand = 
                new Random(System.
                  currentTimeMillis());
  
  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 = 
        Toolkit.getDefaultToolkit().
          getImage("background02.gif");
    mediaTracker.addImage(
                   backGroundImage, 0);
    
    //Get and track 6 images to use 
    // for sprites
    gifImages[0] = 
           Toolkit.getDefaultToolkit().
               getImage("redball.gif");
    mediaTracker.addImage(
                      gifImages[0], 0);
    gifImages[1] = 
           Toolkit.getDefaultToolkit().
             getImage("greenball.gif");
    mediaTracker.addImage(
                      gifImages[1], 0);
    gifImages[2] = 
           Toolkit.getDefaultToolkit().
              getImage("blueball.gif");
    mediaTracker.addImage(
                      gifImages[2], 0);
    gifImages[3] = 
           Toolkit.getDefaultToolkit().
            getImage("yellowball.gif");
    mediaTracker.addImage(
                      gifImages[3], 0);
    gifImages[4] = 
           Toolkit.getDefaultToolkit().
            getImage("purpleball.gif");
    mediaTracker.addImage(
                      gifImages[4], 0);
    gifImages[5] = 
           Toolkit.getDefaultToolkit().
            getImage("orangeball.gif");
    mediaTracker.addImage(
                      gifImages[5], 0);
    
    //Block and wait for all images to 
    // be loaded
    try {
      mediaTracker.waitForID(0);
    }catch (InterruptedException e) {
      System.out.println(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 = 
        backGroundImage.getWidth(this);
    int height = 
       backGroundImage.getHeight(this);

    //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){
      System.out.println(
                  "Waiting for image");
      width = backGroundImage.
                        getWidth(this);
      height = backGroundImage.
                       getHeight(this);
    }//end while loop
    
    //Display the frame
    setSize(width,height);
    setVisible(true);
    setTitle(
        "Copyright 2001, R.G.Baldwin");

    //Create and start animation thread
    animationThread = new Thread(this);
    animationThread.start();
  
    //Anonymous inner class window 
    // listener to terminate the 
    // program.
    this.addWindowListener(
                   new WindowAdapter(){
      public void windowClosing(
                        WindowEvent e){
        System.exit(0);}});
    
  }//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(
           gifImages[0].getWidth(this),
           gifImages[0].
                     getHeight(this)));
      spriteManager.addSprite(
        makeSprite(position, cnt % 6));
    }//end for loop

    //Loop, sleep, and update sprite 
    // positions once each 83 
    // milliseconds
    long time = 
            System.currentTimeMillis();
    while (true) {//infinite loop
      spriteManager.update();
      repaint();
      try {
        time += animationDelay;
        Thread.sleep(Math.max(0,time - 
          System.currentTimeMillis()));
      }catch (InterruptedException e) {
        System.out.println(e);
      }//end catch
    }//end while loop
  }//end run method
  //---------------------------------//
  
  private Sprite makeSprite(
      Point position, int imageIndex) {
    return new Sprite(
          this, 
          gifImages[imageIndex], 
          position, 
          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 = 
         createImage(getSize().width, 
                     getSize().height);
      offScreenGraphicsCtx = 
          offScreenImage.getGraphics();
    }//end if
    
    // Draw the sprites offscreen
    spriteManager.drawScene(
                 offScreenGraphicsCtx);

    // Draw the scene onto the screen
    if(offScreenImage != null){
         g.drawImage(
           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) {
    g.drawImage(
               image, 0, 0, component);
  }//end drawBackgroundImage()
}//end class BackgroundImage
//===========================

class SpriteManager extends Vector {
  private BackgroundImage 
                       backgroundImage;

  public SpriteManager(
     BackgroundImage backgroundImage) {
    this.backgroundImage = 
                       backgroundImage;
  }//end constructor
  //---------------------------------//
  
  public Point getEmptyPosition(
                 Dimension spriteSize){
    Rectangle trialSpaceOccupied = 
      new Rectangle(0, 0, 
                    spriteSize.width, 
                    spriteSize.height);
    Random rand = 
         new Random(
           System.currentTimeMillis());
    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() %
                      backgroundImage.
                      getSize().width);
      trialSpaceOccupied.y = 
        Math.abs(rand.nextInt() %
                     backgroundImage.
                     getSize().height);

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

      //Test for collision. Positive 
      // result indicates a collision
      int hitIndex = 
              testForCollision(sprite);
      if (hitIndex >= 0){
        //a collision has occurred
        bounceOffSprite(cnt,hitIndex);
      }//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();
                                cnt++){
      sprite = (Sprite)elementAt(cnt);
      if (sprite == testSprite)
        //don't check self
        continue;
      //Invoke testCollision method 
      // of Sprite class to perform
      // the actual test.
      if (testSprite.testCollision(
                               sprite))
        //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)elementAt(oneHitIndex);
    Sprite otherSprite = 
      (Sprite)elementAt(otherHitIndex);
    Point swap = 
           oneSprite.getMotionVector();
    oneSprite.setMotionVector(
        otherSprite.getMotionVector());
    otherSprite.setMotionVector(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.
    backgroundImage.
                drawBackgroundImage(g);

    //Iterate through sprites, drawing
    // each sprite
    for (int cnt = 0;cnt < size();
                                 cnt++)
      ((Sprite)elementAt(cnt)).
                    drawSpriteImage(g);
  }//end drawScene()
  //---------------------------------//
  
  public void addSprite(Sprite sprite){
    add(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(
          position.x,
          position.y,
          image.getWidth(component),
          image.getHeight(component)));
    this.motionVector = motionVector;
    //Compute edges of usable graphics
    // area in the Frame.
    int topBanner = (
                 (Container)component).
                       getInsets().top;
    int bottomBorder = 
                ((Container)component).
                    getInsets().bottom;
    int leftBorder = (
                (Container)component).
                     getInsets().left;
    int rightBorder = (
                (Container)component).
                    getInsets().right;
    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;
  }//setSpaceOccupied()
  //---------------------------------//
  
  public void setSpaceOccupied(
                       Point position){
    spaceOccupied.setLocation(
               position.x, position.y);
  }//setSpaceOccupied()
  //---------------------------------//
  
  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
    position.translate(
       motionVector.x, motionVector.y);

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

Listing 6

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.

baldwin.richard@iname.com






Comment and Contribute

 


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

 

 


Enterprise Development Update

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

Sitemap | Contact Us

Rocket Fuel