Images

Drawing text on the screen is fun and all, but you probably want to have some images in your game, right? Right. Before you draw images on the screen, let's learn about some image fundamentals: transparency types and file formats.

Transparency

Imagine you have a simple image you want to display, as shown in Screenshot.

Screenshot The image of the hero shows that he's really just a big sphere.

Java graphics 02fig04.gif


The image in Screenshot is a character on a white background, but is the background part of the image, and does it get drawn, too? That depends on the image's transparency. You can use three types of image transparency: opaque, transparent, and translucent.

  • Opaque. Every pixel in the image is visible.
  • Transparent. Every pixel in the image is either completely visible or completely see-through. In the image, the white background could be transparent so that what's "underneath" the image shows through when it is drawn.
  • Translucent. Pixels can be partially transparent, to create a ghostlike, partially see-through effect. Also, translucency can be used just on the edges of an image, to create an anti-aliased image.

In the character image, you probably want to make the white background transparent so that when the image is drawn, what's behind the character is shown rather than having the character surrounded by a white box. Optionally, you can make the edges translucent so the image would be anti-aliased.

File Formats

Two basic types of image formats exist: raster and vector. A raster image format describes images just like your display does, in terms of pixels with a specified bit depth. A vector image format describes an image geometrically and can be resized without degrading the quality. The Java API doesn't have any vector formats built in, so we focus on raster images. If you're interested in vector images, check out Apache's Scalable Vector Graphics (SVG) implementation, called Batik, at http://xml.apache.org/batik/. The Java runtime has three different raster formats built in, and you can read them with hardly any effort. These three formats are GIF, PNG, and JPEG:

  • GIF. GIF images can be either opaque or transparent, and can have 8-bit color or less. Although GIF has high compression for graphics images without a lot of color variation, PNG supercedes GIF's functionality, so there's no reason to use GIF anymore.
  • PNG. PNG images can have any type of transparency: opaque, transparent, or translucent. Also, PNG images can have any bit depth, all the way up to 24-bit color. The compression ratio for 8-bit PNG images is about the same as for GIF images.
  • JPEG. JPEG images can be opaque, 24-bit images only. JPEG has high compression for photographic images, but it is a lossy compression, so the image isn't an exact replica of its source.

These image file formats can be created in common paint programs such as Adobe Photoshop (www.adobe.com), Jasc Paint Shop Pro (www.jasc.com), and the GIMP (www.gimp.org).

Reading Images

So how do you translate a GIF, PNG, or JPEG file into something you can display? This is done via Toolkit's getImage() method: It parses the image file and returns an Image object. Here's an example:

Toolkit toolkit = Toolkit.getDefaultToolkit();
Image image = toolkit.getImage(fileName);


This code looks innocent enough, but it doesn't actually load the image. The image begins loading in another thread. If you display the image before it is finished loading, only part (or none) of the image actually gets displayed. You can use a MediaTracker object to watch the image and wait for it to finish loading, but there is an easier solution. The ImageIcon class loads an image using MediaTracker for you. The ImageIcon class in the javax.swing package loads an image using the Toolkit and waits for it to finish loading before it returns. For example:

ImageIcon icon = new ImageIcon(fileName);
Image image = icon.getImage();


Okay, now that you can load images, you can try it. The ImageTest class in Listing 2.3 works similarly to the FullScreenTest class, also using SimpleScreenManager. ImageTest draws one JPEG background image and four PNG foreground images, waits 10 seconds, and then exits. The background is a JPEG file because it is photographic quality, so the JPEG format compresses it better than a PNG would. The PNG images it displays are opaque, transparent, and translucent. One translucent image is entirely translucent, while the other is translucent only around the edges, to make the image anti-aliased. See Screenshot for a screen capture of ImageTest.

Screenshot The ImageTest program shows the different types of transparency.

Java graphics 02fig05.gif


Again, if you'd like to run ImageTest at a different display mode, specify the mode at the command line, just like in FullScreenTest.

Listing 2.3 ImageTest.java
import java.awt.*;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
public class ImageTest extends JFrame {
 public static void main(String[] args) {
 DisplayMode displayMode;
 if (args.length == 3) {
 displayMode = new DisplayMode(
 Integer.parseInt(args[0]),
 Integer.parseInt(args[1]),
 Integer.parseInt(args[2]),
 DisplayMode.REFRESH_RATE_UNKNOWN);
 }
 else {
 displayMode = new DisplayMode(800, 600, 16,
 DisplayMode.REFRESH_RATE_UNKNOWN);
 }
 ImageTest test = new ImageTest();
 test.run(displayMode);
 }
 private static final int FONT_SIZE = 24;
 private static final long DEMO_TIME = 10000;
 private SimpleScreenManager screen;
 private Image bgImage;
 private Image opaqueImage;
 private Image transparentImage;
 private Image translucentImage;
 private Image antiAliasedImage;
 private boolean imagesLoaded;
 public void run(DisplayMode displayMode) {
 setBackground(Color.blue);
 setForeground(Color.white);
 setFont(new Font("Dialog", Font.PLAIN, FONT_SIZE));
 imagesLoaded = false;
 screen = new SimpleScreenManager();
 try {
 screen.setFullScreen(displayMode, this);
 loadImages();
 try {
 Thread.sleep(DEMO_TIME);
 }
 catch (InterruptedException ex) { }
 }
 finally {
 screen.restoreScreen();
 }
 }
 public void loadImages() {
 bgImage = loadImage("images/background.jpg");
 opaqueImage = loadImage("images/opaque.png");
 transparentImage = loadImage("images/transparent.png");
 translucentImage = loadImage("images/translucent.png");
 antiAliasedImage = loadImage("images/antialiased.png");
 imagesLoaded = true;
 // signal to AWT to repaint this window
 repaint();
 }
 private Image loadImage(String fileName) {
 return new ImageIcon(fileName).getImage();
 }
 public void paint(Graphics g) {
 // set text anti-aliasing
 if (g instanceof Graphics2D) {
 Graphics2D g2 = (Graphics2D)g;
 g2.setRenderingHint(
 RenderingHints.KEY_TEXT_ANTIALIASING,
 RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
 }
 // draw images
 if (imagesLoaded) {
 g.drawImage(bgImage, 0, 0, null);
 drawImage(g, opaqueImage, 0, 0, "Opaque");
 drawImage(g, transparentImage, 320, 0, "Transparent");
 drawImage(g, translucentImage, 0, 300, "Translucent");
 drawImage(g, antiAliasedImage, 320, 300,
 "Translucent (Anti-Aliased)");
 }
 else {
 g.drawString("Loading Images...", 5, FONT_SIZE);
 }
 }
 public void drawImage(Graphics g, Image image, int x, int y,
 String caption)
 {
 g.drawImage(image, x, y, null);
 g.drawString(caption, x + 5, y + FONT_SIZE +
 image.getHeight(null));
 }
}


Before the images are loaded, the ImageTest class shows a "Loading images" message. After the images are loaded, repaint() is called to signal the AWT to repaint the screen. At that point, the background and four PNGs are drawn.

Hardware-Accelerated Images

Hardware-accelerated images are images that are stored in video memory rather than system memory. Images that are hardware-accelerated can be copied to the screen faster than images that aren't. Java tries to hardware-accelerate any image you load by using Toolkit's getImage() method. Because Java makes it automatic, you usually don't have to take any extra effort to make an image hardware-accelerated. However, a few issues will keep your images from being accelerated:

  • If you constantly change the contents of the image (for example, drawing graphics primitives onto the image), the image won't be accelerated.
  • As of Java SDK 1.4.1, translucent images aren't accelerated. Only opaque and transparent images are accelerated. Because of this, translucent images are used sparingly in this tutorial.
  • Not every system has accelerated image capability.

If you want to force an image to be hardware-accelerated on systems that support it, you can create a VolatileImage. VolatileImages are images that are stored in video memory. VolatileImages are created using Component's createVolatileImage(int w, int h) method or GraphicsConfiguration's createCompatibleVolatileImage(int w, int h) method. Unfortunately, VolatileImages can be only opaque. VolatileImages can lose their contents at any time, hence the word "volatile." For example, if a screen saver starts up or the display mode changes, the VolatileImage might be wiped out of video memory, forcing you to redraw whatever is supposed to be there. You can check whether a VolatileImage has lost its contents by using the validate() and contentsLost() methods. The validate() method makes sure the image is compatible with the current display mode, and the contentsLost() method returns information about whether the contents of the image have been lost since the last validate() call. Here's an example of how to check and restore a VolatileImage:

// create the image VolatileImage image = createVolatileImage(w, h);
...
// draw the image do {
 int valid = image.validate(getGraphicsConfiguration());
 if (valid == VolatileImage.IMAGE_INCOMPATIBLE) {
 // image isn't compatible with this display; re-create it
 image = createVolatileImage(w, h);
 }
 else if (valid == VolatileImage.IMAGE_RESTORED) {
 // restore the image
 Graphics2D g = image.createGraphics();
 myDrawMethod(g);
 g.dispose();
 }
 else {
 // draw the image on the screen
 Graphics g = screen.getDrawGraphics();
 g.drawImage(image, 0, 0, null);
 g.dispose();
 }
}
while (image.contentsLost());


This code simply loops until the image is successfully drawn on the screen.

Image-Drawing Benchmarks

Well, opaque and transparent images are accelerated, but just how fast are they? To find out, let's modify the ImageTest class to create an ImageSpeedTest class, shown in Listing 2.4. ImageSpeedTest draws the four images used in ImageTest repeatedly for a specified time period and then prints how many images were drawn per second to the console. CAUTION You should never spend this much time in the paint() method. In this simple example, no damage is done, but in a real game, this wouldn't work. The AWT event dispatch thread calls the paint() method, but the AWT event dispatch thread also handles keyboard, mouse, and lots of other events (which we'll get into in the next chapter), so doing this is essentially "choking" the AWT thread. We'll get into a better way to do something like this in the next section.


Listing 2.4 ImageSpeedTest.java
import java.awt.*;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
public class ImageSpeedTest extends JFrame {
 public static void main(String args[]) {
 DisplayMode displayMode;
 if (args.length == 3) {
 displayMode = new DisplayMode(
 Integer.parseInt(args[0]),
 Integer.parseInt(args[1]),
 Integer.parseInt(args[2]),
 DisplayMode.REFRESH_RATE_UNKNOWN);
 }
 else {
 displayMode = new DisplayMode(800, 600, 16,
 DisplayMode.REFRESH_RATE_UNKNOWN);
 }
 ImageSpeedTest test = new ImageSpeedTest();
 test.run(displayMode);
 }
 private static final int FONT_SIZE = 24;
 private static final long TIME_PER_IMAGE = 1500;
 private SimpleScreenManager screen;
 private Image bgImage;
 private Image opaqueImage;
 private Image transparentImage;
 private Image translucentImage;
 private Image antiAliasedImage;
 private boolean imagesLoaded;
 public void run(DisplayMode displayMode) {
 setBackground(Color.blue);
 setForeground(Color.white);
 setFont(new Font("Dialog", Font.PLAIN, FONT_SIZE));
 imagesLoaded = false;
 screen = new SimpleScreenManager();
 try {
 screen.setFullScreen(displayMode, this);
 synchronized (this) {
 loadImages();
 // wait for test to complete
 try {
 wait();
 }
 catch (InterruptedException ex) { }
 }
 }
 finally {
 screen.restoreScreen();
 }
 }
 public void loadImages() {
 bgImage = loadImage("images/background.jpg");
 opaqueImage = loadImage("images/opaque.png");
 transparentImage = loadImage("images/transparent.png");
 translucentImage = loadImage("images/translucent.png");
 antiAliasedImage = loadImage("images/antialiased.png");
 imagesLoaded = true;
 // signal to AWT to repaint this window
 repaint();
 }
 private final Image loadImage(String fileName) {
 return new ImageIcon(fileName).getImage();
 }
 public void paint(Graphics g) {
 // set text anti-aliasing
 if (g instanceof Graphics2D) {
 Graphics2D g2 = (Graphics2D)g;
 g2.setRenderingHint(
 RenderingHints.KEY_TEXT_ANTIALIASING,
 RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
 }
 // draw images
 if (imagesLoaded) {
 drawImage(g, opaqueImage, "Opaque");
 drawImage(g, transparentImage, "Transparent");
 drawImage(g, translucentImage, "Translucent");
 drawImage(g, antiAliasedImage,
 "Translucent (Anti-Aliased)");
 // notify that the test is complete
 synchronized (this) {
 notify();
 }
 }
 else {
 g.drawString("Loading Images...", 5, FONT_SIZE);
 }
 }
 public void drawImage(Graphics g, Image image, String name) {
 int width = screen.getFullScreenWindow().getWidth() -
 image.getWidth(null);
 int height = screen.getFullScreenWindow().getHeight() -
 image.getHeight(null);
 int numImages = 0;
 g.drawImage(bgImage, 0, 0, null);
 long startTime = System.currentTimeMillis();
 while (System.currentTimeMillis() - startTime
 < TIME_PER_IMAGE)
 {
 int x = Math.round((float)Math.random() * width);
 int y = Math.round((float)Math.random() * height);
 g.drawImage(image, x, y, null);
 numImages++;
 }
 long time = System.currentTimeMillis() - startTime;
 float speed = numImages * 1000f / time;
 System.out.println(name + ": " + speed + " images/sec");
 }
}


Don't think of this as the ultimate image-drawing benchmark. Remember, you're testing only four images on one machine. The results will vary depending on the computer's video card, processor speed, display mode, and whether your computer really feels like drawing quickly. To give you an idea, here are the results of this test on a 600MHz Athlon with a GeForce-256 video card, a display resolution of 800x600, and a bit depth of 16—and in a good mood at the time:

Opaque: 5550.599 images/sec Transparent: 5478.6953 images/sec Translucent: 85.2197 images/sec Translucent (Anti-Aliased): 113.18243 images/sec


As you can see, on this machine, translucent images are much slower than the hardware-accelerated opaque and transparent images. The reason the anti-aliased image is slightly faster than the fully translucent image is probably because there are more solid pixels in the image, so less blending is done when the image is drawn. In this case, transparent images are almost as fast as opaque ones. Note that some older video cards might not have the capability to draw hardware-accelerated transparent images and will have to resort to the same slow method used by translucent images. Okay, enough about hardware-accelerated images and drawing benchmarks! Let's move on to something you can use in a game: animation.

Animation

The first type of animation we'll go over is cartoon-style animation. This type of animation is displayed as a sequence of images, one after another. This is how animated cartoons work. For an example, let's use a more dynamic version of our hero. What if our hero's hair moved and eyes blinked, as shown in Screenshot?

Screenshot The three animated frames of our hero show that he looks quite innocent with his eyes closed.

Java graphics 02fig06.gif


The images in an animation can be referred to as frames. Each frame displays for a certain amount of time, but frames don't all have to display for the same amount of time. For example, the first frame might display for 200 milliseconds, the second for 75 milliseconds, and so on, as in Screenshot.

Screenshot An animation is made of several images, and each image displays for a certain amount of time. The same image can be used more than once.

Java graphics 02fig07.gif


Now we'll take the concept of an animation and turn it into code. We'll make it so our animations can use the same image more than once. Also, the animations will loop indefinitely instead of just playing through once. The Animation class in Listing 2.5 has three important methods: addFrame(), update(), and getImage(). The addFrame() method adds an image to the animation with a specified time (in milliseconds) to display. The update() method tells the animation that a specified amount of time has passed. Finally, the getImage() method gets the image that should be displayed based on the amount of time that has passed.

Listing 2.5 Animation.java
import java.awt.Image;
import java.util.ArrayList;
/**
 The Animation class manages a series of images (frames) and
 the amount of time to display each frame.
*/
public class Animation {
 private ArrayList frames;
 private int currFrameIndex;
 private long animTime;
 private long totalDuration;
 /**
 Creates a new, empty Animation.
 */
 public Animation() {
 frames = new ArrayList();
 totalDuration = 0;
 start();
 }
 /**
 Adds an image to the animation with the specified
 duration (time to display the image).
 */
 public synchronized void addFrame(Image image,
 long duration)
 {
 totalDuration += duration;
 frames.add(new AnimFrame(image, totalDuration));
 }
 /**
 Starts this animation over from the beginning.
 */
 public synchronized void start() {
 animTime = 0;
 currFrameIndex = 0;
 }
 /**
 Updates this animation's current image (frame), if
 necessary.
 */
 public synchronized void update(long elapsedTime) {
 if (frames.size() > 1) {
 animTime += elapsedTime;
 if (animTime >= totalDuration) {
 animTime = animTime % totalDuration;
 currFrameIndex = 0;
 }
 while (animTime > getFrame(currFrameIndex).endTime) {
 currFrameIndex++;
 }
 }
 }
 /**
 Gets this Animation's current image. Returns null if this
 animation has no images.
 */
 public synchronized Image getImage() {
 if (frames.size() == 0) {
 return null;
 }
 else {
 return getFrame(currFrameIndex).image;
 }
 }
 private AnimFrame getFrame(int i) {
 return (AnimFrame)frames.get(i);
 }
 private class AnimFrame {
 Image image;
 long endTime;
 public AnimFrame(Image image, long endTime) {
 this.image = image;
 this.endTime = endTime;
 }
 }
}


In the Animation class, you might notice the odd % character in the following line:

animTime = animTime % totalDuration;


In case you are unfamiliar with this, the % character is the remainder operator, returning the remainder from an integer division. For example, (% 3) equals 1—the integer result of 10 divided by 3 is 3, with 1 left over. It's used here to make sure the animation time starts over when the animation is done so that the animation loops. The Animation code is fairly straightforward. The inner class AnimFrame contains an image and the amount of time to display it. Most of the work is done in Animation's update() method, which selects the correct AnimFrame to use based on how much time has elapsed.

Active Rendering

To implement animation, you need a way to continuously update the screen in an efficient way. Before, you relied on the paint() method to do any rendering. You could call repaint() to signal the AWT event dispatch thread to repaint the screen, but this can cause delays because the AWT thread might be busy doing other things. Another option is to use active rendering. Active rendering is a term to describe drawing directly to the screen in the main thread. This way, you have control over when the screen actually gets drawn, and it simplifies the code a bit. To use the active rendering technique, use Component's getGraphics() method to get the graphics context for the screen:

Graphics g = screen.getFullScreenWindow().getGraphics();
draw(g);
g.dispose();


Pretty simple, isn't it? As in this example, don't forget to dispose of the Graphics object when you're done drawing. This cleans up some resources that the garbage collector might not get around to for a while.

The Animation Loop

Now you'll use active rendering to continuously draw in a loop. This loop is known as the animation loop. An animation loop follows these steps:

  1. Updates any animations

  2. Draws to the screen

  3. Optionally sleeps for a short period

  4. Starts over with step 1

In code, the animation loop might look something like this:

while (true) {
 // update any animations
 updateAnimations();
 // draw to screen
 Graphics g = screen.getFullScreenWindow().getGraphics();
 draw(g);
 g.dispose();
 // take a nap
 try {
 Thread.sleep(20);
 }
 catch (InterruptedException ex) { }
}


Obviously, in a real-world example, the animation loop shouldn't loop forever. In our examples, we'll make the animation loop stop after a few seconds. Now you have everything you need to try out some animation! AnimationTest1 in Listing 2.6 is a simple example that makes our hero blink. In AnimationTest1, you will update the entire screen every time you draw. Alternatively, you could update only the parts of the screen that have changed since the last draw. This method works great for games in which the background is static, such as the maze in Pac-Man. But most modern games have a dynamic background or have several things happening at once. Also, redrawing only what has changed since the last draw doesn't work when you're using page flipping, which we discuss later. In short, you'll just go ahead and update the entire screen in these examples.

Listing 2.6 AnimationTest1.java
import java.awt.*;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
public class AnimationTest1 {
 public static void main(String args[]) {
 DisplayMode displayMode;
 if (args.length == 3) {
 displayMode = new DisplayMode(
 Integer.parseInt(args[0]),
 Integer.parseInt(args[1]),
 Integer.parseInt(args[2]),
 DisplayMode.REFRESH_RATE_UNKNOWN);
 }
 else {
 displayMode = new DisplayMode(800, 600, 16,
 DisplayMode.REFRESH_RATE_UNKNOWN);
 }
 AnimationTest1 test = new AnimationTest1();
 test.run(displayMode);
 }
 private static final long DEMO_TIME = 5000;
 private SimpleScreenManager screen;
 private Image bgImage;
 private Animation anim;
 public void loadImages() {
 // load images
 bgImage = loadImage("images/background.jpg");
 Image player1 = loadImage("images/player1.png");
 Image player2 = loadImage("images/player2.png");
 Image player3 = loadImage("images/player3.png");
 // create animation
 anim = new Animation();
 anim.addFrame(player1, 250);
 anim.addFrame(player2, 150);
 anim.addFrame(player1, 150);
 anim.addFrame(player2, 150);
 anim.addFrame(player3, 200);
 anim.addFrame(player2, 150);
 }
 private Image loadImage(String fileName) {
 return new ImageIcon(fileName).getImage();
 }
 public void run(DisplayMode displayMode) {
 screen = new SimpleScreenManager();
 try {
 screen.setFullScreen(displayMode, new JFrame());
 loadImages();
 animationLoop();
 }
 finally {
 screen.restoreScreen();
 }
 }
 public void animationLoop() {
 long startTime = System.currentTimeMillis();
 long currTime = startTime;
 while (currTime - startTime < DEMO_TIME) {
 long elapsedTime =
 System.currentTimeMillis() - currTime;
 currTime += elapsedTime;
 // update animation
 anim.update(elapsedTime);
 // draw to screen
 Graphics g =
 screen.getFullScreenWindow().getGraphics();
 draw(g);
 g.dispose();
 // take a nap
 try {
 Thread.sleep(20);
 }
 catch (InterruptedException ex) { }
 }
 }
 public void draw(Graphics g) {
 // draw background
 g.drawImage(bgImage, 0, 0, null);
 // draw image
 g.drawImage(anim.getImage(), 0, 0, null);
 }
}


Screenshot


   
Comments