dcsimg

The Lost Art of Writing Applets

When Java was introduced in 1994, it became an overnight success largely because of Netscape Navigator: Navigator put the Java interpreter on millions of computers. Wanting to capitalize on the new, suddenly pervasive Java platform, thousands of developers learned the Java language as quickly as possible, anxious to write applets for a hungry audience enthralled with the Internet.

When Java was introduced in 1994, it became an overnight success largely because of Netscape Navigator: Navigator put the Java interpreter on millions of computers. Wanting to capitalize on the new, suddenly pervasive Java platform, thousands of developers learned the Java language as quickly as possible, anxious to write applets for a hungry audience enthralled with the Internet.

Today, applets are still used for games, chat, and various thin-client applications, but few people learn Java to write applets. Instead, the hot spots of Java programming are servlets, application servers, and open source class libraries. In the web browser, applets have been eclipsed by Macromedia’s Flash, server-side web applications, and other alternatives.

But just as it appeared that applets were becoming passÉ, cellular phones, personal digital assistants, and other devices on the wireless Web have made applets relevant again. And few Java developers are better poised to code for those resource-limited environments than Karl Hörnell, one of the most experienced Java applet developers on the planet. H¨rnell, who distributes his work from his web site at http://www.javaonthebrain.com, has been writing Java applets and publishing source code and programming advice since 1996.

At the moment, H¨rnell offers 35 games and other applets on his web site. Most were written for a variety of web browsers with the goal of “write once, run everywhere,” even though he describes that effort as “a hopeless situation.”

His newer work has been written to run on mobile environments such as PalmOS, Nokia cell phones, and several emerging standards, including i-mode Java and Sun’s Mobile Information Device Profile (MIDP).

This month, Java Matters picks H¨rnell’s brain and takes a look at how he draws bitmap graphics in applets.

Bit-Mapped Animation








java_01
Figure One: images.gif contains a set of frames, aligned side-by-side

H¨rnell’s games contain numerous bitmapped animations. A good example is his IceBlox applet, a game where a penguin pushes blocks of ice around a cave, squashing enemies and dodging fire (just like the popular, early ’80s arcade game called “Pengo”). The game uses 49 images, all taken from a single GIF file. Let’s use H¨rnell’s technique in an application of our own.

Listing One contains Bats.java, a Java applet that demonstrates the familiar technique of composing an animation in an offscreen buffer Image. In addition to the offscreen buffer, Bats.java uses six other Image objects, one for each a frame of the bat animation and a string that displays the animation speed. The images.gif file, shown in Figure One, is 1,356 pixels wide and 354 pixels tall. Each image in the sequence is 256 pixels wide and 354 pixels tall, so those dimensions are used for the offscreen and onscreen Image objects.




Listing One: Bats.java, an animation applet – Part 1


1 import java.awt.*;
2 import java.awt.event.*;
3 import java.awt.image.*;
4 /* This Java 1.1 applet pulls six images of a bat from
5 the file images.gif, drawing each in sequence in the
6 applet window. Hit + or – to change the animation speed. */
7 public class Bats extends java.applet.Applet implements Runnable {
8 static int OFF_X=256, OFF_Y=354;
9 static int ON_X=256,ON_Y=354, BATS=6;
10 Image offImage, onImage, collection;
11 Image[] bats;
12 Graphics offGraphics, onGraphics;
13 int batstate;
14 Thread runner;
15 long delay = 100;
16 public void init() {
17 setBackground(Color.white);
18 // load the offscreen canvas
19 offImage = createImage(OFF_X, OFF_Y);
20 offGraphics = offImage.getGraphics();
21 offGraphics.setColor(Color.white);
22 offGraphics.fillRect(0, 0, OFF_X, OFF_Y);
23 // create the onscreen canvas
24 onImage = createImage(ON_X, ON_Y);
25 onGraphics= onImage.getGraphics();
26 onGraphics.setColor(Color.white);
27 // wait until the graphic is loaded
28 MediaTracker media = new MediaTracker(this);
29 collection = getImage(getCodeBase(),”images.gif”);
30 media.addImage(collection, 0);
31 try {
32 media.waitForID(0);
33 } catch(InterruptedException ie) { // do nothing
34 }
35 // create the individual bats
36 ImageProducer producer = collection.getSource();
37 bats = new Image[BATS];
38 for (int i = 0; i < BATS; i++) {
39 ImageFilter filter = new CropImageFilter(256 *
40 i, 0, 256, 354);
41 bats[i] = createImage(new
42 FilteredImageSource(producer,filter));
43 media.addImage(bats[i], 1);
44 }
45 // wait for bats to be available
46 try {
47 media.waitForID(1);
48 } catch(InterruptedException ie) {
49 // do nothing
50 }
51 batstate = 0;
52 resize(ON_X, ON_Y);
53 }
54 public void start() {
55 // create the animation thread
56 if (runner == null) {
57 runner = new Thread(this);
58 runner.start();
59 }
60 }
61 public void stop() {
62 // stop the animation thread
63 if (runner != null) {
64 runner = null;
65 }
66 }
67 public void run() {
68 // run until the applet is stopped
69 Thread current = Thread.currentThread();
70 while (runner == current) {
71 try {
72 runner.sleep(delay);
73 } catch (InterruptedException ie) {
74 //do nothing
75 }
76 // cycle through the animation frames
77 batstate++;
78 if (batstate > (BATS – 1)) {
79 batstate = 0;
80 }
81 // draw the new bat on the offscreen canvas
82 offGraphics.drawImage(bats[batstate],0,0,this);
83 offGraphics.setColor(Color.black);
84 // report the animation delay
85 offGraphics.drawString(“Rate: ” + delay +
86 ” Hit +/- to change”, 5, OFF_Y – 25);
87 repaint();
88 }
89 }
90 public boolean keyDown(Event evt, int key) {
91 switch (key) {
92 // increase delay when + key hit
93 case 43:
94 delay++;
95 break;
96 // decrease delay when – key hit
97 case 45:
98 if (delay > 0) {
99 delay–;
100 }
101 break;
102 default:
103 break;
104 }
105 return false;
106 }
107 // make sure the applet window has keyboard focus
108 public boolean mouseDown(Event e, int x, int y) {
109 requestFocus();
110 return false;
111 }
112 public void paint(Graphics g) {
113 g.drawImage(offImage, 0, 0, this);
114 }
115 public void update(Graphics g) {
116 paint(g);
117 }
118 }

Lines 19-34 in the init() method in Listing One create the images and load the graphics file images.gif into another Image object called collection. A MediaTracker object is used to stall until the graphic completes loading — the call to waitForID() won’t return until it loads successfully or some kind of error aborts the attempt.

After the graphic is loaded, it’s chopped up to create six separate Image objects using an interface and two classes from the java.awt.image package: ImageProducer, CropImageFilter, and FilteredImageSource, respectively. To crop an Image object, you must get the ImageProducer that created the image. As coded on line 36, the image’s getSource() method returns this object.

The cropped area of an image is represented by a CropImageFilter object, which is created using the same four arguments used to define a rectangle in many windowing classes: the (x,y) coordinates of the upper-left corner, followed by the width and height. And because the six bats are lined up side-by-side, a simple for loop (lines 38-44) is used to crop the correct area containing each creature. Crop ImageFilter is a subclass of ImageFilter, the superclass of several image-manipulating classes in the Abstract Windowing Toolkit.

Once an ImageProducer and an ImageFilter are available, a new FilteredImageSource can be created by using those two former classes as arguments to the constructor (lines 41-43). FilteredImageSource is itself an image producer, so it can be used to create an Image containing nothing but the cropped part of the original image. The HTML code required to display this applet is shown in Listing Three.




Listing Three: Bats.html, a snippet of HTML for the applet


<applet code=”Bats.class” width=”256″ height=”354″>
</applet>

Like IceBlox, the Bats applet is written using Java 1.1. This makes the program usable in the widest range of web browsers and other environments that support applets, and doesn’t require the overhead associated with Swing or the Java Plug-in.

Image Creation








java_02
Figure Two: Images for the maze are kept in one file, pipes.gif

In the IceBlox applet, H¨rnell uses several Image objects to hold all of the characters and obstacles in the game. In IceBlox, the penguin and other elements of the game can be redrawn quickly enough to realize the animation rate required by the game.

For another project, a two-dimensional maze escape game called Dungeon Dregs, more than 100 drawing operations are required whenever a player completes one level and moves to another. The Graphics method drawImage() isn’t fast enough for the task, so an alternative, copyArea(), is used with two off-screen buffers instead of one. Copying bits within the same buffer is significantly faster and requires a single Image object rather than one for each visual element being drawn.

The Pipes applet in Listing Two demonstrates this technique, drawing a maze using sixteen individual segments. The maze is created by drawing 324 separate squares in an 18-by-18 grid, where each square is one of the sixteen, bit-mapped images representing walls, corners, and wall endpoints shown in Figure Two.




Listing Two: Pipes.java, a maze drawing applet


1 import java.awt.*;
2 /** This Java 1.1 applet draws a maze using 324 separate
3 drawing operations, using two buffers and the copyArea
4 method to update the screen quickly. */
5 public class Pipes extends java.applet.Applet {
6 static int HEIGHT = 360, WIDTH = 360;
7 static int ROWS = 18, COLUMNS = 18;
8 static int PIPE_HEIGHT = 20, PIPE_WIDTH = 20;
9 Image offImage1, offImage2, onImage, pipes;
10 Graphics offGraphics1, offGraphics2, onGraphics;
11 public void init() {
12 setBackground(Color.white);
13 // load the first offscreen buffer
14 offImage1 = createImage(WIDTH, HEIGHT + PIPE_HEIGHT);
15 offGraphics1 = offImage1.getGraphics();
16 offGraphics1.setColor(Color.white);
17 offGraphics1.fillRect(0, 0, WIDTH, HEIGHT);
18 // wait until the pipe graphic is loaded
19 MediaTracker media = new MediaTracker(this);
20 pipes = getImage(getCodeBase(), “pipes.gif”);
21 media.addImage(pipes, 0);
22 try {
23 media.waitForID(0);
24 } catch (InterruptedException ie) {
25 // do nothing
26 }
27 offGraphics1.drawImage(pipes, 0, HEIGHT, this);
28 // create maze
29 int[] maze = {
30 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3,
31 4, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 5,
32 4, 4, 1, 2, 3, 1, 2, 2, 3, 1, 2, 2, 2, 2, 2, 3, 5, 5,
33 4, 4, 4, 0, 4, 6, 2, 2, 8, 5, 1, 11, 1, 2, 2, 8, 15, 5,
34 4, 4, 4, 0, 6, 2, 2, 2, 2, 8, 6, 3, 5, 14, 2, 2, 3, 5,
35 4, 9, 6, 2, 2, 3, 1, 3, 0, 1, 3, 5, 6, 2, 2, 3, 5, 5,
36 4, 1, 2, 2, 11, 4, 4, 6, 2, 8, 5, 5, 14, 2, 3, 5, 5, 5,
37 4, 4, 1, 2, 2, 8, 6, 2, 3, 1, 8, 6, 2, 2, 8, 5, 5, 5,
38 4, 4, 4, 1, 2, 2, 2, 11, 4, 5, 1, 2, 2, 2, 2, 8, 5, 5,
39 4, 4, 4, 4, 1, 7, 7, 7, 8, 5, 5, 1, 7, 3, 1, 3, 5, 5,
40 4, 4, 4, 4, 6, 7, 7, 3, 1, 8, 6, 8, 0, 5, 5, 5, 5, 5,
41 4, 4, 4, 6, 7, 7, 13, 4, 6, 7, 7, 7, 7, 8, 15, 5, 5, 5,
42 4, 9, 6, 7, 7, 7, 7, 8, 1, 7, 3, 1, 7, 7, 7, 8, 5, 5,
43 4, 1, 7, 7, 7, 7, 7, 3, 4, 16, 5, 6, 13, 1, 7, 13, 5, 5,
44 4, 4, 1, 7, 7, 7, 7, 8, 4, 5, 5, 1, 3, 6, 7, 3, 5, 5,
45 4, 4, 6, 7, 7, 7, 7, 13, 6, 8, 5, 5, 6, 7, 7, 8, 5, 5,
46 4, 6, 7, 7, 7, 7, 7, 7, 7, 7, 8, 6, 7, 7, 7, 7, 8, 5,
47 6, 7, 7, 7, 7, 7, 7, 7, 3, 1, 7, 7, 7, 7, 7, 7, 7, 8
48 };
49 int piece;
50 for (int row = 0; row < ROWS; row++) {
51 for (int col = 0; col < COLUMNS; col++) {
52 piece = maze[row * ROWS + col];
53 offGraphics1.copyArea(
54 piece * PIPE_WIDTH – 20,
55 HEIGHT,
56 PIPE_WIDTH,
57 PIPE_HEIGHT,
58 col * PIPE_WIDTH – (piece * PIPE_WIDTH – 20),
59 row * PIPE_HEIGHT – HEIGHT
60 );
61 }
62 }
63 // create primary off-screen buffer
64 offImage2 = createImage(WIDTH, HEIGHT);
65 offGraphics2 = offImage2.getGraphics();
66 offGraphics2.setColor(Color.white);
67 offGraphics2.fillRect(0, 0, WIDTH, HEIGHT);
68 // draw maze
69 offGraphics2.drawImage(offImage1, 0, 0, this);
70 }
71 public void paint(Graphics g) {
72 g.drawImage(offImage2, 0, 0, this);
73 }
74 public void update(Graphics g) {
75 paint(g);
76 }
76 }

The project uses two off-screen buffers: offImage1 and offImage2. offImage1 is the workspace for offImage2, holding enough space for an applet window 360 by 360 pixels in size and the pipes.gif graphic, which holds all sixteen maze pieces. Lines 13-27 of Listing Two create the buffer and load and draw the graphic to the buffer.

After the graphic is loaded, the maze is drawn in the buffer by copying pieces of it to the area reserved for the applet window. An integer array, maze[], holds values representing the piece to use. The full code is in Listing Four at lines 29-45




Listing Four: Pipes.html displays the maze applet


<applet code=”Pipes.class” width=”360″ height=”360″>
</applet>

A for loop cycles through the array, one maze piece at a time. The copyArea() method is called during each iteration, copying a 20-by-20 piece to a spot in the maze. copyArea() takes six arguments. The first four define a rectangular area using the (x,y) coordinates of its upper left corner and the rectangle’s width and height. The last two arguments indicate the position where the copied rectangle should be duplicated. This position is relative to the (x,y) coordinates used as the first two arguments — negative numbers move to the left and upwards, respectively, and positive numbers move to the right and downwards. For example, the 38th piece in the maze is copied from (60,360) to (40,40).

The Pipes applet, which employs the same level creation technique as Dungeon Dregs, is designed to be extended with additional mazes. Listing Four contains the HTML necessary to present the applet.

These examples represent a small sampling of the programming techniques that H¨rnell demonstrates on his Web site, which contains more than 250 pages documenting his games. H¨rnell’s currently porting an improved version of IceBlox to as many mobile devices as he can — at present, it works on 15 brands of Sony Ericsson and Nokia phones.

Next month, Java Matters compares the Java development kits and runtime environments available on Linux.



Rogers Cadenhead is a Web application developer and book author. To contact Cadenhead, visit his weblog at http://www.cadenhead.org/workbench. You can download the images and source code used in this article from http://www.linux-mag.com/downloads/2003-05/java.

Comments are closed.