November 21, 2014
Hot Topics:

Fun with Java: Frame Animation

  • November 15, 2001
  • By Richard G. Baldwin
  • Send Email »
  • More Articles »

Java Programming, Lecture Notes #1464


Preface

Having fun with 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 eighth in of a group of lessons that will teach you how to write animation programs in Java.

Animation programs

The first lesson in the group was entitled Fun with Java: Sprite Animation, Part 1.  That lesson, plus the six lessons following that one, provided an in-depth explanation of the use of Java for doing sprite animation.

The previous lesson, (and the last in the series on sprite animation), was entitled Fun with Java: Sprite Animation, Part 7.

Frame animation

In this lesson, I will move forward and teach you how to also do frame animation using Java.  In fact, I will combine sprite animation with frame animation in this lesson.

What is the difference?

What is the difference between sprite animation and frame animation?  This is how I view the difference between the two.

With sprite animation, an image moves around on the computer screen, but the look of that image doesn't change over time.

With frame animation, the look of an image can change over time, whether it moves or not.

Spherical sea creatures

In this program, I will combine the two such that spherical sea creatures will swim around in a fish tank.  Each creature will change the way it looks over time.  In particular, each creature will change its color as it swims.

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 in Java

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.

Spherical sea creatures

The first program, which was discussed in the previous seven lessons, showed 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 that program is shown in Figure 1.

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

Changing color

Many sea creatures have the ability to change their color in very impressive ways.  The second program, which I will cover in its entirety in this lesson, will simulate that process using a combination of sprite and frame animation.  (This will really be an upgrade to the previous program, and much of the code from the previous program will be used.)

Because a screen shot cannot represent changes over time, the screen shot shown in Figure 1 also represents the output from the program being discussed in this lesson.

Sea worms

The third program, to be discussed in the next 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.

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.

Getting the images

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.

Discussion and Sample Program

Very similar to the previous program

Although this program is quite long, most of the program is identical to the program named Animate01, which was discussed in the previous series of lessons.

Only discuss new material

In this lesson, I will discuss only those parts of the program that are new or different from the previous program.  However, I have provided a copy of the entire program in Listing 8 near the end of the lesson.  You can copy it into a source file on your local disk, compile it, run it, and see the results.

Because I will be discussing only that code that is new or different, it may be necessary for you to go back and study the previous program named Animate01 in order to understand this program.

The makeSprite method

This program differs from the previous program in only two areas:

  • The Sprite class from which new sprites are instantiated.
  • The makeSprite method of the controlling class, which is used to create new sprites.
Listing 1 shows an abbreviated version of the controlling class with all of the code other than the new method named makeSprite removed for brevity.
 
public class Animate02 extends Frame 
                   implements Runnable{
  //All the methods in this class are
  // identical to those in Animate01
  // except for the method named 
  // makeSprite.

  //Code deleted for brevity
  //---------------------------------//

  private Sprite makeSprite(
       Point position, int imageIndex){
    return new Sprite(
        this, 
        gifImages,
        imageIndex,
        1,
        rand.nextInt() % 20,
        position, 
        new Point(rand.nextInt() % 5,
                  rand.nextInt() % 5));
  }//end makeSprite()
  //---------------------------------//

  //Code deleted for brevity

}//end class Animate02

Listing 1

The primary differences

The primary difference between this program and the previous program named Animate01 can be summarized as follows:

Each Sprite had only one image

Each Sprite object in the previous program had an instance variable of type Image.  The image whose reference was stored in the instance variable was used to provide the visual manifestation of the sprite on the screen.  Because a given sprite owned a reference to only a single image, that sprite always looked the same on the screen.

Each Sprite has an array of images

In this program, each Sprite object has an instance variable, which is a reference to an array of references to Image objects.

The images stored in the Image objects are used to provide the visual manifestation of the sprites when they appear on the screen.  However, because each sprite now owns references to more than one image, the sprite is able to change how it looks over time by using different images as its visual manifestation.

What is an animation frame?

Each of the images in the array is typically referred to as a frame.  The process of causing a sprite to cycle through the frames, changing the way that it looks in the process, is often referred to as frame animation.

(Note that the use of the word frame in this context has nothing to do with the Java class named Frame.)

The substantive changes

All of the substantive changes in this program were made in the class named Sprite.  However, because the argument list for the constructor of the Sprite class changed (to accommodate more incoming information), it was also necessary to modify the method named makeSprite shown in Listing 1.

The changes that were made had to do with changes to the parameter list for the Sprite constructor.  The new parameter list includes a reference to an array of references to Image objects (gifImages) along with some other new parameters that deal with the manner in which the sprite cycles through the frames.

The Sprite class

As before, the Sprite class is the workhorse of this animation program.

Each of the sprites swimming around in the fish tank is an object of the class named Sprite.  As is the typical objective in object-oriented programming, a sprite knows how to take care of itself.

Where are you?

For example, an object of the Sprite class knows how to tell other objects about the space that it occupies in the fish tank.

What is your speed and direction?

It knows how to tell other objects about its motion vector, which determines the speed and direction of its motion.

It knows how to use its motion vector in conjunction with a random number generator to incrementally advance its position to the next location in its movement through the water.  In so doing, it knows how to protect itself from excessive speed.

Oops, just hit a wall!

It knows what to do if it runs into one of the walls of the fish tank.  Basically, it bounces off the wall much like a pool ball bounces off the cushions on a pool table.  When this happens, it modifies its motion vector accordingly.

Please draw a picture of yourself

When requested to do so, it knows how to draw itself onto a graphics context that it receives along with the request.

Did we just collide?

When requested to do so, it can determine if it has collided with another sprite whose reference it receives along with the request.

Change your appearance

Finally, this new and improved sprite knows how to use an array of images to change how it looks over time.

This is a very key class insofar as this program is concerned.  The behavior of the methods in this class determines the overall behavior of the animation process.

Discuss in fragments

As usual, I will discuss the program in fragments.

Most of the code in this revised Sprite class is identical to the code in the Sprite class used in the previous program named Animate01.  In keeping with the spirit of this lesson, I will not discuss the code that I discussed in the previous lessons.  Rather, for the most part, I will discuss only that code that is new or different.  When you see //... in the code fragments, that means that code was omitted for brevity.

The beginning of the Sprite class

Listing 2 shows the beginning of the Sprite class along with some new or different declarations for instance variables.
 

class Sprite {
  private Image[] image;
  private int frame;
  private int frameDisplayIncrement;
  private int frameDisplayDuration;
  private int frameDurationCounter;
  //...

Listing 2

An array of Image references

In the earlier version, the reference variable named image was of type Image.  However, in this version, it is of type Image[].  In other words, it previously referred to a single object of type Image.  Now it refers to an array of references to Image objects.

The constructor

Listing 3 shows the signature for the constructor.  The significant thing about Listing 3 is the makeup of the formal argument list, which I will discuss below.
 

  public Sprite(//constructor
             Component component, 
             Image[] image,
             int startingFrame, 
             int frameDisplayIncrement,
             int frameDisplayDuration, 
             Point position, 
             Point motionVector){

Listing 3

More parameters required

The constructor for the new Sprite class takes seven parameters, as opposed to only four parameters for the previous version.

As before, the first parameter is a reference to a Component object.  In fact, this parameter is assumed to be a reference to the Frame object in which this animation is run.

This parameter is used to determine the size of the Frame.  It is also used as a required ImageObserver in some of the methods in the class. (I discussed image observers in an earlier lesson in this series.)

An array of Image references

Whereas previously, the second parameter was a reference to an object of type Image, the second parameter in the new version is a reference to an array containing references of objects of type Image.

The images in the array are used to provide a visual manifestation for the sprite, and that visual manifestation changes over time.

Cycling through the animation frames

The third, fourth, and fifth parameters are new to this version of the Sprite class.  These parameters are used to determine how the sprite cycles through the set of images in the array.

The startingFrame parameter

The parameter named startingFrame specifies the first frame to use at the beginning of the frame animation process.  In effect, this determines the color of a sprite when it first appears on the screen.  If you examine the code in Listing 8 near the end of this lesson, you will see that different values are used for the fifteen sprites that populate the fish tank.

The frameDisplayIncrement parameter

The parameter named frameDisplayIncrement allows for skipping some of the images in the array of images.

In this program, this value was set to 1 for all sprites.  Thus, when cycling through the images, each sprite uses every image as its visual manifestation.

The frameDisplayDuration parameter

The parameter named frameDisplayDuration is used to determine how long a given image will be used as the visual manifestation for a sprite.

In this program, this value was specified as a random number with a maximum value of 20 for each new sprite.  Thus, each sprite will cycle through the six images in the array at a different rate.

Same as previous parameters

The sixth and seventh parameters for the new constructor are the same as the third and fourth parameters of the constructor in the previous program.  The sixth parameter named position specifies the initial position of the sprite.

The seventh parameter named motionVector is a motion vector, which determines the initial speed and direction of motion for a new Sprite, object.

Some new code

Listing 4 shows some of the code in the constructor that is new to this version of the program.
 

    //...
    frame = startingFrame;
    this.frameDisplayIncrement = 
                 frameDisplayIncrement;

    frameDisplayDuration = 
        Math.abs(frameDisplayDuration);

    if(frameDisplayDuration < 5) 
      frameDisplayDuration = 5;
    this.frameDisplayDuration = 
                  frameDisplayDuration;

    frameDurationCounter = 
             this.frameDisplayDuration;
    //...

Listing 4

The first two statements in Listing 4 simply save the incoming values for startingFrame and frameDisplayIncrement in the appropriate instance variables.

Duration must be positive

The code having to do with the frameDisplayDuration does two things.  First it uses the abs method of the Math class to guarantee that the duration is a positive value.

Minimum duration is five animation cycles

Second, it guarantees that the value is at least 5.  This means that each sprite object will use an Image object as its visual manifestation for at least five animation cycles (or about one-half second at twelve frames per second).

In this program, this means that each sprite will remain the same color for at least five animation cycles.

The frameDurationCounter

As you may have surmised earlier, the instance variable named frameDurationCounter is used to determine when a sprite chooses a new Image object as its visual manifestation (when it changes color in this program). The value of the frameDurationCounter is initialized in the code of Listing 4.

All of the remain code in the constructor is essentially the same as in the previous version of the program, so I won't discuss it here.

The incFrame method

That brings us to a method named incFrame, which is completely new to this version of the program.  The entire method is shown in Listing 5.
 

  private void incFrame() {
    if ((frameDisplayDuration > 0) && 
        (--frameDurationCounter <= 0)){
      // Reset the frame trigger
      frameDurationCounter = 
                  frameDisplayDuration;

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

Listing 5

Later we will see that this method is invoked each time the sprite is requested to update its location on the screen (once during each animation cycle).

Decrement the frameDurationCounter

The code in this method is fairly ugly, particularly in this narrow publication format.

When you wade through it, you discover that it decrements the frameDurationCounter, (which was initialized to the value of the frameDisplayDuration) each time the incFrame method is invoked.

Reset the counter and increase the frame variable

When the counter hits zero,

  • The counter is reset back to the value of the frameDisplayDuration
  • The value of the variable named frame is increased by the value of the frameDisplayIncrement
Choose an image

Later we will see that the value of frame is used to choose an image from the array of images as the visual manifestation of the sprite.

Bracket the value of the frame variable

Some testing is done to ensure that the value for frame is not greater than the number of images in the array of images and is not less than zero.  If so, the value of frame is corrected appropriately.

Frame control code

This is essentially the control code that determines how the sprite cycles through the array of images, selecting an image for its visual manifestation during each animation cycle.  There are only about two more statements in the entire Sprite class that have anything to do with frame animation.  The first of those appears in Listing 6.

Revised updatePosition method

The code in Listing 6 shows the beginning of the revised updatePosition method of the Sprite class.

You may recall from the previous lessons that this is the method that the SpriteManager invokes, once during each animation cycle, to cause each individual sprite to draw itself on the screen in its new position.
 

  public void updatePosition() {
    incFrame();//increment the frame
    //...

Listing 6

Incrementing the frame variable

As you can see, the first statement in this new version of the method invokes the incFrame method discussed above to cause the value of the variable named frame to change when appropriate.

As you can also see, a great deal of code that is essentially the same as in the previous program was deleted for brevity, taking us all the way down to the revised method named drawSpriteImage shown in Listing 7.

The revised drawSpriteImage method

As you may recall from the previous lessons, the drawSpriteImage method is the method that the SpriteManager invokes on each Sprite object to cause the sprite to draw itself onto the offscreen graphics context once during each animation cycle
 

  public void drawSpriteImage(
                           Graphics g){
    g.drawImage(image[frame], 
                spaceOccupied.x, 
                spaceOccupied.y, 
                component);
  }//end drawSpriteImage()

Listing 7

What is the difference?

The difference between the new version of the drawSpriteImage method and the previous version is so important that I have provided the previous version in a different color in Listing 8 below for comparison.
 

  public void drawSpriteImage(
                           Graphics g){
    g.drawImage(image,
                spaceOccupied.x,
                spaceOccupied.y,
                component);
  }//end drawSpriteImage()

//--NOTE: THIS IS CODE FROM AN EARLIER
PROGRAM--

Listing 8

A reference to an Image object

The thing to note in these two listings is the invocation of the drawImage method.  In both versions, the first parameter to the drawImage method is a reference to the Image object that will be rendered onto the screen as the visual manifestation of the sprite.

One Image object available

However, in the previous version of the program, this is a reference to a single Image object passed into the Sprite constructor when the Sprite object was instantiated.

A choice of Image objects

In the new version, this is a reference extracted from an array of references using the frame variable as an index into the array.

The reference to the array of Image objects was received as an incoming parameter to the constructor when the Sprite object was instantiated.  In the new version, the incFrame method discussed earlier is used to establish the value stored in frame, which identifies the Image object that is used to provide the visual manifestation for the sprite during each animation cycle.

Six colored spheres

In this program, the array contains references to images of six small spheres of different colors.  As the animation progresses and the incFrame method controls the value of the variable named frame, different colored spheres are selected as the visual manifestation of a sprite, and the sprites appear to change color over time.

The remaining code in the Sprite class was discussed in earlier lessons and therefore won't be discussed further here.

Summary

In this lesson, I have taught you how to achieve frame animation in Java.  I accomplished this by showing you how to upgrade the sprite animation program from the earlier lessons into a new program that provides both sprite animation and frame animation.

What's Next?

In the next lesson in this series, I will provide an additional upgrade to convert the spherical sea creatures into sea worms that have the ability to slither around in the fish tank and to change the colors of different parts of their bodies in a random fashion as they slither.

Complete Program Listing

A complete listing of the program is provided in Listing 8.
 
/*File Animate02.java
Copyright 2001, R.G.Baldwin
This program displays several animated
colored spherical sea creatures 
swimming around in an aquarium. Each 
creature maintains generally the same 
course until it collides with another 
creature or with a wall.  However, 
the creatures have the ability to 
change course based on the addition or 
subtraction of random values from the 
components of their motion vector
about once in every ten updates.

In addition, each creature uses frame 
animation to change colors in the order
red, green blue, yellow, purple, 
orange.  Each creature switches among 
the colors on a periodic rate, but the 
period may be different for each 
creature.  Thus, each creature cycles 
through the same colors, but remains a
particular color for a different 
amount of time than the other 
creatures.  The amount of time that a 
creature remains a particular color is 
based on a random number between 5 
and 20 updates.

The primary changes in this program
relative to the program named Animate01
were made in the Sprite class, and in 
calls to the constructor for the Sprite
class in the makeSprite() method.

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

public class Animate02 extends Frame 
                   implements Runnable{
  //All the methods in this class are
  // identical to those in Animate01
  // except for the methodnamed 
  // makeSprite.
  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 Animate02();
  }//end main
  //---------------------------------//

  Animate02() {//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,
        1,
        rand.nextInt() % 20,
        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 Animate02
//===================================//

class BackgroundImage{
  //This class is identical to that
  // used in Animate01
  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 {
  //This class is identical to that
  // used in Animate01
  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; 
  
  private int frame;
  private int frameDisplayIncrement;
  private int frameDisplayDuration;
  private int frameDurationCounter;

  public Sprite(//constructor
             Component component, 
             Image[] image,
             int startingFrame, 
             int frameDisplayIncrement,
             int frameDisplayDuration, 
             Point position, 
             Point motionVector){
    //Seed a random number generator 
    // for this sprite with the sprite 
    // position.
    rand = new Random(position.x);
    
    frame = startingFrame;
    this.frameDisplayIncrement = 
                 frameDisplayIncrement;
    frameDisplayDuration = 
        Math.abs(frameDisplayDuration);
    if(frameDisplayDuration < 5) 
      frameDisplayDuration = 5;
    this.frameDisplayDuration = 
                  frameDisplayDuration;
    frameDurationCounter = 
             this.frameDisplayDuration;
    this.component = component;
    this.image = image;
    setSpaceOccupied(new Rectangle(
       position.x,
       position.y,
       image[0].getWidth(component),
       image[0].getHeight(component)));
    this.motionVector = motionVector;
    //Compute edges of usable graphics 
    // area
    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()
  //---------------------------------//
  
  private void incFrame() {
    if ((frameDisplayDuration > 0) && 
        (--frameDurationCounter <= 0)){
      // Reset the frame trigger
      frameDurationCounter = 
                  frameDisplayDuration;

      // Increment the frame
      frame += frameDisplayIncrement;
      if (frame >= image.length)
        frame = 0;
      else if (frame < 0)
        frame = image.length - 1;
    }//end if
  }//end incFrame
  //---------------------------------//
  
  public void updatePosition() {
    incFrame();//increment the frame
    
    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 
    // small random change to its 
    // motionVector.  When a change 
    // occurs, the motionVector
    // coordinate values are forced to 
    // fall between -7 and 7.
    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

    //Make the move
    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
      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
    
    //save new motionVector
    if (bounceRequired) 

     setMotionVector(tempMotionVector);

    //update spaceOccupied
    setSpaceOccupied(position);
  }//end updatePosition()
  //---------------------------------//
  
  public void drawSpriteImage(
                           Graphics g){
    g.drawImage(image[frame], 
                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 8

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.

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