Newsletter sign-up
View all newsletters

Enterprise Java Newsletter
Stay up to date on the latest tutorials and Java community news posted on JavaWorld

Sponsored Links

Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs

Java Fun and Games: Java visits the arcade

Develop arcade games with JGame

  • Print
  • Feedback

In the 1980s, people flocked to arcades to play Pacman, Space Invaders, Frogger, Donkey Kong, and other games. These classics are still fun to play: If you've never played one of these arcade games, or if you just want to take a trip down memory lane, check out Classic '80s Games in Resources.

Do you ever dream of developing a Java-based arcade game that is similar to the classics? If so, you can turn this dream into reality with JGame, a Java game engine created by Boris van Schooten. This article introduces you to JGame, its features, directories and files, and example games. We also explore JGame's architecture—its engine, game objects, and timers.

Note: You can now build and run the applets presented in Java Fun and Games using DevSquare, an online development tool. Read the user guide available in Resources to get started.

Introducing JGame

JGame is a small 2D Java game engine whose high-level framework—based on sprites with automatic animation and collision detection, and a tile-based background with easy sprite-tile interaction facilities—simplifies classic-style arcade game development. Games run as applications or applets, scaling to any window size—they even run full-screen. (To learn more about sprites and tiles, visit Wikipedia.)

This article focuses on JGame version 1.2. Although this version has a few deficiencies (lack of sound support and the inability to create games with scrolling backgrounds are two examples—these problems will most likely be addressed in a future version), version 1.2 offers several features that facilitate arcade game development and deployment:

  • Built-in animated "sprite" engine with easy animation definition
  • Tile-based background handling with decorative backdrop
  • Automatic collision detection with sprites and background tiles, and easy background tile interaction
  • Ability to load sprites, tiles, and color fonts directly from sprite sheets
  • Images and animations defined in a text file
  • A state machine model for in-game sequences
  • A standard game state machine and some standard game objects
  • Debugging facilities, which include visualizing bounding boxes and printing debug messages next to an object on the play field
  • Optimized and accelerated graphics for various displays, without requiring extra packages; works well on remote X11 displays
  • Ability to program game at one window size; game can be scaled to any desired window size (even full-screen) when run
  • Tested on various platforms; can be easily run as an applet or as an application (and from a jar file)

Engine installation

You must install JGame before you can work with this software. Surf on over to JGame's Webpage (see Resources for a link) and select a distribution file—jgame-20061023.tar.gz or jgame-20061023.zip, for version 1.2. After downloading and unarchiving the distribution file, I recommend moving the jgame home directory to your root directory (for convenience).

JGame comes with precompiled Java 1.4 classfiles. If you plan to use JGame with Java 1.4 or higher, there is nothing left to do—JGame is installed. However, if you plan to use JGame with Java 1.2 and/or 1.3, you must recompile JGame according to the instructions found in a file named makefile—one of the various directories and files located in the jgame home directory, a list of which follows below:

  • examples contains source files and precompiled Java 1.4 classfiles for this package
  • gfx contains GIF, PCX, and PNG image files for the example games bundled with JGame
  • html contains HTML and related files for running the example games as applets
  • javadoc contains the Java documentation for JGame's two packages of classes
  • jgame contains source files and precompiled Java 1.4 classfiles for this package
  • CHANGES logs various changes (including bug fixes) applied to each JGame version
  • LICENSE identifies the JGame license, copyright, and warranty information
  • make.bat compiles source files in directories examples and jgame
  • make-docs.bat creates the packages documentation located in directory javadoc
  • make-jar.bat creates a jar file with all relevant JGame classfiles and resource files
  • Makefile shows how to compile source code, create a Java 1.2 version of JGame, and more
  • makepkg.sh presents a Unix shell script for packaging JGame into a distribution file
  • manifest identifies the main class for the jar file
  • MANUAL provides insight into game programming with JGame
  • README provides a starting point for exploring JGame

Example games

JGame includes 11 example games that demonstrate JGame-based game development: NebulaAlpha, Insecticide, ChainReaction, SpaceRun, SpaceRun II, Munchies, WaterWorld, CavernsOfFire, MatrixMiner, PubMan, and DungeonsOfHack. Because these games exist in the examples package, you must include the examples. prefix when running the game as an application or as an applet.

You can run any example game as an application by making sure that jgame is the current directory, or by adding jgame's path to the CLASSPATH environment variable. For example, to run PubMan (a Pacman clone—you are chased by mugs of beer instead of ghosts) as an application, specify java examples.PubMan. By default, this game runs full-screen.

Instead of running full-screen, you can force most example games to display their output in a window, by specifying integer arguments on the command line. For example, to run PubMan in a 300-horizontal-by-300-vertical-pixel window, invoke java examples.PubMan 300 300. Figure 1 shows the resulting window.

Figure 1. PubMan's output has been scaled to 300 horizontal by 300 vertical pixels. The green region on the right is a scaling artifact.

You can also run an example game as an applet. The html directory contains several HTML files for each example; each HTML file runs the example at a specific window size (including full-screen). For example, this directory's applet-pubman-320x240.html file employs the following <applet> tag to run PubMan at a window size of 320 horizontal by 240 vertical pixels:

<applet code="examples.PubMan" width="320" height="240" codebase="..">
PubMan applet
</applet>

JGame architecture

JGame is composed of classes located in the jgame and examples packages. The classes in the jgame package implement an architecture in terms of an engine, game objects, and timers. The classes in the examples package specify standardized game objects, and implement the example games—the example game classes are not really part of the architecture.

Engine

The engine is responsible for starting, terminating, and managing a game. Management tasks include loading a game's images, maintaining a list of sprites (foreground game objects) and a matrix of equal-size tiles (background game images), animating the game as a sequence of frames, painting each frame, moving sprites, detecting collisions between sprites and other sprites or tiles, and responding to user input.

JGame implements its engine via jgame.JGEngine and support classes. Because JGEngine is abstract, it must be subclassed by a game. The subclass must provide a parameterless constructor to run the game as an applet—JGEngine subclasses java.applet.Applet. To run the game as an application, supply a public static void main(String [] args) method in the subclass:

 public class Game extends JGEngine
{
   public Game ()
   {
      // Initialize game as an applet.
   }

   public static void main (String [] args)
   {
      // Initialize game as an application.
   }

   // ...
}

The parameterless constructor must invoke JGEngine's public void initEngineApplet(int nrtilesx, int nrtilesy, int tilex, int tiley, Color fgcolor, Color bgcolor, Font msgfont) method to initialize the engine in applet mode. This method specifies the following seven parameters:

  • nrtilesx defines the number of horizontal tiles; 1 is the minimum value
  • nrtilesy defines the number of vertical tiles; 1 is the minimum value
  • tilex defines the tile width (in pixels); 1 is the minimum value
  • tiley defines the tile height (in pixels); 1 is the minimum value
  • fgcolor defines the foreground color for drawing text; a null reference selects default white
  • bgcolor defines the game's background color; a null reference selects default black
  • msgfont defines the font for drawing message text; a null reference selects default Helvetica

A game is played on a virtual play field, which is described by nrtilesx, nrtilesy, tilex, and tiley. The play field's width is nrtilesx * tilex pixels; the play field's height is nrtilesy * tiley pixels. JGame automatically scales virtual play field output to the current window size. The JGEngine methods below return play field values:

  • public int pfHeight() returns nrtilesy * tiley
  • public int pfTileHeight() returns tiley
  • public int pfTilesX() returns nrtilesx
  • public int pfTilesY() returns nrtilesy
  • public int pfTileWidth() returns tilex
  • public int pfWidth() returns nrtilesx * tilex

During game play, the game can change the foreground color, background color, and message font specified by parameters fgcolor, bgcolor, and msgfont. The game can also display a background image on the virtual play field, instead of displaying this field in the background color. The JGEngine methods below accomplish these tasks:

  • public void setBGColor(Color bgcolor) sets the background color, which is displayed in borders and behind transparent tiles if no background image is defined.
  • public void setBGImage(String bgimg) sets the image to display behind transparent tiles. Image size must be a multiple of the tile size. A null argument reverts to using the background color.
  • public void setFGColor(Color bgcolor) sets the foreground color, which is used for printing text and status messages. It is also the default color for painting.
  • public void setMsgFont(Font msgfont) sets the unscaled message font. It is used for displaying status messages. It is also the default font for painting.

The main() method must invoke JGEngine's public void initEngine(int nrtilesx, int nrtilesy, int tilex, int tiley, Color fgcolor, Color bgcolor, Font msgfont, int width, int height) method to initialize the engine in application mode. This method specifies an additional two parameters:

  • width defines the width (in pixels) of the game's output window; 0 indicates full screen
  • height defines the height (in pixels) of the game's output window; 0 indicates full screen

Before I reveal the source code to a game that runs as an applet, or as an application, there is one more item to consider: JGEngine specifies a public abstract void initGame() method that must be implemented by the subclass. This method is responsible for initializing variables, loading images, and performing other game-initialization tasks. Listing 1 implements a do-nothing version of this method.

Listing 1. Game.java

 // Game.java

import jgame.*;

public class Game extends JGEngine
{
   // Establish a virtual play field that is 100 pixels by 100 pixels. All
   // output in this play field is scaled to 800 by 600 pixels when the game
   // is run as an application. When the game is run as an applet, the output
   // is scaled to whatever window size is specified by the <applet> tag's
   // width and height parameters.

   final static int NTILESX = 1;
   final static int NTILESY = 1;
   final static int TILEW = 100;
   final static int TILEH = 100;

   public Game ()
   {
      initEngineApplet (NTILESX, NTILESY, TILEW, TILEH, null, null, null);
   }

   public Game (int width, int height)
   {
      initEngine (NTILESX, NTILESY, TILEW, TILEH, null, null, null, width,
                  height);
   }

   public void initGame ()
   {
   }

   public static void main (String [] args)
   {
      // Run the game at a window size of 800 by 600 pixels.

      new Game (800, 600);
   }
}

When Game runs as an applet, a Web browser invokes Game's parameterless constructor, which invokes initEngineApplet(). The browser next invokes JGEngine's overridden public void init() method. When Game runs as an application, main() indirectly invokes initEngine(), which internally invokes init() as its final step.

The init() method first determines if initEngineApplet() was previously invoked. If so, Applet's public int getWidth() and public int getHeight() methods are also invoked to return the window size from the width and height parameters of an HTML document's <applet> tag.

Moving on, init() creates and installs a jgame.JGCanvas instance (a package-private class) that manages sprites, processes mouse-related user input, and provides a drawing surface on which to render the game's output; registers a key listener with the JGCanvas instance to process keyboard-related user input originating from the canvas; and starts a thread to run the game.

This new thread first invokes the game's initGame() method, which gives a game an opportunity to perform game-specific initialization. One of these initialization tasks involves defining (and loading) the game's graphics. Invoke JGEngine's public void defineGraphics(String filename) method to accomplish this task:

 public void initGame ()
{
   // Load images described by the game.tbl file. By convention,
   // .tbl (table) is specified as this file's extension.

   defineGraphics ("game.tbl");
}

The defineGraphics() method takes a single argument, which is the name of a text file that defines the game's images, image maps (large images containing regularly spaced smaller images, typically used for fonts and sprites, also known as sprite sheets), and animations (sequences of images). Images may be obtained from image maps. Figure 2 reveals an image map.

Figure 2. The gfx/explo32.gif image map contains seven 32-by-32-pixel images

Each line in the text file defines a single image, image map, or animation. This line must contain three, four, five, six, eight, nine, or ten tab-separated tokens; otherwise, defineGraphics() ignores the line. This token count is used by defineGraphics() to invoke either a JGEngine or a JGCanvas method to define the image, image map, or animation. The counts are described more thoroughly below:

  • Three tokens: The first token identifies an animation, the second token identifies a sequence of images to animate (a semicolon separates each image or image map name—the image map entry specifies an index to the image), and the third token specifies the animation's speed (must be greater than 0). Images are animated in a forward order. The example pod pod1;pod3;pod4;pod2;pod5 0.3 defines a pod animation with five pod images and an animation speed of 0.3. The animation order is forward. See the ten-tokens example for pod1's definition.
  • Four tokens: The first three tokens are the same as described above, with the fourth token identifying animation order (true/forward, false/backward). The example enemy enemy1;enemy2;enemy3;enemy4 0.3 true defines an enemy animation with four enemy images, an animation speed of 0.3, and a forward animation order. Because true is the default, its specification is redundant.
  • Five tokens: The first token identifies a sprite or tile image, the second token is a 1-to-4-character tile identifier, the third token is a tile-collision identifier, the fourth token names the image's file, and the fifth token identifies an image operator: x flips the image horizontally, y flips the image vertically, r rotates the image 90 degrees, u rotates the image 180 degrees, and l rotates the image 270 degrees. The tile identifier and tile-collision identifier do not apply to sprite images. The example wall0 #0 1 gfx/iceblock-16.gif - defines a wall0 tile image with #0 as the tile identifier, 1 as the tile-collision identifier, gfx/iceblock-16.gif as the image's file, and no image operator.
  • Six tokens: The first three tokens are the same as those described in the five-token count. The fourth token names an image map, the fifth token is a zero-based index into the image map, and the sixth token identifies an image operator—see the previous list of image operators. Specify any other character, such as -, to leave the image alone. The example fireball0 p 0 spr_map 5 - defines a fireball0 sprite image with p as a placeholder tile identifier, 0 as a placeholder tile-collision identifier, sprmap as the sprite's image map, 5 as the sprite image's index into this map, and no image operator.
  • Eight tokens: The first token identifies an image map, the second token identifies the file that contains the map, the third and fourth tokens identify the horizontal and vertical offsets (respectively) to the first image, the fifth and sixth tokens identify each image's width and height (respectively), and the seventh and eighth tokens identify the horizontal and vertical pixel space (respectively) between the map's images. The example pod_map gfx/podblue14x14.gif 0 0 14 14 0 0 defines a pod_map image map with gfx/podblue14x14.gif as the map file, 0 as the horizontal and vertical pixel offsets to the first image, 14 as each image's width and height (in pixels), and 0 as the pixel spacing between the map's images.
  • Nine tokens: The first token identifies a sprite or tile image, the second token is a 1-to-4-character tile identifier, the third token is a tile-collision identifier, the fourth token names the image's file, and the fifth token identifies an image operator—see the previous list of image operators. The final four tokens identify the dimensions of a bounding box: upper-left corner (x, y), width, and height. The tile identifier and tile-collision identifier do not apply to sprite images. The example ship p 0 gfx/ship32x16.gif - 4 4 22 10 defines a ship sprite image with p as a placeholder tile identifier, 0 as a placeholder tile collision identifier, gfx/ship32x16.gif as the sprite image's file, no image operator, and bounding box dimensions 4 4 22 10.
  • Ten tokens: The first three tokens match those described in the nine-token count. The fourth token identifies an image map, the fifth token is a zero-based map index, and the sixth token identifies an image operator—see the previous list of image operators. The final four tokens identify the dimensions of a bounding box. The example pod1 p 0 pod_map 0 - 1 0 12 14 defines a pod1 sprite image with p as a placeholder tile identifier, 0 as a placeholder tile-collision identifier, pod_map as the sprite's image map, no image operator, and bounding box dimensions 1 0 12 14. See the eight-tokens example for pod_map's definition.

Depending on the number of images to load and their sizes, defineGraphics() can take a lot of time to complete. For this reason, defineGraphics() works with JGCanvas to present an initialization screen with a wait message and a progress bar that reflects the progress of the images being loaded.

A splash image can be shown on the initialization screen by defining an image named splash_image. As soon as this image has been defined, it is displayed above the progress bar. Because it does not make sense to display the splash image after images have been loaded, the splash_image definition should appear at the beginning of the text file.

To give you some guidance on defineGraphics() and splash images, I've revised my Game program to invoke defineGraphics() from its initGame() method. The defineGraphics() method loads a splash image and a background image from a game.tbl file, whose contents appear in Listing 2.

Listing 2. game.tbl

 splash_image - 0 images/gametitle.gif -
background - 0 images/marsbg.jpg -

I don't regard splash and background images as tile images, which is why I've chosen - and 0 as tile identifier and tile-collision identifier placeholders—I don't regard splash and background images as sprite images, either. In the listing, I separate each definition's tokens with single-space characters. However, I employ single-tab characters as token separators in game.tbl.

Note
The defineGraphics() method employs a java.util.StringTokenizer to break each definition line into tokens: StringTokenizer toker = new StringTokenizer (line, "\t");. The problem with this tokenizer is that it only recognizes tab characters. It would be more helpful if the tokenizer recognized spaces (lines containing embedded spaces are ignored). You can force the tokenizer to recognize spaces in addition to tabs by adding a single space character after \t in the previous statement, which yieldsStringTokenizer toker = new StringTokenizer (line, "\t ");.

Although the revised Game program's initGame() method explicitly invokes setBGImage() to display the background image (after defineGraphics() finishes), it does not explicitly display the splash image—the splash image is implicitly displayed through a partnership between JGCanvas and JGEngine. Listing 3 presents the revised source code.

Listing 3. Game.java (revised)

 // Game.java

import jgame.*;

public class Game extends JGEngine
{
   final static int NTILESX = 1;
   final static int NTILESY = 1;
   final static int TILEW = 800;
   final static int TILEH = 600;

   public Game ()
   {
      initEngineApplet (NTILESX, NTILESY, TILEW, TILEH, null, null, null);
   }

   public Game (int width, int height)
   {
      initEngine (NTILESX, NTILESY, TILEW, TILEH, null, null, null, width,
                  height);
   }

   public void initGame ()
   {
      // Load images described by the game.tbl file. By convention,
      // .tbl (table) is specified as this file's extension.

      defineGraphics ("game.tbl");

      // The image size, which is 800 by 600, must be a multiple of the tile
      // size, which is 800 by 600.

      setBGImage ("background");
   }

   public static void main (String [] args)
   {
      // Run the game at a window size of 800 by 600 pixels.

      new Game (800, 600);
   }
}

By specifying a single tile with a width and height equal to the window's width and height, the source code establishes a virtual play field equal to the window size. Neither the splash image nor the background image is distorted due to scaling. Another reason for equating play field size to window size: the 800-by-600-pixel background image must be a multiple of the tile size. Figure 3 presents the splash image.

Figure 3. The WAR ON MARS splash image appears on the initialization screen. Click on thumbnail to view full-sized image.

This article's code archive, which can be downloaded from Resources, contains this program's source code and images. After compiling the source code and running the program, use image operators to modify the splash and background images. For example, r in splash_image - 0 images/gametitle.gif r rotates the splash image 90 degrees. And y in background - 0 images/marsbg.jpg y flips the background image vertically.

Caution
Because the background image is not square, you cannot rotate this image such that its width and height change. For example, if you attempt to rotate the background image 90 degrees (via image operator r), so that the width becomes 600 and the height becomes 800, the engine throws exceptions.

After initGame() finishes, the new thread enters an animation loop. Each loop iteration creates and renders a single animation frame (frame for short), which can be thought of as the next step in the game's evolution. This loop continues until the user terminates the game by pressing the Shift+Esc key-combination.

Note
The JGEngine class's documentation states that you must press Shift+Esc to terminate a game. Because this key-combination does not work on my platform, I've replaced the public void keyReleased(KeyEvent e) and public void keyTyped(KeyEvent e) event-handling methods in JGEngine's JGListener inner class with the following methods, which solves this problem—be sure to recompile all source files if you are using a version of Java other than 1.4:
 public void keyReleased (KeyEvent e)
{
int keychar = e.getKeyChar ();
int keycode = e.getKeyCode ();
if (keycode >= 0 && keycode < 256) 
{
    canvas.keymap [keycode] = false;
}

/* shift escape = exit */

if (e.isShiftDown () 
    && e.getKeyCode () == KeyEvent.VK_ESCAPE 
    && !i_am_applet) 
{
    System.exit(0);
}
}

public void keyTyped (KeyEvent e)
{
}

The animation loop creates a frame by calling JGEngine's private void doFrameAll() method. This method handles many important tasks, ranging from removing sprites no longer in play to saving some of each sprite's state. Understanding these tasks, and the order in which they execute, helps you understand how your game works. Tasks for the current loop iteration include:

  1. Invoke canvas.flushRemoveList(), which removes all sprites marked for removal during sprite movement and collision-checking callbacks in the previous loop iteration.
  2. Invoke canvas.flushAddList(), which adds all sprites created and marked for addition in the previous loop iteration to the engine's list of sprites. The sprites are available to this loop iteration.
  3. Invoke tickTimers(), which advances all of the game's timers one frame. This may result in their callbacks being invoked (to execute game logic).
  4. Invoke canvas.flushRemoveList(), which removes all sprites marked for removal during sprite movement and collision-checking callbacks initiated by timer callbacks.
  5. Invoke canvas.flushAddList(), which adds all sprites created and marked for addition by timer callbacks to the engine's list of sprites. The sprites are available to this loop iteration.
  6. Assign gamestate_nextframe to gamestate. This statement saves the reference to this iteration's java.util.Vector of game states.
  7. Execute gamestate_nextframe = new Vector(10, 20) followed by gamestate_nextframe.addAll(gamestate). These statements copy the Vector previously referenced by gamestate_nextframe.
  8. Invoke invokeGameStateMethods("start", gamestate_new), which tries to invoke all startstate methods whose names are stored in gamestate_new.
  9. Invoke gamestate_new.clear(), which ensures that the previously called startstate methods are not called on the next loop iteration.
  10. Invoke canvas.flushRemoveList(), which removes all sprites marked for removal during sprite movement and collision-checking callbacks initiated by startstate methods.
  11. Invoke canvas.flushAddList(), which adds all sprites created and marked for addition by startstate methods to the engine's list of sprites. The sprites are available to this loop iteration.
  12. Invoke JGEngine's public void doFrame() method. A game subclass can override this method to move sprites, check for collisions, and perform other tasks.
  13. Invoke invokeGameStateMethods("doFrame", gamestate), which tries to invoke all doFramestate methods whose names are stored in gamestate. You might prefer to move sprites and perform other tasks in a state-specific way via these methods.
  14. Invoke canvas.frameFinished(), which saves some of the state for each of the sprites still in the game. The next iteration's game logic may need to access this state.

The tasks listed above show that a game can be implemented as a state machine, where the game transitions from one state to another state—a title state to an in-game state, for example. To support a state machine, JGEngine supplies public void setGameState(String state), public void addGameState(String state), and other state-related methods.

The setGameState() method lets you specify a single state for the game. If this state has not been previously set, the state's startstate method (if defined in the game) is invoked. The addGameState() method lets you add states to the game. As with setGameState(), each state's startstate method (if defined) is invoked, unless the state was previously set.

Following the call to doFrame(), each state's doFramestate method (if defined) is invoked. Even if you invoke JGEngine's public void removeGameState(String state) method to remove a state from within doFrame(), doFramestate is invoked—the use of gamestate_nextframe and gamestate makes this possible.

Following its call to doFrameAll(), the loop iteration invokes canvas.repaint() to render the current frame. This method call eventually results in the engine invoking JGEngine's public void paintFrame() method, followed by all defined paintstate methods for current game states—use these methods to render custom status information.

Some of the example games use state-related methods to control game play. They avoid overriding the doFrame() method, provide various doFramestate methods, and override paintFrame(), while also providing paintstate methods. To illustrate how a game performs state transitions, I've excerpted some code from the ChainReaction example game:

 public void initGame ()
{
   defineGraphics ("examples/chain_reaction.tbl");
   setFrameRate (40, 4);
   setGameState ("Title");
   setCursor (new Cursor (Cursor.CROSSHAIR_CURSOR));
}

public void doFrameTitle ()
{
   if (getMouseButton (1))
   {
       clearMouseButton (1);
       setGameState ("InGame");

       // Initialize variables.
   }
}

public void paintFrameTitle ()
{
   drawImageString ("CHAIN REACTION", 100, 130, -1, "font_map", 32, 0);
   drawImageString ("MOUSE BUTTON TO START", 0, 270, -1,"font_map", 32, -2);
}

In the code fragment above, initGame() invokes setGameState ("Title"); to ensure that the game begins in a title state. This results in calls to doFrameTitle() followed by paintFrameTitle(). If doFrameTitle() determines that the user has pressed mouse button #1, it invokes setGameState ("InGame"); to transition the game to the in-game state.

The code fragment reveals several additional JGEngine methods. Because the fragment's methods are important to many games, the following list introduces them (and a few other useful methods) to you—this list does not include methods for moving sprites and checking for collisions because I discuss these methods later:

  • public void clearKey(int key) resets the status of the specified key to indicate that the key is in the released state.
  • public void clearMouseButton(int button) resets the status of the specified button (1 for button 1) to indicate that the button is in the released state.
  • public void drawImageString(String string, int x, int y, int align, String imgmap, int char_offset, int spacing) draws a line of text on the play field:
    • string identifies the text to draw
    • x and y identify the leftmost character's coordinates
    • align identifies the text alignment (-1 = left, 0 = center, 1 = right)
    • imgmap identifies an image map describing the text's font
    • char_offset identifies a character code for the first character in the image map
    • spacing identifies the number of pixels between successive character images
  • public boolean getKey(int key) returns the status of the specified key. A true status indicates that the key is pressed.
  • public boolean getMouseButton(int button) returns the status of the specified button (1 for button 1). A true status indicates that the button is pressed.
  • public void removeObjects(String prefix, int cidmask) removes all sprites with the given name prefix and/or match the cidmask -- removeObjects (null, 0); removes all sprites.
  • public void setCursor(Cursor cursor) changes the current mouse cursor to the cursor identified by cursor. Pass null to hide the current mouse cursor.
  • public void setFrameRate(double fps, double maxframeskip) sets the frame rate to the fps frames per second value (35 is the default) and the maximum number of frames to skip before displaying the next frame to the maxframeskip value (4 is the default). Because the act of drawing many frames per second is usually a game's most processor-intensive activity, it may be necessary to skip frames to allow the game to run at its original speed on slow computers. Although the game appears to jitter, it doesn't slow down.

To save you the bother of defining states, invoking setGameState() and addGameState() at appropriate points in the game, and specifying your own state-specific startstate, doFramestate, and paintFramestate methods, JGame provides jgame.StdGame. This class implements a state machine with standard game states.

Tip
Instead of hard-coding a window size into your game's source code (see the 800 and 600 values in Listing 1's and Listing 2's main() methods), you can pass these values to your application on its command line, and conveniently retrieve these values by invoking StdGame's public static Dimension parseSizeArgs(String[] args) method. This method assumes that the first two command-line arguments are window width and height integers.

Game objects

Game objects, which I refer to as sprites, are the actors that participate in a game. When a sprite is created, it is automatically marked for addition to the engine's list of sprites. It is not added to this list until the next canvas.flushAddList() method call occurs in the animation loop.

Note
To find out why the engine marks sprites for addition and removal, and subsequently invokes flushAddList() and flushRemoveList() to perform the additions and removals, consult the Object updating section in the MANUAL.

JGame implements sprites via its jgame.JGObject class. An instance of this class manages its own position, speed, direction of movement, animation (if animation is specified), and expiry—sprite expires after a certain number of frames, sprite expires when the sprite moves off the screen, or sprite does not expire.

Call any one of 10 constructors to create a JGObject instance. The simplest constructor to call is public JGObject(String name, boolean unique_id, double x, double y, int collisionid, String gfxname): the parameters respectively name the sprite, determine if one or multiple instances can exist, and specify the sprite's initial position, collision ID, and image/animation definition ID.

Because this constructor does not affect a sprite's speed, movement direction, and expiry, the sprite defaults to no movement, movement towards the right and down whenever the speed is specified, and no expiry. If the sprite has an image, it also has an image bounding box (based on image definition values or image size) and a tile bounding box, which are used when checking for collisions with sprites and tiles.

Define a sprite by subclassing JGObject. The sprite class can be an inner class of your JGEngine or StdGame subclass, which gives it direct access to these class's methods. Alternatively, the sprite class can be an independent class because JGObject contains a reference to its associated JGEngine. The code fragment below illustrates sprite definition:

 class Game extends JGEngine
{
   // ...

   class Alien extends JGObject
   {
      final static int COLID = 1;

      Alien (double x, double y, double xdir)
      {
         // Create a unique Alien sprite at the specified position, using the
         // specified collision ID, and with the image defined by alien in
         // game.tbl.

         // If false was passed in the super() call below, only one alien
         // sprite could exist in the game. This sprite would only last until
         // a new alien sprite was created.

         super ("alien", true, x, y, COLID, "alien");

         // The alien sprite will not move until told to do so. The method
         // call below tells the alien sprite to move left or right (based on
         // xdir) and downwards.

         setDirSpeed ((xdir < 0.5) ? -1 : 1, 1, 1.0, 1.0);
      }

      // ...
   }
}

The JGObject class provides x, y, xdir, ydir, xspeed, and yspeed public fields, which you can manipulate directly. It also provides methods that you might prefer to call to set these fields. For example, public void setDirSpeed(int xdir, int ydir, double xspeed, double yspeed) sets the sprite's horizontal and vertical directions and speeds.

The JGObject class also provides a public void move() method that the engine calls when it is time to move the sprite. A sprite class overrides this method to adjust some combination of the fields above. For example, the following code fragment implements Alien's move() method:

 public void move ()
{
   // Reverse this alien sprite's current horizontal direction if it
   // runs into the left or right sides of the play field.

   if (x < 0)
       xdir = 1;
   else
   if (x > pfWidth ()-32)
       xdir = -1;

   // Reverse this alien sprite's current vertical direction if it runs
   // into the bottom or (just past the) top sides of the play field.

   if (y < -90)
       ydir = 1;
   else
   if (y > pfHeight ()-32)
       ydir = -1;
}

When sprites move, they are bound to collide with other sprites, or with tiles. The engine uses a bounding box to determine if a collision has occurred. If the collision occurs between sprites, JGObject's public void hit(JGObject obj) method is called. But if the collision is between a sprite and a tile, all three of JGObject's hit_bg() methods are called.

As with move(), the hit() and hit_bg() methods default to doing nothing. You need to override these methods to respond appropriately to sprite-sprite and sprite-tile collisions. For example, the code fragment below responds to a collision between two sprites (hit()'s obj parameter identifies the sprite that collided with the current sprite):

 public void hit (JGObject obj)
{
   // Create and register a unique explosion sprite positioned at the
   // alien's most recent coordinates. A collision identifier is not
   // necessary, which is why 0 is passed as a placeholder. The
   // explosion animation is defined by explo. It doesn't move anywhere, 
   // hence 0 is passed for the horizontal and vertical speeds. Finally,
   // the explosion will last for 32 frames.

   new JGObject ("explo", true, x, y, 0, "explo", 0, 0, 32);

   // Remove this alien sprite.

   remove ();

   // Remove the alien sprite that collided with this alien sprite.

   obj.remove ();
}

In response to a collision, you typically do something interesting, like presenting an exploding animated sprite. You would also typically remove both the sprite that collided and the current sprite from the game. This requires that you implement a pair of calls to JGObject's public void remove() method, which marks the sprites for removal—removal occurs as soon as possible.

Note
Responding properly to collisions can be somewhat tricky. For example, a sprite may pass through a wall tile when it should not. To avoid these problems, a sprite may need to step back to its previous step prior to the collision. The JGObject class provides several getLast methods that a sprite can use to determine its previous position and other information. This information is made available via the animation loop's canvas.frameFinished() method call.

The engine invokes move() via JGEngine's public void moveObjects() method. Similarly, hit(), and hit_bg() are invoked via JGEngine's public void checkCollision(int srccid, int dstcid) and public void checkBGCollision(int tilecid, int objcid) methods.

Your game class's doFrame() or doFramestate method is responsible for invoking moveObjects(), which invokes each sprite's move() method. After move() completes, the sprite's animation is updated (if an animation was specified in the constructor), and the sprite position is updated by multiplying the direction by the speed and adding it to the position.

Following moveObjects(), doFrame() or doFramestate typically invokes checkCollision() and checkBGCollision(). If the image bounding boxes for the srcid and dstid sprites intersect, checkCollision() invokes hit(). The other method works similarly. Consider the code fragment below:

 public void doFrame ()
{
   // Move all alien sprites. For each sprite, Alien's move() method is
   // called.
   moveObjects ();

   // Checks for collisions between all alien sprites. If a collision
   // occurs, Alien's hit() method is called.

   checkCollision (Alien.COLID, Alien.COLID);

   // Generate a new alien sprite anywhere above the top of the screen.

   if (random (1, 50, 1) == 25)
       new Alien (random (0, pfWidth ()-32), -90, random (0.0, 1.0));
}

The animation loop invokes doFrame(), which invokes moveObjects(), which invokes each sprite's move() method. Then, doFrame() invokes checkCollision(), which might invoke a sprite's hit() method. Finally, a new Alien is created and introduced into the game. Two of JGEngine's random() methods make this more interesting.

I excerpted the previous code fragments from a second revision to my Game program. This revision moves hostile aliens around a play field. Because these aliens are so angry, they destroy each other when they come into contact. To achieve a realistic explosion, I borrowed one of Boris's explosion image maps. You can see part of this explosion and a few aliens in Figure 4.

Figure 4. Aliens go to war on Mars. Click on thumbnail to view full-sized image.

I would love to continue WAR ON MARS, but this article is long enough. Continue this game on your own: Create a Player class similar to NebulaAlpha's Player class. The player should appear near the bottom of the play field. In response to user input, the player should move left or right, and should fire projectiles at (and destroy) the aliens. The game ends when an alien collides with (and kills) the player.

To save you the bother of defining certain kinds of sprites, JGame's examples package provides five standard sprite classes that subclass JGObject—StdDungeonMonster, StdDungeonPlayer, StdMazeMonster, StdMazePlayer, and StdScoring. You can subclass these classes or use them directly.

Tip
You can spice up your game's on-screen score and other messages by using StdScoring. This class animates a message by cycling through an array of colors.

Timers

A timer is responsible for generating a callback into the game after a specific number of frames have elapsed. The callback can be used to remove game objects and reset a game to an initial state when the game ends, or it can accomplish some other task. The timer is updated, sprites removed by its callback disappear, and sprites added by its callback become accessible to the game before doFrame() is invoked.

JGame implements timers via its jgame.JGTimer class. This class provides three constructors for creating and registering a timer with the engine: public JGTimer(int frames_to_alarm, boolean one_shot), public JGTimer(int frames_to_alarm, boolean one_shot, JGObject parent), and public JGTimer(int frames_to_alarm, boolean one_shot, String parent).

All three constructors have two parameters in common: frames_to_alarm specifies the number of frames (starting with 0) that must elapse before the callback is generated; one_shot specifies whether the callback is generated once—and then the timer removes itself (true)—or generated continuously (false).

The second constructor specifies a parent parameter that identifies a JGObject as the timer's parent. Similarly, the third constructor specifies a parent parameter that identifies a String game state as the timer's parent. When the parent object is no longer alive, or the parent game state is no longer valid, the timer removes itself without generating a callback.

Because JGTimer is abstract, it must be subclassed and its public abstract void alarm() callback method implemented to perform the appropriate game logic when this method is invoked. This is demonstrated in the following code fragment, which I've excerpted from the ChainReaction game's source code:

 if (y >= pfHeight ()-32) 
{ 
    // trooper lands

    setSpeed(0,0);
    colid = 0;
    setGraphic ("trooper_land");
    if (!inGameState ("GameOver")) 
    {
        addGameState ("GameOver");
        new JGTimer (250, true) 
        { 
            public void alarm () 
            {
               removeObjects (null, 0);
               setGameState ("Title");
            } 
        };
    }
}

The code fragment determines whether a paratrooper sprite has reached the bottom of the screen. This happens when y is greater than pfHeight ()-32. If another paratrooper sprite has not previously "landed" (we are not in the GameOver game state), addGameState ("GameOver"); adds GameOver to the game's existing game states, which takes effect as soon as the code fragment ends.

A one-shot timer is also created. This timer invokes alarm() after 250 frames have elapsed. This time delay allows game action to continue, which includes additional paratroopers landing at the bottom of the screen. But once alarm() is invoked, it invokes removeObjects (null, 0); to mark all sprites for removal, followed by setGameState ("Title"); to return the game to its title screen.

Conclusion

For brevity, I restricted my coverage of JGame's architecture to a few important topics. There is much more for you to discover. For example, jgame provides the AppConfig and ImageUtil classes. And JGEngine provides public static void dbgShowGameState(boolean enabled) and other debugging methods to help you debug a malfunctioning game.

In spite of version 1.2's lack of sound support and other deficiencies, JGame is an interesting and useful game engine with entertaining examples. Now that you have a basic understanding of JGame, study the source code to its architecture and examples classes to deepen this understanding—and read the MANUAL. You can also implement the two improvements mentioned in this article.

If you've had success developing with other gaming tools, please share those experiences in the discussion thread that appears below.

About the author

Jeff Friesen is a freelance software developer and educator specializing in C, C++, and Java technology.
  • Print
  • Feedback

Resources