Java 1.1 Unleashed
- 50 -
Java Game Programming
by Tim Macinta
IN THIS CHAPTER
- Selecting an Appropriate Version of Java
- Graphics: Creating a Graphics Engine
- Improving the Bare-Bones Engine
- Sounds
- Java-Specific Game Design Issues
- A Simple Example: The Skiing Game
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 you do not use them properly. This chapter explains how
to use the advantages of Java to write games and how to avoid the pitfalls that accompany
the power.
Selecting an Appropriate Version of Java
Before you set out to write a game in Java, you must first decide which version
of Java you are going to use. Should you use the most recent version of Java (currently
1.1) or should you use an older version so that more people can use your game? This
decision depends largely on how you plan to distribute your game. If you plan to
distribute your game as an application, the decision is pretty simple: use the latest
version of Java because you can take advantages of the latest improvements; if the
user doesn't have the latest version of Java, you can just ship it with your application.
However, if you plan to distribute your game as an applet, the choice is a little
more complicated. Should you use the latest version of Java so that you have a much
richer set of class libraries to work with, or should you use the 1.0 version of
Java so that you can reach the largest audience? For now, the solution is to use
the 1.0 version of Java. Currently, Netscape Navigator 4.0 and HotJava are the only
Web browsers that support the Java 1.1 API--and most users are not yet using these
browsers. In the future, the Java 1.1 API will be widespread enough that it will
be reasonable to write a game using the 1.1 API. For now, however, it's best to play
it safe and use the Java 1.0 API so that everybody who has a Java-enabled browser
can use the game.
This chapter covers what you need to know to write games in both the Java 1.0
and the 1.1 API. Most of what you need to know to write games in Java does not depend
on whether or not you are using the Java 1.0 or the 1.1 API. The main issue between
the different APIs is the issue of handling user input. This difference is really
only of secondary importance because the bulk of the code in this chapter is the
same regardless of what API you use it for.
Consequently, all the examples in this chapter are first shown with code that
conforms to the Java 1.0 API. If there is a different way of doing things with the
Java 1.1 API, an alternative that conforms to the Java 1.1 API is also provided.
Don't forget that the Java 1.1 API is backward compatible with the Java 1.0 API,
so the 1.0 examples work regardless of which version of Java you use (the Java 1.1
API example just shows a better way of doing things in the newer version of Java).
The Java 1.1 versions of the examples are in the Chap50/jdk1_1 directory
on the CD-ROM that accompanies this book; the Java 1.0 versions of the examples are
in the Chap50 directory.
Graphics: Creating a Graphics Engine
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.
Movable Object Blocks in Java
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
50.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 in Listing 50.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 50.1 through 50.4, you can find finished versions of all the
code in the CHAP50 directory on the CD-ROM that accompanies this book. The
files that correspond to Listings 50.1 through 50.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
titled example.html with a Java-enabled browser.
Listing 50.1. MOB.java: The code to create a bare-bones
movable object block (MOB).
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 50.1, the constructor for our movable object block (MOB)
takes an Image and stores it away to be drawn when needed. After we instantiate
a MOB (that is, after we call 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 will be drawn, but so will their backgrounds. Chances are, you'll
want to have a background for the entire game and 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
you think this seems like a laborious process, 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, however, 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 which pixels are transparent
and which 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 that
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, distributed by Home Pages, Inc.,
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.
Construction of a Graphics Engine
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 the GraphicsEngine class. Listing 50.2 shows the bare
bones of a graphics engine--the minimum you need to handle multiple movable objects.
Even this engine leaves out several things 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 50.2. GraphicsEngine.java: A bare-bone graphics
engine that tracks your movable objects.
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;
}
}
Introducing Linked Lists
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
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 suites our purposes well because a linked list is a structure 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
50.1; then the technical explanation should make more sense.
To understand what a linked list is, think of a train as an example: 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 50.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 50.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 can just override AddMOB() so that it stores all the objects
in a sorted order to begin with.
Painting Images from a List
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
50.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
responsible for redrawing an image when it changes or moves. Don't worry about where
you 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 50.1
provides a graphical explanation of this process.
Figure 50.1.
A graphical representation of a Chain.
Installing the Graphics Engine
The graphics engine in Listing 50.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 50.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 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 50.3, it would be a good idea
to type and compile Listings 50.1 through 50.3 so that you can get an idea of what
the code does. Save each listing with the filename specified in each listing's heading
and compile the code by using the javac command on each of those files.
In addition to compiling the code in Listings 50.1 through 50.3, you must also
create the HTML file shown in Listing 50.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 must 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 50.3 that specifies the name of the
picture being loaded.
If you want to use the JDK 1.1 alternative version of this applet, simply save
the code in Listing 50.5 to a file named Game.java instead of using the
code in Listing 50.3. Of course, if you do use the JDK 1.1 alternative, you must
make sure that your browser or applet viewer is Java 1.1 compliant.
Listing 50.3. Game.java: This sample applet illustrates
the GraphicsEngine class in Java 1.0.
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 50.4. This code must be in an HTML file if you
are 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>
Listing 50.5. Game.java: This sample applet illustrates
the GraphicsEngine class in Java 1.1.
import java.awt.event.*;
import java.awt.*;
import java.applet.Applet;
import java.net.URL;
public class Game extends Applet implements MouseMotionListener {
GraphicsEngine engine;
MOB picture1;
public void init() {
addMouseMotionListener(this);
try {
engine = new GraphicsEngine(this);
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 void mouseDragged (MouseEvent evt) {
}
public void mouseMoved (MouseEvent evt) {
picture1.x = evt.getX();
picture1.y = evt.getY();
repaint();
}
}
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 50.2.
Figure 50.2.
This is what the example of the bare-bones GraphicsEngine should look
like.
Let's go over how the code that links the GraphicsEngine to 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 often, 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 50.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 (or the mouseMoved()
method if you used the Java 1.1-compliant example in Listing 50.5). 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; these 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 (if you're using the Java 1.1 example, nothing is returned
because nothing has to be returned).
-
NOTE: The AWT event model underwent a
major overhaul in Java 1.1; one of the things that changed was that methods that
handle events don't return anything anymore. The AWT no longer has to know whether
or not the mouseMove() method was taken care of because it is now implicitly
taken care of when you register an observer with a component.
Improving the Bare-Bones Engine
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 you consider
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 you need is some sort of prioritization scheme. The improved version of the
graphics engine will implement 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 50.6 shows the changes that have to be made to the MOB class
to implement prioritization. Listing 50.7 shows the changes that have to be made
to the GraphicsEngine class, and List- ing 50.8 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.
Once again, a version of this applet that conforms to the JDK 1.1 API is provided.
If you prefer to use a Java 1.1 example, simply use the code in Listing 50.9 for
the changes that have to be made to Game instead of using the code in Listing
50.8.
-
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 0).
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 50.6, 50.7, and 50.8
(or 50.9 for the Java 1.1 version). 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
50.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.
Also notice that the bouncing object is always in front of the object you control
with your mouse. This is because the bouncing object was assigned a higher priority
than all the other objects. Now press the S key. The first object 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 (look at the Game.keyTyped() example if you
are using the Java 1.1 example). 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.
Figure 50.3.
What the final version of the GraphicsEngine example should look like.
Listing 50.6. MOB.java: The enhanced version of the MOB
class.
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 50.7. GraphicsEngine.java: The enhanced version
of the GraphicsEngine class.
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 50.8. Game.java: An extended example showing the
properties of the GraphicsEngine class in Java 1.0.
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;
}
}
Listing 50.9. Game.java: An extended example showing the
properties of the GraphicsEngine class in Java 1.1.
import java.awt.event.*;
import java.awt.*;
import java.applet.Applet;
import java.net.URL;
public class Game extends Applet implements Runnable,
MouseMotionListener, KeyListener {
Thread kicker;
GraphicsEngine engine;
MOB picture1, picture2, picture3, picture4, picture5, picture6;
public void init() {
addMouseMotionListener(this);
addKeyListener(this);
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/3 + 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 void mouseDragged (MouseEvent evt) {
}
public void mouseMoved (MouseEvent evt) {
int mx = evt.getX();
int my = evt.getY();
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;
}
public void keyPressed (KeyEvent evt) {
}
public void keyReleased (KeyEvent evt) {
}
public void keyTyped (KeyEvent evt) {
switch (evt.getKeyChar()) {
case 'a':
picture6.visible = !picture6.visible;
break;
case 's':
if (picture1.priority==5) {
picture1.priority=7;
}
else {
picture1.priority=5;
}
break;
}
}
}
Double Buffering
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 50.7). 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.
Invisibility and Other Possible Extensions
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 50.7 to see
how this works. This feature comes in handy when you have an object you want to show
only part of the time. For example, you can make a bullet 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 (or the keyTyped() method if you are using the
Java 1.1 example), 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 50.6, 50.7, and 50.8 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 can step through several images
instead of just displaying one, the addition of velocity and acceleration parameters,
or even a collision-detection method that allows you to tell when two movable objects
have hit each other. Feel free to extend the code to accommodate your needs.
Using the Graphics Engine to Develop Games
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 (published by
Sams.net Publishing) and Teach Yourself Internet Game Programming with Java (also
published by Sams.net Publishing) are good titles.
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 this 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 we developed in this chapter to create full-blown
games.
Sounds
We've spent all this time learning how to do the graphics for a game in Java,
but what about sounds? Sound support in Java is very sparse. The Java development
team worked hard on the version 1.x releases of Java but 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 in 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 1.0 or Java 1.1 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.
Java-Specific Game Design Issues
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.
Picking a User Interface
When picking a user interface, you should keep several things 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 the different browsers in which your applet
runs.
Limiting Factors
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.
Downloading
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 download 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, download levels two and up 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 MediaTracker
with the addImage() method and then call checkAll() with true
as an argument.
NOTE: Java 1.1 includes 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. Another option for packaging everything together
is supplied by some browsers, such as Netscape 3.0; these browsers 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 have 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.
Execution Speed
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 50.8, 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++.
A Simple Example: The Skiing Game
The code in Listing 50.10 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 that you don't have to bother typing it in.
As usual, Listing 50.11 provides an alternative version of this game that uses
the features in the JDK 1.1. Use Listing 50.11 instead of Listing 50.10 if you want
to use the Java 1.1-specific version.
Listing 50.10. Ski.java: A very simple game that shows
how to use the GraphicsEngine to create games in Java 1.0.
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;
}
}
Listing 50.11. Ski.java: A very simple game that shows
how to use the GraphicsEngine to create games in Java 1.1.
import java.awt.event.*;
import java.awt.*;
import java.applet.Applet;
import java.net.URL;
public class Ski extends Applet implements Runnable, MouseMotionListener {
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() {
addMouseMotionListener(this);
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 void mouseDragged (MouseEvent evt) {
}
public void mouseMoved (MouseEvent evt) {
player.x = evt.getX() - player_width/2;
}
}
Playing the Game
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 50.4.
Once the game is properly running, you see a skier and three trees on the screen.
Figure 50.4.
A simple 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.
Understanding the Code
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.
Initializing Everything
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 can 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.
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.
Creating Movement
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. If you want to use the JDK 1.1 version, all you have to do is
override the mouseMoved() method.
Summary
In this chapter, we developed a basic Java graphics engine 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 example 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. 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 development machine.
|