Java After Hours: 10 Projects Youll Never Do at Work
Applications that draw using sprites often do their drawing in a new execution streamthat is, a new thread. The reason for this is because while the new thread draws the sprites and moves them around, the main thread in the application can be doing other thingssuch as, in this application, closing the window when the user clicks the close button. The drawing and graphics work that goes on behind the scenes will take place in this new thread. To handle a new thread, you make sure that an application implements the Runnable interface: import java.awt.*; import java.awt.event.*; public class Aquarium extends Frame implements Runnable { . . . }
The Runnable interface has only one method, run, which is called when you start a new Thread object connected to the current object. Here's how you create and start that new thread, a Thread object named thread: import java.awt.*; import java.awt.event.*; public class Aquarium extends Frame implements Runnable { Image aquariumImage, memoryImage; Image[] fishImages = new Image[2]; MediaTracker tracker; Thread thread; Aquarium() { setTitle("The Aquarium"); . . . memoryImage = createImage(getSize().width, getSize().height); memoryGraphics = memoryImage.getGraphics(); thread = new Thread(this); thread.start(); this.addWindowListener(new WindowAdapter(){ public void windowClosing( WindowEvent windowEvent){ System.exit(0); } } ); } . . . } You can see the significant methods of the Thread class in Table 1.4.
To implement the Runnable interface and support the new thread, you have to add a run method with the code you want to execute in the new thread to the current object. When the thread.start method is called, the run method is called, executing the thread's code. To create and draw the sprites in this application, add the run method to the Aquarium class. To initialize the fish sprites, the code starts by determining the actual edges of the container they're in, which means determining the dimensions of the client window (the area inside all the toolbars and borders) of the current window. You can determine that area using various methods inherited by the Frame classjava.awt.Frame is built on top of java.awt.Window, which is built on top of java.awt.Container, which in turn is built on top of java.awt.Component. In this case, you can use the Component method getSize and the Container method getInsets to determine the actual dimensions in which the fish have to swim around, which is stored in a Java Rectangle object named edges: public class Aquarium extends Frame implements Runnable { Image aquariumImage, memoryImage; Image[] fishImages = new Image[2]; Thread thread; MediaTracker tracker; Graphics memoryGraphics; Aquarium() { . . . } public void run() { Rectangle edges = new Rectangle(0 + getInsets().left, 0 + getInsets().top, getSize().width - (getInsets().left + getInsets().right), getSize().height - (getInsets().top + getInsets().bottom)); . . . } Next in run, the code creates the fish sprites. Each fish will be represented by a Fish object that will do all the fish needs to doswim and draw itself in the aquarium. To keep track of the Fish objects, you'll need to store them in a Java Vector object named fishes, and the number of fish will be stored in an int named numberFishes: public class Aquarium extends Frame implements Runnable { Image aquariumImage, memoryImage; Image[] fishImages = new Image[2]; Thread thread; MediaTracker tracker; Graphics memoryGraphics; int numberFish = 12; Vector<Fish> fishes = new Vector<Fish>(); . . .
That's how things look if you're using Java 1.5 (the default for this book), where you need to specify the type of objects in a Vector; if you're using Java 1.4, declare the fishes vector this way: Vector fishes = new Vector(); Next, you create all the fish. The Fish class's constructor needs the two images for each fish (left-pointing and right-pointing), the edges rectangle so the fish knows the bounds of the aquarium, and a pointer to the Aquarium object itself so the fish knows where it's supposed to draw itself. Here's how the run method creates the fish, passing those values to the Fish class's constructor: public void run() { for (int loopIndex = 0; loopIndex < numberFish; loopIndex++){ Rectangle edges = new Rectangle(0 + getInsets().left, 0 + getInsets().top, getSize().width - (getInsets().left + getInsets().right), getSize().height - (getInsets().top + getInsets().bottom)); for (int loopIndex = 0; loopIndex < numberFish; loopIndex++){ fishes.add(new Fish(fishImages[0], fishImages[1], edges, this)); try { Thread.sleep(20); } catch (Exception exp) { System.out.println(exp.getMessage()); } } . . . }
That's the loop that actually creates the Fish objects. Want more fish? Just change the value assigned to numberFish in that variable's declaration. Try this for a blast: int numberFish = 120;
Note also that after each fish is created and added to the fishes vector, the code calls the THRead.sleep method. This method makes the thread pause by the number of milliseconds you passin this case, that's 20 milliseconds. The code pauses for 20 milliseconds between creating various fish because each fish uses the system time to generate its random position and velocity, and some systems can't resolve time periods less than thisif you simply created one fish immediately after the other, some would end up with exactly the same coordinates and velocity (which means one fish might be hidden behind another, even after they bounced off walls and kept moving around the aquarium). After the fish are created, all you have to do is to keep calling their swim method to have them move around the tank. Here's what that looks likenote that you can adjust how fast the fish move by changing the value assigned to the sleepTime variable, which is how long the thread sleeps between moving individual fish: public class Aquarium extends Frame implements Runnable { Image aquariumImage, memoryImage; Image[] fishImages = new Image[2]; Thread thread; MediaTracker tracker; Graphics memoryGraphics; int numberFish = 12; int sleepTime = 110; Vector<Fish> fishes = new Vector<Fish>(); . . . public void run() { for (int loopIndex = 0; loopIndex < numberFish; loopIndex++){ Rectangle edges = new Rectangle(0 + getInsets().left, 0 + getInsets().top, getSize().width - (getInsets().left + getInsets().right), getSize().height - (getInsets().top + getInsets().bottom)); fishes.add(new Fish(fishImages[0], fishImages[1], edges, this)); try { Thread.sleep(20); } catch (Exception exp) { System.out.println(exp.getMessage()); } } Fish fish; while (runOK) { for (int loopIndex = 0; loopIndex < numberFish; loopIndex++){ fish = (Fish)fishes.elementAt(loopIndex); fish.swim(); } try { Thread.sleep(sleepTime); } catch (Exception exp) { System.out.println(exp.getMessage()); } repaint(); } }
Note the runOK variable herethe while loop keeps calling the swim method of each fish while this variable is true. What's runOK all about? It's the variable used to end the thread when the application is ended. You add the declaration of this variable to the code this way: public class Aquarium extends Frame implements Runnable { Image aquariumImage, memoryImage; Image[] fishImages = new Image[2]; Thread thread; MediaTracker tracker; Graphics memoryGraphics; int numberFish = 12; int sleepTime = 110; Vector<Fish> fishes = new Vector<Fish>(); boolean runOK = true; . . .
And when the user closes the window, the application will set runOK to false before actually ending. Therefore, add the following code: this.addWindowListener(new WindowAdapter(){ public void windowClosing( WindowEvent windowEvent){ runOK = false; System.exit(0); } } ); That'll end the thread simply by making the run method finish and return. This way, we get rid of the thread when the application ends. |