by Tim Macinta
Creating games with Java is a lot like creating games with other languages. You have to deal with the design of movable objects (often referred to as sprites), the design of a graphics engine to keep track of the movable objects, and double buffering to make movement look smooth. There is a good chance that future versions of Java will provide built-in support for sprites and double buffering, but for now, we have to add the support ourselves. This chapter covers methods for creating these standard building blocks of games using Java.
Thankfully, Java takes care of a lot of the dirty work you would have to do if you were writing a game in another language. For example, Java provides built-in support for transparent pixels, making it easier to write a graphics engine that can draw nonrectangular objects. Java also has built-in support for allowing several different programs to run at once-perfect for creating a world with a lot of creatures, each with its own special methods for acting. However, the added bonuses of Java can turn into handicaps if they are not used properly. This chapter deals with how to use the advantages of Java to write games and how to avoid the pitfalls that accompany the power.
A graphics engine is essential to a well-designed game in Java. A graphics engine is an object that is given the duty of painting the screen. The graphics engine keeps track of all objects on the screen at one time, the order in which to draw the objects, and the background to be drawn. By far, the most important function of the graphics engine is the maintenance of movable object blocks.
So what's the big fuss about movable object blocks (MOBs)? Well, they make your life infinitely easier if you're interested in creating a game that combines graphics and user interaction, as most games do. The basic concept of a movable object is that the object contains both a picture that will be drawn on the screen and information that tells you where the picture is to be drawn on the screen. To make the object move, you simply tell the movable object (more precisely, the graphics engine that contains the movable object) which way to move and you're done-redrawing is automatically taken care of.
The bare-bones method for making a movable object block in Java
is shown in Listing 41.1. As you can see, the movable object consists
merely of an image and a set of coordinates. You may be thinking,
"Movable objects are supposed to take care of all the redrawing
that needs to be done when they are moved. How is that possible
with just the code from Listing 41.1?" Redrawing is the graphics
engine's job. Don't worry about it for now; it's covered a little
later.
Note |
If you don't feel like typing all the code from Listings 41.1 through 41.4, you can find finished versions of all the code in the CHAP41 directory on the CD-ROM that accompanies this book. The files that correspond to Listings 41.1 through 41.4 are called MOB.java, GraphicsEngine.java, Game.java, and example.html. You can see the final version of the GraphicsEngine test by viewing the page entitled example.html with a Java-enabled browser. |
Listing 41.1. Use this code to create a bare-bones movable
object block (MOB). Save this code in a file called MOB.java.
import java.awt.*; public class MOB { public int x = 0; public int y = 0; public Image picture; public MOB(Image pic) { picture=pic; } }
As you can see in Listing 41.1, the constructor for our movable object block (MOB) takes an Image and stores it away to be drawn when needed. After we've instantiated an MOB (that is, after we've called the constructor), we have a movable object that we can move around the screen just by changing its x and y values. The engine will take care of redrawing the movable object in the new position, so what else is there to worry about?
One thing to consider is the nature of the picture that is going to be drawn every time the movable object is drawn. Consider the place in which the image probably will originate. In all likelihood, the picture will either come from a GIF or JPEG file, which has one very important consequence-it will be rectangular. So what? Think about what your video game will look like if all your movable objects are rectangles. Your characters would be drawn, but so would their backgrounds. Chances are, you'll want to have a background for the entire game; it would be unacceptable if the unfilled space on character images covered up your background just because the images were rectangular and the characters were of another shape.
When programming games in other languages, this problem is often
resolved by examining each pixel in a character's image before
drawing it to see whether it's part of the background. If the
pixel is not part of the background, it's drawn as normal. If
the pixel is part of the background, it's skipped and the rest
of the pixels are tested. Pixels that aren't drawn usually are
referred to as transparent pixels. If this seems like a
laborious process to you, it is. Fortunately, Java has built-in
support for transparent colors in images, which simplifies your
task immensely. You don't have to check each pixel for transparency
before it's drawn because Java can do that automatically! Java
even has built-in support for different levels of transparency.
For example, you can create pixels that are 20-percent transparent
to give your images a ghostlike appearance. For now, though, we'll
deal only with fully transparent pixels.
Note |
Whether or not you know it, you are probably already familiar with transparent GIFs. If you have ever stumbled across a Web page with an image that wasn't perfectly rectangular, you were probably looking at a transparent GIF. |
Java's capability to draw transparent pixels makes the task of
painting movable objects on the screen much easier. But how do
you tell Java what pixels are transparent and what pixels aren't?
You could load the image and run it through a filter that changes
the ColorModel, but that
would be doing it the hard way. Fortunately, Java supports transparent
GIF files. Whenever a transparent GIF file is loaded, all the
transparency is preserved by Java. That means your job just got
a lot easier.
Note |
A new Adobe PhotoShop plug-in allows you to save your images as transparent (and interlaced) GIFs. You can download it from http://www.adobe.com. |
Now the problem becomes how to make transparent GIFs. This part is easier than you think. Simply use your favorite graphics package to create a GIF file (or a picture in some other format that you can eventually convert to a GIF file). Select a color that doesn't appear anywhere in the picture and fill all areas that you want to be transparent with the selected color. Make a note of the RGB value of the color you use to fill in the transparent places. Now you can use a program to convert your GIF file into a transparent GIF file. I use Giftool, available at http://www.homepages.com/tools/index.html, to make transparent GIF files. You simply pass to Giftool the RGB value of the color you selected for transparency, and Giftool makes that color transparent inside the GIF file. Giftool is also useful for making your GIF files interlaced. Interlaced GIF files are the pictures that initially appear with block-like edges and keep getting more defined as they continue to load.
Now you have movable objects that know where they're supposed to be and don't eat up the background as they go there. The next step is to design something that will keep track of your movable objects and draw them in the proper places when necessary. This is the job of our GraphicsEngine class. Listing 41.2 shows the bare bones of a graphics engine. This is the minimum you need to handle multiple movable objects. Even this engine leaves out several things that nearly all games need, but we'll get to those things later. For now, let's concentrate on how this bare-bones system works to give you a solid grasp of the basic concepts.
Listing 41.2. A bare-bones graphics engine that tracks your
movable objects. The code should be saved in a file called GraphicsEngine.java.
import java.awt.*; import java.awt.image.*; public class GraphicsEngine { Chain mobs = null; public GraphicsEngine() {} public void AddMOB (MOB new_mob) { mobs = new Chain(new_mob, mobs); } public void paint(Graphics g, ImageObserver imob) { Chain temp_mobs = mobs; MOB mob; while (temp_mobs != null) { mob = temp_mobs.mob; g.drawImage(mob.picture, mob.x, mob.y, imob); temp_mobs = temp_mobs.rest; } } } class Chain { public MOB mob; public Chain rest; public Chain(MOB mob, Chain rest) { this.mob = mob; this.rest = rest; } }
Before we detail how the GraphicsEngine class works, let's touch on the Chain class. The Chain class looks rather simple-and it can be-but don't let that fool you. Entire languages such as LISP and Scheme have been built around data structures that have the same function as the Chain class. The Chain class is simply a data structure that holds two objects. Here, we're calling those two objects item and rest because we are going to use Chain to create a linked list. The power of the Chain structure-and those structures like it-is that it can be used as a building block to create a multitude of more complicated structures. These structures include circular buffers, binary trees, weighted di-graphs, and linked lists, to name a few. Using the Chain class to create a linked list is suitable for our purposes.
Our goal is to keep a list of the moveable objects that have to
be drawn. A linked list suits our purposes well because a linked
list is a structure that is used to store a list of objects. It
is referred to as a "linked" list because each point
in the list contains an object and a link to a list with the remaining
objects.
Note |
The concept of a linked list-the Chain class in this case-may be a little hard to grasp at first because it is defined recursively. If you don't understand it right away, don't worry. Try to understand how the Chain class is used in the code first and study Figure 41.1; the technical explanation should make more sense. |
Figure 41.1: A graphical representation of a Chain.
To understand what a linked list is, think of a train as an example of a linked list: Consider the train to be the first car followed by the rest of the train. The "rest of the train" can be described as the second car followed by the remaining cars. This description can continue until you reach the last car, which can described as the caboose followed by nothing. A Chain class is analogous to a train. A Chain can be described as a movable object followed by the rest of the Chain, just as a train can be described as a car followed by the rest of the train. And just as the rest of the train can be considered a train by itself, the rest of the Chain can be considered a Chain by itself, and that's why the rest is of type Chain.
From the looks of the constructor for Chain,
it appears that you need an existing Chain
to make another Chain. This
makes sense when you already have a Chain
and want to add to it, but how do you start a new Chain?
To do this, create a Chain
that is an item linked to nothing. How do you link an item to
nothing? Use the Java symbol for nothing-null-to
represent the rest Chain.
If you look at the code in Listing 41.2, that's exactly what we
did. Our instance variable mobs
is of type Chain, and it
is used to hold a linked list of movable objects. Look at the
method AddMOB() in Listing
41.2. Whenever we want to add another movable object to the list
of movable objects we're controlling, we simply make a new list
of movable objects that has the new movable object as the first
item and the old Chain as
the rest of the list. Notice that the initial value of mobs
is null, which is used to
represent nothing.
Note |
You may wonder why we even bothered making a method called AddMOB() when it only ended up being one line long. The point in making short methods like AddMOB() is that if GraphicsEngine were subclassed in the future, it would be a lot easier to add functionality if all you have to do is override one method as opposed to changing every line of code that calls AddMOB(). For example, if you wanted to sort all your moveable objects by size, you could just override AddMOB() so that it stores all the objects in a sorted order to begin with. |
Now that we have a method for keeping a list of all the objects that have to be painted, let's review how to use the list. The first thing you should be concerned about is how to add new objects to the list of objects that have to be painted. You add a new object to the list by using the AddMOB() method shown in Listing 41.2. As you can see from the listing, all the AddMOB() method does is to replace the old list of objects stored in mobs with a new list that contains the new object and a link to the old list of objects.
How do we use the list of movable objects once AddMOB() has been called for all the movable objects we want to handle? Take a look at the paint() method. The first thing to do is copy the pointer to mobs into a temporary Chain called temp_mobs. Note that the pointer is copied, not the actual contents. If the contents were copied instead of the pointer, this approach would take much longer and would be much more difficult to implement. "But I thought Java doesn't have pointers," you may be thinking at this point. That's not exactly true; Java doesn't have pointer arithmetic, but pointers are still used to pass arguments, although the programmer never has direct access to these pointers.
temp_mobs now contains a pointer to the list of all the movable objects to be drawn. The task at hand is to go through the list and draw each movable object. The variable mob is used to keep track of each movable object as we get to it. The variable temp_mobs represents the list of movable objects we have left to draw (that's why we started it off pointing to the whole list). We'll know all our movable objects have been drawn when temp_mobs is null, because that will be just like saying the list of movable objects left to draw is empty. That's why the main part of the code is encapsulated in a while loop that terminates when temp_mobs is null.
Look at the code inside the while loop of the paint() method. The first thing that is done is to assign mob to the movable object at the beginning of the temp_mobs Chain so that there is an actual movable object to deal with. Now it's time to draw the movable object. The g.drawImage() command draws the movable object in the proper place. The variable mob.picture is the picture stored earlier when the movable object was created. The variables mob.x and mob.y are the screen coordinates at which the movable object should be drawn; notice that paint() looks at these two variables every time the movable object is drawn, so changing one of these coordinates while the program is running has the same effect as moving it on the screen. The final argument passed to g.drawImage(), imob, is an ImageObserver that is responsible for redrawing an image when it changes or moves. Don't worry about where to get an ImageObserver from; chances are, you'll be using the GraphicsEngine class to draw inside a Component (or a subclass of Component such as Applet), and a Component implements the ImageObserver interface so that you can just pass the Component to GraphicsEngine whenever you want to repaint.
The final line inside the while loop shortens the list of movable objects that have to be drawn. It points temp_mobs away from the Chain that it just drew a movable object off the top of and points it to the Chain that contains the remainder of the MOBs. As we continue to cut down the list of MOBs by pointing to the remainder, temp_mobs eventually winds up as null, which ends the while loop with all our movable objects drawn. Figure 41.1 provides a graphical explanation of this process.
The graphics engine in Listing 41.2 certainly had some important things left out, but it does work. Let's go over how to install the GraphicsEngine inside a Component first, and then go back and improve on the design of the graphics engine and the MOB.
Listing 41.3 shows an example of how to install the GraphicsEngine
inside a Component. It just
so happens that the Component
we're installing it in is an Applet
(remember that an Applet
is a subclass of Component)
so we can view the results with a Web browser. Keep in mind that
the same method called to use the GraphicsEngine
inside an Applet can also
be used to install the GraphicsEngine
inside other Components.
Note |
Remember that you can find all the code presented in the listings in this chapter on the CD-ROM that accompanies this book. |
Before trying to understand the code in Listing 41.3, it would be a good idea to type and compile Listings 41.1 through 41.3 so that you can get an idea of what the code does. Save each listing with the filename specified in each listing's header and compile the code by using the javac command on each of those files.
In addition to compiling the code in Listings 41.1 through 41.3, you must also create the HTML file as shown in Listing 41.4. (Use a Java-enabled browser or the JDK applet viewer to view this file once you have compiled everything.) As the final step, you have to place a small image file to be used as the movable object in the same directory as the code and either rename it to one.gif or change the line inside the init() method in Listing 41.3 that specifies the name of the picture being loaded.
Listing 41.3. This sample applet illustrates the GraphicsEngine
class. The code should be saved in a file named Game.java.
import java.awt.*; import java.applet.Applet; import java.net.URL; public class Game extends Applet { GraphicsEngine engine; MOB picture1; public void init() { try { engine = new GraphicsEngine(); Image image1 = getImage(new URL(getDocumentBase(), "one.gif")); picture1 = new MOB(image1); engine.AddMOB(picture1); } catch (java.net.MalformedURLException e) { System.out.println("Error while loading pictures..."); e.printStackTrace(); } } public void update(Graphics g) { paint(g); } public void paint(Graphics g) { engine.paint(g, this); } public boolean mouseMove (Event evt, int mx, int my) { picture1.x = mx; picture1.y = my; repaint(); return true; } }
Listing 41.4. This code must be put into an HTML file in order
to view the applet.
<html> <head> <title>GraphicsEngine Example</title> </head> <body> <h1>GraphicsEngine Example</h1> <applet code="Game.class" width=200 height=200> </applet> </body> </html>
Once you have the example up and running, the image you selected should appear in the upper-left corner of the applet's window. Pass your mouse over the applet's window. The image you have chosen should follow your pointer around the window. If you use the images off the CD-ROM that accompanies this book, you should see something similar to Figure 41.2.
Figure 41.2: This is what the example of the barebones GraphicsEngine should look like.
Let's go over how the code that links the GraphicsEngine into the applet called Game works. Our instance variables are engine, which controls all the movable objects we can deliver, and picture1, a movable object that draws the chosen image .
Take a look at the fairly straightforward init() method. You initialize engine by setting it equal to a new GraphicsEngine. Next, the image you chose is loaded with a call to getImage(). This line creates the need for the try and catch statements that surround the rest of the code to catch any invalid URLs. After the image is loaded, it is used to create a new MOB, and picture1 is initialized to this new MOB. The work is completed by adding the movable object to engine so that engine will draw it in the future. The remaining lines (the lines inside the catch statement) are there to provide information about any errors that occur.
The update() method is used to avoid flickering. By default, applets use the update() method to clear the window they live in before they repaint themselves with a call to their paint() method. This can be a useful feature if you're changing the display only once in a while, but with graphics-intensive programs, this can create a lot of flicker because the screen refreshes itself frequently. Because the screen refreshes itself so frequently, once in a while, it catches the applet at a point at which it has just cleared its window and hasn't yet had a chance to redraw itself. This situation is what causes flicker.
The flicker was eliminated here simply by leaving out the code that clears the window and going straight to the paint() method. If you already ran this example applet, you have probably already noticed that although not clearing the screen solves the problem of flickering, it creates another problem: the movable object is leaving streaks! (If you haven't run the applet yet, you can see the streaks in Figure 41.2.) Don't worry; the streaks will be eliminated a little later when we introduce double buffering into our graphics engine.
As you can see, the Game.paint() method consists of one line: a call to the paint() method in engine. It might seem like a waste of time going from update() to paint() to engine.paint() just to draw one image. Once you have a dozen or more movable objects on the screen at once, however, you'll appreciate the simplicity of being able to add the object in the init() method and then forget about it the rest of the time, letting the engine.paint() method take care of everything.
Finally, we have the mouseMove() method. This is what provides the tracking motion so that the movable object follows your pointer around the window. There are, of course, other options for user input that are discussed later in this chapter. The tracking is accomplished simply by setting the coordinates of the movable object to the position of the mouse. The call to repaint() just tells the painting thread that something has changed; the painting thread calls paint() when it gets around to it, so you don't have to worry about redrawing any more. To finish up, true is returned to inform the caller that the mouseMove() event was taken care of.
Now that the framework has been laid for a functional graphics engine, it's time to make improvements. Let's start with movable objects. What should be considered when thinking about the uses movable objects have in games? Sooner or later, chances are that you'll want to write a game with a lot of movable objects. It would be much easier to come up with some useful properties that you want all your movable objects to have now so that you don't have to deal with each movable object individually later.
One area that merits improvement is the order in which movable objects are painted. What if you had a ball (represented by a movable object) that was bouncing along the screen, and you wanted it to travel in front of a person (also represented by a movable object)? How could you make sure that the ball was drawn after the person every time, to make it look like the ball was is in front of the person? You could make sure that the ball is the first movable object added to the engine, ensuring that it's always the last movable object painted. However, that approach can get hairy if you have 10 or 20 movable objects that all have to be in a specific order. Also, what if you wanted the same ball to bounce back across the screen later on, but this time behind the person? The method of adding movable objects in the order you want them drawn obviously wouldn't work, because you would be switching the drawing order in the middle of the program.
What is needed is some sort of prioritization scheme. The improved version of the graphics engine implements a scheme in which each movable object has an integer that represents its priority. The movable objects with the highest priority number are drawn last and thus appear in front.
Listing 41.5 shows the changes that have to be made to the MOB
class to implement prioritization. Listing 41.6 shows the changes
that have to be made to the GraphicsEngine
class, and List
ing 41.7 shows the changes that have to be made to the Game
applet. Several other additional features have also been added
in these listings, and we'll touch on those later.
Note |
Our prioritization scheme does not impose any restrictions on the priority of each object. There is no need to give objects sequential priorities (that is, you can give objects priorities like 1, 5, and 20 instead of using priorities 1, 2, and 3). You can also assign the same priority to more than one object if you don't care which object is drawn on top (that is, you can leave all your objects with the default priority of zero). |
The heart of the prioritization scheme lies in the new version of GraphicsEngine.paint(). The basic idea is that before any movable objects are drawn, the complete list of movable objects is sorted by priority. The highest priority objects are put at the end of the list so that they are drawn last and appear in front; the lowest priority objects are put at the beginning of the list so that they are drawn first and appear in back. A bubble sort algorithm is used to sort the objects. Bubble sort algorithms are usually slower than other algorithms, but they tend to be easier to implement. In this case, the extra time taken by the bubble sort algorithm is relatively negligible because the majority of time within the graphics engine is eaten up displaying the images.
Update your code in the files MOB.java, GraphicsEngine.java, and Game.java with the updated code shown in Listings 41.5, 41.6, and 41.7 respectively. Compile the updated versions of all three files and add an image called background.jpg to your directory (this image is the background image for the applet). You should see something that looks like Figure 41.3 (especially if you're using the files from the CD-ROM that accompanies this book). After getting the example up and running, look at the init() method in the Game class and pay particular attention to the priorities assigned to each movable object. From looking at the mouseMove() method, you should be able to see that the first five movable objects line up in a diagonal line of sorts (as long as you move the mouse slowly). If you move the mouse slowly, you should see that three of the first five movable objects are noticeably in front of the other two. This should make sense if you examine the priorities they were assigned inside the Game.init() method.
Figure 41.3: What the final version of the GraphicsEngine example should look like.
Also notice that the bouncing object is always in front of the objects you control with your mouse. This is because it was assigned a higher priority than all the other objects. Now press the S key. The first object that your mouse controls should now be displayed in front of the bouncing object. Take a look at the Game.keyDown() method to see why this occurs. You will see that pressing the S key toggles the priority of picture1 between a priority that is lower than the bouncing object and a priority that is higher than the bouncing object.
Listing 41.5. The enhanced version of the MOB
class. Save this code in a file named MOB.java.
import java.awt.*; public class MOB { public int x = 0; public int y = 0; public Image picture; public int priority = 0; public boolean visible = true; public MOB(Image pic) { picture=pic; } }
Listing 41.6. The enhanced version of the GraphicsEngine
class. Save the code in a file called GraphicsEngine.java.
import java.awt.*; import java.awt.image.*; public class GraphicsEngine { Chain mobs = null; public Image background; public Image buffer; Graphics pad; public GraphicsEngine(Component c) { buffer = c.createImage(c.size().width, c.size().height); pad = buffer.getGraphics(); } public void AddMOB (MOB new_mob) { mobs = new Chain(new_mob, mobs); } public void paint(Graphics g, ImageObserver imob) { /* Draw background on top of buffer for double buffering. */ if (background != null) { pad.drawImage(background, 0, 0, imob); } /* Sort MOBs by priority */ Chain temp_mobs = new Chain(mobs.mob, null); Chain ordered = temp_mobs; Chain unordered = mobs.rest; MOB mob; while (unordered != null) { mob = unordered.mob; unordered = unordered.rest; ordered = temp_mobs; while (ordered != null) { if (mob.priority < ordered.mob.priority) { ordered.rest = new Chain(ordered.mob, ordered.rest); ordered.mob = mob; ordered = null; } else if (ordered.rest == null) { ordered.rest = new Chain(mob, null); ordered = null; } else { ordered = ordered.rest; } } } /* Draw sorted MOBs */ while (temp_mobs != null) { mob = temp_mobs.mob; if (mob.visible) { pad.drawImage(mob.picture, mob.x, mob.y, imob); } temp_mobs = temp_mobs.rest; } /* Draw completed buffer to g */ g.drawImage(buffer, 0, 0, imob); } } class Chain { public MOB mob; public Chain rest; public Chain(MOB mob, Chain rest) { this.mob = mob; this.rest = rest; } }
Listing 41.7. An extended example showing the properties of
the GraphicsEngine
class. Save this code in a file called Game.java.
import java.awt.*; import java.applet.Applet; import java.net.URL; public class Game extends Applet implements Runnable { Thread kicker; GraphicsEngine engine; MOB picture1, picture2, picture3, picture4, picture5, picture6; public void init() { try { engine = new GraphicsEngine(this); engine.background = getImage(new URL(getDocumentBase(), "background.jpg")); Image image1 = getImage(new URL(getDocumentBase(), "one.gif")); picture1 = new MOB(image1); picture2 = new MOB(image1); picture3 = new MOB(image1); picture4 = new MOB(image1); picture5 = new MOB(image1); picture6 = new MOB(image1); picture1.priority = 5; picture2.priority = 1; picture3.priority = 4; picture4.priority = 2; picture5.priority = 3; picture6.priority = 6; engine.AddMOB(picture1); engine.AddMOB(picture2); engine.AddMOB(picture3); engine.AddMOB(picture4); engine.AddMOB(picture5); engine.AddMOB(picture6); } catch (java.net.MalformedURLException e) { System.out.println("Error while loading pictures..."); e.printStackTrace(); } } public void start() { if (kicker == null) { kicker = new Thread(this); } kicker.start(); } public void run() { requestFocus(); while (true) { picture6.x = (picture6.x+3)%size().width; int tmp_y = (picture6.x % 40 - 20)/3; picture6.y = size().height/2 - tmp_y*tmp_y; repaint(); try { kicker.sleep(50); } catch (InterruptedException e) { } } } public void stop() { if (kicker != null && kicker.isAlive()) { kicker.stop(); } } public void update(Graphics g) { paint(g); } public void paint(Graphics g) { engine.paint(g, this); } public boolean mouseMove (Event evt, int mx, int my) { picture5.x = picture4.x-10; picture5.y = picture4.y-10; picture4.x = picture3.x-10; picture4.y = picture3.y-10; picture3.x = picture2.x-10; picture3.y = picture2.y-10; picture2.x = picture1.x-10; picture2.y = picture1.y-10; picture1.x = mx; picture1.y = my; return true; } public boolean keyDown (Event evt, int key) { switch (key) { case 'a': picture6.visible = !picture6.visible; break; case 's': if (picture1.priority==5) { picture1.priority=7; } else { picture1.priority=5; } break; } return true; } }
Two big features also implemented in the improved code are double buffering and the addition of a background image. This is accomplished entirely in GraphicsEngine (as shown in List-ing 41.6). Notice the changes in the constructor for GraphicsEngine. The graphics engine now creates an image so that it can do off-screen processing before it's ready to display the final image. The off-screen image is named buffer, and the Graphics context that draws into that image is named pad.
Now take a look at the changes to the paint() method in GraphicsEngine. Notice that, until the end, all the drawing is done into the Graphics context pad instead of the Graphics context g. The background is drawn into pad at the beginning of the paint() method and then the movable objects are drawn into pad after they have been sorted. Once everything is drawn into pad, the image buffer contains exactly what we want the screen to look like; so we draw buffer to g, which causes it to be displayed on the screen.
Another feature that was added to the extended version of the movable objects was the capability to make your movable objects disappear when they aren't wanted. This was accomplished by giving MOB a flag called visible. Take a look at the end of GraphicsEngine.paint() method in Listing 41.6 to see how this works. This feature would come in handy if you had an object that you wanted to show only part of the time. For example, you could make a bullet as a movable object. Before the bullet is fired, it is in a gun and should not be visible, so you set visible to false and the bullet isn't shown. Once the gun is fired, the bullet can be seen, so you set visible to true and the bullet is shown. Run the Game applet and press the A key a few times. As you can see from the keyDown() method, pressing the A key toggles the visible flag of the bouncing object between true and false.
By no means do the features shown in Listings 41.5, 41.6, and 41.7 exhaust the possibilities of what can be done with the structure of movable objects. Several additional features can easily be added, such as a centering feature for movable objects so that they are placed on the screen based on their center rather than their edge, an animation feature so that a movable object could step through several images instead of just displaying one, the addition of velocity and acceleration parameters, or even a collision-detection method that would allow you to tell when two movable objects have hit each other. Feel free to extend the code as needed to accommodate your needs.
We haven't actually written a game yet, but we have laid the foundation for writing games. You now have objects you can move around the screen simply by changing their coordinates. These tools have been the building blocks for games since the beginning of graphics-based computer games. Use your imagination and experiment. If you need more help extending the concepts described here concerning the creation of games with movable objects and their associated graphics engines, pick up a book devoted strictly to game programming. Tricks of the Game-Programming Gurus (Sams Publishing) and Teach Yourself Internet Game Programming with Java (Sams Publishing) are good examples.
At the end of this chapter, you will find the source code for and a short discussion of a very simple skiing game that was written using the GraphicsEngine built in the first part of this chapter. The skiing game and the source code are also available on the CD-ROM that accompanies this book. Studying the code for the skiing game and making small experimental changes to the code should help you understand how to use the building blocks developed in this chapter to create full-blown games.
We've spent all this time learning how to do the graphics for
a game in Java, but what about sounds? Sound in Java is very sparse.
The Java development team worked hard on the first release of
Java, but they unfortunately didn't have time to incorporate a
lot of sound support.
Note |
Although it's possible to develop much better sound control using the undocumented sun.audio.* classes, doing so is generally a bad idea for several reasons. First of all, the sun.audio.* classes are not part of the core Java API, so there is no guarantee that they will always be there on every implementation of the virtual machine. Second, JavaSoft is currently working on adding better audio support to the Java core classes, so you won't have to worry about the lack of functionality in the future. |
Check out java.applet.AudioClip in Java release 1.0 to discover the full extent of sound use. There are only three methods: loop(), play(), and stop(). This simple interface makes life somewhat easier. Use Applet.getAudioClip() to load an AudioClip in the AU format and you have two choices: Use the play() method to play it at specific times, or use the loop() method to play it continuously. The applications for each are obvious. Use the play() method for something that's going to happen once in a while, such as the firing of a gun; use the loop() method for something that should be heard all the time, such as background music or the hum of a car engine.
When thinking about the design of your game, there are some Java-specific design issues you must consider. One of Java's most appealing characteristics is that it can be downloaded through the Web and run inside a browser. This networking aspect brings several new considerations into play. Java is also meant to be a cross-platform language, which has important ramifications in the design of the user interface and for games that rely heavily on timing.
When picking a user interface, there are several things you should keep in mind. Above all, remember that your applet should be able to work on all platforms because Java is a cross-platform language. If you choose to use the mouse as your input device, keep in mind that regardless of how many buttons your mouse has, a Java mouse has only one button. Although Java can read from any button on a mouse, it considers all buttons to be the same button. The Java development team made the design choice to have only one button so that Macintosh users wouldn't get the short end of the stick.
If you use the keyboard as your input device, it is even more critical for you to remember that although the underlying platforms might be vastly different, Java is platform independent. This becomes a problem because the different machines that Java can run on may interpret keystrokes differently when more than one key is held down at once. For example, you may think it worthwhile to throw a supermove() method in your game that knocks an opponent off the screen, activated by holding down four secret keys at the same time. However, doing this might destroy the platform independence of your program because some platforms may not be able to handle four keystrokes at once. The best approach is to design a user interface that doesn't call into question whether it is truly cross-platform. Try to get by with only one key at a time, and stay away from control and function keys in general because they can be interpreted as browser commands by different browsers in which your applet runs.
As with any programming language, Java has its advantages and its disadvantages. It's good to know both so that you can exploit the advantages and steer clear of the disadvantages. Several performance issues arise when you are dealing with game design in Java-some of which are a product of the inherent design of Java and some of which are a product of the environment in which Java programs normally run.
One of the main features of Java is that it can be downloaded and run across the Net. Because automatically downloading the Java program you want to run is so central to the Java software model, the limitations imposed by using the Net to get your Java bear some investigation. First, keep in mind that most people with a network connection aren't on the fastest lines in the world. Although you may be ready to develop the coolest animation ever for a Java game, remember that nobody will want to see it if it takes forever to download. It is a good idea to avoid extra frills when they are going to be costly in terms of downloading time.
One trick you can use to get around a lengthy download time is
to download everything you can in the background. For example,
you can send level one of your game for downloading, start the
game, and while the user plays level one, levels two and up are
sent for downloading in a background thread. This task is simplified
considerably with the java.awt.MediaTracker.
To use the MediaTracker class,
simply add all your images to a MediaTracker
with the addImage() method
and then call checkAll()
with true as an argument.
Note |
According to the JavaSoft Web page at http://java.sun.com, Java 1.1 will include the ability to store all your images, classes, and other files in a JAR file. A JAR file is a new type of Java ARchive file, created (among other reasons) to speed up network transfers by reducing the number of connections. For the time being, some browsers, such as Netscape 3.0, allow you to store all your classes in one ZIP file. |
Opening a network connection can take a significant amount of time. If you have 30 or 40 pictures to send for downloading, the time this takes can quickly add up. One trick that can help decrease the number of network connections you have to open is to combine several smaller pictures into one big picture. You can use a paint program or an image-editing program to create a large image that is made up of your smaller images placed side by side. You then send for downloading only the large image. This approach decreases the number of network connections you need to open and can also decrease the total number of bytes contained in the image data. Depending on the type of compression used, if the smaller images that make up your larger image are similar, you will probably achieve better compression by combining them into one picture. Once the larger picture has been loaded from across the network, the smaller pictures can be extracted using the java.awt.image.CropImageFilter class to crop the image for each of the original smaller images.
Another thing you should keep in mind with applets is timing. Java is remarkably fast for an interpreted language, but graphics handling usually leaves something to be desired when it comes to rendering speed. Your applet probably will be rendered inside a browser, which slows it down even more. If you are developing your applets on a state-of-the-art workstation, keep in mind that a large number of people will be running Java inside a Web browser on much slower PCs. When your applets are graphics intensive, it's always a good idea to test them on slower machines to make sure that the performance is acceptable. If you find that an unacceptable drop in performance occurs when you switch to a slower platform, try shrinking the Component that your graphics engine draws into. You may also want to try shrinking the images used inside your movable objects because the difference in rendering time is most likely the cause of the drop in performance.
Another thing to watch out for is poor threading. A top-of-the-line workstation may allow you to push your threads to the limit, but on a slow PC, computation time is often far too precious. Improperly handled threading can lead to some bewildering results. In the run() method in Listing 41.7, notice that we tell the applet's thread to sleep for 50 milliseconds. Try taking this line out and seeing what happens. If you're using the applet viewer or a browser, it will probably lock up or at least appear to respond very slowly to mouse clicks and keystrokes. This happens because the applet's thread, kicker, eats up all the computation time and there's not much time left over for the painting thread or the user input thread. Threads can be extremely useful, but you have to make sure that they are put to sleep once in a while to give other threads a chance to run.
Fortunately, there is hope on the horizon concerning the relatively slow execution speed of Java. Just-in-time compilers, which greatly enhance the performance of Java, are starting to appear. Just-in-time compilers compile Java bytecode into native machine code on the fly so that Java programs can be run almost as fast as compiled languages such as C and C++.
The code in Listing 41.8 shows a simple example of a game that has been built using the GraphicsEngine developed in the first part of this chapter. The game is also provided in the file Ski.java on the CD-ROM that accompanies this book, so you don't have to bother typing it in.
Listing 41.8. A very simple game that shows how to use the
GraphicsEngine
to create games. Save this code in a file called Ski.java.
import java.awt.*; import java.applet.Applet; import java.net.URL; public class Ski extends Applet implements Runnable { Thread kicker; GraphicsEngine engine; MOB tree1, tree2, tree3, player; int screen_height = 1, screen_width = 1, tree_height = 1, tree_width = 1; int player_width = 1, player_height = 1; int step_amount = 10; public void init() { try { engine = new GraphicsEngine(this); Image snow = getImage(new URL(getDocumentBase(), "snow.jpg")); engine.background = snow; Image tree = getImage(new URL(getDocumentBase(), "tree.gif")); MediaTracker tracker = new MediaTracker(this); tracker.addImage(tree, 0); tracker.addImage(snow, 0); tree1 = new MOB(tree); tree2 = new MOB(tree); tree3 = new MOB(tree); Image person = getImage(new URL(getDocumentBase(), "player.gif")); tracker.addImage(person, 0); tracker.waitForID(0); tree_height = tree.getHeight(this); tree_width = tree.getWidth(this); player_width = person.getWidth(this); player_height = person.getHeight(this); player = new MOB(person); screen_height = size().height; screen_width = size().width; player.y = screen_height/2; player.x = screen_width/2; tree1.x = randomX(); tree1.y = 0; tree2.x = randomX(); tree2.y = screen_height/3; tree3.x = randomX(); tree3.y = (screen_height*2)/3; player.priority = player.y-(tree_height-player_height); tree1.priority = tree1.y; tree2.priority = tree2.y; tree3.priority = tree3.y; engine.AddMOB(player); engine.AddMOB(tree1); engine.AddMOB(tree2); engine.AddMOB(tree3); } catch (Exception e) { System.out.println("Error while loading pictures..."); e.printStackTrace(); } } public void start() { if (kicker == null) { kicker = new Thread(this); } kicker.start(); } public void run() { while (true) { increment(tree1); increment(tree2); increment(tree3); if (hit(tree1) || hit(tree2) || hit(tree3)) { step_amount = 0; } else step_amount++; repaint(); try { kicker.sleep(100); } catch (InterruptedException e) { } } } public void increment(MOB m) { m.y -= step_amount; if (m.y < -tree_height) { m.y = m.y+screen_height+2*tree_height; m.x = randomX(); } m.priority = m.y; } public boolean hit(MOB m) { return (m.y < player.priority+tree_height/2 && m.y >= player.priority && m.x > player.x-tree_width && m.x < player.x+player_width); } public int randomX() { return (int) (Math.random()*screen_width); } public void stop() { if (kicker != null && kicker.isAlive()) { kicker.stop(); kicker = null; } } public void update(Graphics g) { paint(g); } public void paint(Graphics g) { engine.paint(g, this); } public boolean mouseMove (Event evt, int mx, int my) { player.x = mx - player_width/2; return true; } }
To play the game, simply use a Java-enabled browser or the JDK applet viewer to view the file called ski.html. You should see something like Figure 41.4. Once the game is properly running, you see a skier and three trees on the screen.
Figure 41.4: A smiple skiing game applet that uses the GraphicsEngine.
Use the mouse to control the skier. Moving your mouse left and right without holding the mouse button down causes the skier to move left and right. You cannot move the player up and down.
The object of the game is to avoid hitting the trees. Notice that as long as you avoid hitting the trees, you continue to accelerate. Hence, the longer you avoid hitting the trees, the faster you go. Hitting a tree brings the skier to a halt, and you have to start accelerating from a standstill again.
The skiing game basically grew out of the GraphicsEngine example developed earlier in this chapter. Support for things that weren't needed (like a bouncing head) was removed, and support for new things (like moving trees) was added. Tweaking code like this is an excellent way to learn a language and to learn new programming methods. Don't be afraid to tweak the code yourself-experimenting with existing code is a great way to build up some experience before attempting to write something from scratch.
As its name implies, the init() method is in charge of initializing the applet. It is called once and only once at the beginning of the applet's life cycle. In the case of the skiing game, the init() method takes care of creating the graphics engine to handle all the objects (loading the images for the background, the trees, and the skier; creating new MOBs from these images; adding these MOBs to the graphics engine; and gathering information about the size of the images).
The start() method is also part of the initialization process, but unlike the init() method, it may be called more than once. The idea behind the start() method is that it is called every time the Web page it's in is viewed; the start() method can be called several times if the user goes back to the same Web page several times in the same session.
In this case, the start() method is used just to start a thread. The thread is used to move the trees. The thread is stopped when the user leaves the Web page because the stop() method (which is called every time the Web page is left) contains code that stops the thread.
The movement of the trees is accomplished inside the run() method. Inside the run()method, the increment() method is called for each of the trees, causing them to move up the screen. Whenever a tree has gone so far up the screen that it is no longer visible, it is placed at the bottom of the screen at a random position, creating the illusion that there are an infinite number of trees when in fact there are only three.
The run() method also checks to see whether or not the skier has hit a tree. If the skier has hit a tree, the variable step_amount is set to zero. If the skier has not hit a tree, the variable step_amount is increased by 1. The variable step_amount is used to decide how far to move the tree up the screen each time. If the skier has hit a tree, the trees don't move at all that particular time (because the skier has stopped). If the skier hasn't hit a tree, the trees move up by step_amount number of pixels. step_amount is increased by 1 each time the skier passes (that is, does not hit) a tree. Because step_amount increases, this makes the trees move up the screen faster, creating the illusion that the skier is accelerating.
Finally, the movement of the skier is controlled completely by the mouse. Because the mouseMove() method is called every time the mouse is moved, all we have to do is override the mouseMove() method so that whenever the mouse moves, we move the player.
In this chapter, we developed a basic graphics engine with Java that can be used for game creation. This graphics engine incorporated movable objects with prioritization and visibility settings, double buffering, and a background image. We also went over a very simple example of a game that was built using the tools presented in the chapter. However, the focus was on the construction of the tools rather than the construction of the sample game because the tools can be expanded to produce a multitude of games.
This chapter also touched on issues you should keep in mind when developing games with Java. It is important to remember that Java is a cross-platform language and therefore runs on different platforms. When you develop your games, you should be aware that people will want to run them on machines that may not have the same capabilities as your machine.