TOC
BACK
FORWARD
HOME

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.

TOCBACKFORWARDHOME


©Copyright, Macmillan Computer Publishing. All rights reserved.