All Categories :
Java
Chapter 41
Java Game Programming
by Tim Macinta
CONTENTS
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.
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 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.
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 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;
}
}
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 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.
|
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 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.
Installing the Graphics Engine
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;
}
}
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 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.
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 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.
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 (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.
Picking a User Interface
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.
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 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.
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 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;
}
}
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 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.
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 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.
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.
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.
Contact
reference@developer.com with questions or comments.
Copyright 1998
EarthWeb Inc., All rights reserved.
PLEASE READ THE ACCEPTABLE USAGE STATEMENT.
Copyright 1998 Macmillan Computer Publishing. All rights reserved.