Java Programming   Java Programming
 |Sofia Home | Content Gallery |
width="10" Home
width="10" Syllabus
width="10" Schedule
width="10" Lessons
width="10" Assignments
width="10" Discussion
width="10" Resources

Lesson 11.4 Animation

Threads & Animation

Animation is really simple in Java. The simplest variety involves repeated calls to repaint(), each time drawing your image at a different location. In this lesson you'll learn to do simple animation, moving images around the screen. Along the way you'll be introduced to Java's Thread class, and you'll revisit the Component update() method.

Let's look at an example. By tradition, this is known as the "Bouncing Ball" program. Since bouncing balls are, well, boring, I attempted to liven things up by bouncing a wiener-dog image around the screen. Unfortunately, I couldn't find any good wiener-dog images, and the one I drew was unrecognizable.

I had to settle for a flying tiger, and hope that Bill Watterson never sees this page. [As a fallback, I have a small gif of myself at a Christmas party, wearing a bear hat. The wiener-dogs are better.]

FlyingTiger1

We'll start the FlyingTiger1 applet by copying Image1.java from the last section. Here are the steps you need to follow:

1. Attributes

2. The init() Method

3. Changing paint()

 
Attributes
Start by adding the following fields alongside the Image field, img:
  • A pair of int variables you'll use to keep track of your image on the screen. Call them x and y.
  • A pair of int variables that represent the amount to move x and y for each frame of the animation. Call these dx and dy, and initialize them to 5.
  • A pair of int variables to represent the height and width of the image. Call these imh and imw. Initialize both to -1.
Back to Top
The init() Method
Inside your init() method, load your image as normal using getImage(). Pick a nice small image that will look good bouncing around the screen. Then, add the following lines to init():
  • Create and use a MediaTracker object to wait for your image to completely load before leaving init().
  • Set the background color of your applet to black.
  • Initialize x and y to the center of the screen, using getSize().height / 2 and getSize().width / 2. That doesn't quite place the image right in the middle of the screen, [it "hangs" down and to the right], but that's fine.
Changing paint()
Make the following changes to the paint() method:
  • Create two int variables w and h. Initialize them to the size of the applet, using getSize().width and getSize().height. [This was not done during init() so that resizing of the applet would be possible in appletviewer]
  • If imw is less than zero [you initialized it to -1, remember], then initialize both imw and imh using the Image getWidth(this) and getHeight(this) methods.
  • Add dx and dy to x and y. [For instance, x = x + dx;]
  • Check to see if x is less than zero--thus disappearing off the left of the applet--or if x + imw is greater than w--thus disappearing off the right side of the applet. If either of these are true, then change dx so that it has the value -dx.
  • Do the same thing with y. Check if it is less than zero [off the top] or if y + imh is greater than h [off the bottom]. Reverse the sign of dy if so.
  • Draw your image using drawImage() at x,y.
  • Send a repaint(50) message.

The Results

Here's the finished code with the changes marked in red. The FlyingTiger1 applet is running at the bottom:

FlyingTiger1.java
import java.awt.*;
import java.applet.*;

public class FlyingTiger1 extends Applet
{
  Image img = null;
  int x, y, dx = 5, dy = 5;
  int imw = -1, imh = -1;

  public void init()
  {
    img = getImage(getCodeBase(), "tiger.gif");
    MediaTracker mt = new MediaTracker(this);
    mt.addImage(img, 0);
    try
    {
      mt.waitForID(0);
    }
    catch (InterruptedException ie) { }

    setBackground(Color.black);

    x = getSize().width / 2;
    y = getSize().height / 2;
  }

  public void paint(Graphics g)
  {
    int w = getSize().width;
    int h = getSize().height;

    if (imw < 0)
    {
      imw = img.getWidth(this);
      imh = img.getHeight(this);
    }
    // Animate the image
    x += dx;
    y += dy;

    if (x + imw > w || x < 0 ) dx = - dx;
    if (y + imh > h || y < 0 ) dy = - dy;

    g.drawImage(img, x, y, this);

    repaint(50);
  }
}

A lurking Hobbes.
Back to Top

A Few Modifications

You can change the amount of "timeout" supplied to the repaint() method to speed up or slow down the animation. Unfortunately, this is very unreliable. Writing:

repaint(50);

means "schedule a paint() sometime within the next 50 milliseconds", instead of "wait 50 milliseconds and then paint()". The current browsers all seem extremely uneven in their support of this version of repaint().

Speed and Trajectory

Just by using the basic program structure, however, we can easily change the speed, trajectory or both by changing the values of dx, dy. For instance, initializing dx and dy like this:

dx = (int) (Math.random() * 10) + 5;
dy = (int) (Math.random() *  5) + 5;

gives you the applet, FlyingTiger2.java, shown here:

Refresh the frame to see the applet bouncing at different speeds or trajectories.

Back to update()

Depending upon your browser and OS, you may see significant flicker when running FlyingTiger1 and 2. That's because each call to repaint() is also calling the update() method, and the update() method is erasing the entire applet each time you draw your new image.

Most solutions to this problem involve overriding update() and either handling the erasing yourself, or skiping erasing altogether. Let's see a few of these solutions.

Solution 1: No Erasing
By adding the following method to FlyingTiger2, you can stop the applet from erasing altogether, and eliminate the flicker. 

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

Of course there is a significant side effect, as you can see by looking at the applet running here. Now, instead of flicker, you have tiger trails, as you can see in this applet, FlyingTiger3.java. Except for cases when you want this effect, eliminating erasing is not a solution for animation.

Solution 2: Selective Erasing
Instead of eliminating erasing altogether, you can erase just your image, rather than the entire screen. Doing this involves using fillRect() to erase just the portion of the screen you currently occupy. The problem with this method is that painting is not the highest priority in a Java program, so your applet skips a few steps from time to time, leaving pieces of tiger scattered across the landscape.

The new update() method, implemented in FlyingTiger4.java looks like this: 

public void update(Graphics g)
{
  g.setColor(getBackground());
  g.fillRect(x, y, imw, imh);

  paint(g);
}

Solution 3: Double Buffering
Since the two previous solutions had significant drawbacks, most Java programs that use animation either 1) live with the flicker, or 2) employ a method called double-buffering.

With double-buffering, you do all of your drawing on an "off-screen" Image object that represents your entire applet display. Then, when your offscreen drawing is entirely finished, you paint the entire applet onto the real display, all at once, without erasing the background at all. Here are the steps you need to implement double-buffering in your program:

  • Create an Image field [in the example I've called it oimg ] and set it to null. In addition, create a Graphics field [I've called it og] and set it to null.
  • Image    oimg = null;
    Graphics og   = null;

  • Inside your paint() method, check to see if og is null. If it is, then initialize oimg by using the createImage() method, as shown below, and initialize og by sending oimg the getGraphics() message.
  • int w = getSize().width;
    int h = getSize().height;
    if (og == null)
    {
       oimg = createImage(w, h);
       og = oimg.getGraphics();
    }
  • Now, do all of your drawing on the offscreen Graphics og. [In the example shown below, I've drawn a checkerboard background so you can see how effective the double-buffering technique is.} At the end of the paint() method, use drawImage() to paint the image oimg onto the screen
Here is a link to the applet, FlyingTiger5.java, that implements the double-buffering you see here.

Back to Top

Introducing Threads

Let's look at one more way to control the speed of your applet, using < size=>Thread.sleep() method. If you add a Scrollbar [< size=>sb] to your applet, and then add the following line before < size=>repaint()

< size=>try
{
   Thread.sleep(1000/sg.getValue());
}
catch (InterruptedException ie) { }

< size=>you can make your animation interactive. Here's an applet, FlyingTiger6.java, that implements this strategy, along with the double-buffering you've seen earlier:

What is a Thread?

Threads are like separate programs started and [sometimes] controlled by your main program, but which operate autonomously, and at the same time. Starting a thread is different than launching a completely separate program, because a thread has access to the fields inside your program.

For animation, rather than painting inside the paint() method, which is controlled by your browser's update thread, it make sense to create your own threads that you can control.

Creating Your Own Threads

Here are the five easy steps you follow to create your own threads to your applets:
  • Add implements Runnable to your class header. Create a Thread object as a field, and set its value to null. Pass this to the Thread constructor, usually in your applet's start() method. Start your thread running its start() method. [You'll often set your Thread field back to null in your applet's stop() method as well.]
  • When your thread starts running, it looks for a method named run(). Write a run() method to carry out the actions you want the thread to perform.

Let's create a program which animates a butterfly instead of the tiger, controlling the animation inside a thread. Here are the steps to follow.

Back to Top

The Thread Framework

Start by taking the program FlyingTiger1.java and copying it to Butterfly.java. Then, inside your program, begin by changing the name of the class and adding implements Runnable to the class header.

Your code should look like this:

public class Butterfly extends Applet
                implements Runnable
{
}

Next, create a Thread field inside your class, setting its value to null as shown here:

Thread theThread = null;

  Now, add a start() method to your applet. Inside the applet's < size=>start() method:
  1. Check to see if theThread is null.
  2. If theThread is not null, then use the Thread constructor to create a new Thread object, passing this as the argument. Assign the new Thread to theThread, and call theThread's start() method.
Your applet's start() method should look like this:

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

While you're at it [although this is not strictly required to use a Thread, it is still good programming practice] you should write an applet stop() method. Inside your stop() method, simply set theThread to null. Here's what you should have:

public void stop()
{
  theThread = null;
}
Back to Top

The run() Method

That's it for the general framework, all that's left to do is to write your run() method, which will automatically be called when you send the start() message to your Thread.

The run() method typically contains an "endless" loop. Since we'll be painting inside our run() method, and since we want to use the double-buffering technique which worked so effectivly before, we'll need to add a getGraphics() at the top of the method, and a dispose() at the bottom. The method should look like this:

public void run()
{
  int w = getSize().width;
  int h = getSize().height;

  Image oimg = createImage(w, h);
  Graphics og = oimg.getGraphics();
  Graphics g = getGraphics();   // Endless loop goes here

  g.dispose();
  og.dispose();
}

To control the endless loop inside your run() method, create a local Thread object [call it thisThread], and initialize it by calling the static Thread method currentThread(). In your loop condition, test if thisThread is equal to theThread. As long as they are, your applet's stop() method has not been called and the thread should continue running. Here's what the code should look like:

public void run()
{
  // Initialize

  Thread thisThread = Thread.currentThread(); <>
  while (theThread == thisThread)
  {
    // Painting code here
  }

  // Cleanup
}

The rest of the code inside run() comes mostly from the paint() method, which is eliminated in Butterfly.java. Some of the code, the initialization part, should go before the endles loop. Stick it up by getGraphics(). That part should look like this:

public void run()
{
  // Initialization <>

  og.setColor(getBackground());
  if (imw < 0)
  {
    imw = img.getWidth(this);
    imh = img.getHeight(this);
  }

  // Painting loop

  // Cleanup
}

The final part of the run() method is the code that paints the butterfly inside the loop. Because we want the trajectory for the butterfly to be different than the flying tiger, let's use Math.random(), along with dx and dy to determine how far to move x and y each time. You could do that like this:

x += (int)(Math.random() * dx);
y += (int)(Math.random() * dy);

You still check for an out-of-bounds image in the same way, and the drawing code looks pretty much the same. For, we've drawn a second image on the background, called bg, before drawing the butterfly. After that, the offscreen image is painted on the real screen using drawImage(). After the call to drawImage() though, you'll need to put in a Thread.sleep() for about 1/30 second.

public void run()
{
  // Initialization

  while (theThread == thisThread)
  {
    x += (int)(Math.random() * dx);
    y += (int)(Math.random() * dy); <>

    if (x+imw > w || x <0 ) { dx = -dx; x += dx; }
    if (y+imh > h || y <0 ) { dy = -dy; y += dy; } <>

    og.drawImage(bg, 0, 0, w, h, null);
    og.drawImage(img, x, y, null);
    g.drawImage(oimg, 0, 0, null); <>

    try
    {
      Thread.sleep(1000/30);
    }
    catch (InterruptedException ie) { }
  }
  // Cleanup
}>

You can look at the source code for Butterfly.java to see the details. Here's what the finished applet looks like when it runs:

Back to Top

Something to Talk About

Can you modify FlyingTiger1.java to use two images? (Hint: you'll need two sets of x,y variables, and you'll need to call drawImage() twice in the paint() method.)

Please continue to the next section of this lesson.

 

Back to Top

 

Content Developed by Stephen Gilbert, Licensed under a Creative Commons License
Published by the Sofia Open Content Initiative
© 2004 Foothill-De Anza Community College District &The William and Flora Hewlett Foundation