Using the MIDP 2.0 Game API

Get started building games for J2ME with MIDP 2.0

Let's look at the most important package you'll be dealing with when writing games with Mobile Internet Device Profile (MIDP) 2.0: the package javax.microedition.lcdui.game. In this chapter, I show you the main parts of a MIDP 2.0 game by explaining the code of an example game called Tumbleweed. The game involves a cowboy walking through a prairie jumping over tumbleweeds. It's kind of a silly game, but it illustrates most of the basics you'll need when writing more reasonable games.

Starting with the MIDlet class

As usual, the application starts with the MIDlet class. In this case, my MIDlet subclass is called Jump. This class is essentially the same as the MIDlet subclass. The only differences here are the use of a separate GameThread class and the fact that when the user presses a command button, I have the MIDlet change the command that's available on the screen. The command change is because the user can pause the game only when it's unpaused, can unpause the game only when it's paused, and can start over only when the game has ended.

Listing 1 shows the game's MIDlet subclass called Jump.java.

Listing 1. Jump.java

package net.frog_parrot.jump;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
/**
 * This is the main class of the Tumbleweed game.
 *
 * @author Carol Hamer
 */
   public class Jump extends MIDlet implements CommandListener {
   //---------------------------------------------------------
   // Commands
    /**
    * The command to end the game.
    */
   private Command myExitCommand = new Command("Exit", Command.EXIT, 99);
   /**
    * The command to start moving when the game is paused.
    */
   private Command myGoCommand = new Command("Go", Command.SCREEN, 1);
   
   /**
    * The command to pause the game.
    */
   private Command myPauseCommand = new Command("Pause", Command.SCREEN, 1);
   /**
    * The command to start a new game.
    */
   private Command myNewCommand = new Command("Play Again", Command.SCREEN, 1);
   //---------------------------------------------------------
   // Game object fields
   /**
    * The canvas that all of the game will be drawn on.
    */
   private JumpCanvas myCanvas;
   /**
    * The thread that advances the cowboy.
    */
   private GameThread myGameThread;
   
//-----------------------------------------------------
   // Initialization and game state changes
   /**
    * Initialize the canvas and the commands.
    */
   public Jump() {
      try {
         myCanvas = new JumpCanvas(this);
         myCanvas.addCommand(myExitCommand);
         myCanvas.addCommand(myPauseCommand);
         myCanvas.setCommandListener(this);
      } catch(Exception e) {
         errorMsg(e);
      }
   }
   
   /**
    * Switch the command to the play again command.
    */
   void setNewCommand () {
      myCanvas.removeCommand(myPauseCommand);
      myCanvas.removeCommand(myGoCommand);
      myCanvas.addCommand(myNewCommand);
   }
   
   /**
    * Switch the command to the go command.
    */
   private void setGoCommand() {
      myCanvas.removeCommand(myPauseCommand);
      myCanvas.removeCommand(myNewCommand);
      myCanvas.addCommand(myGoCommand);
   }
   
   /**
    * Switch the command to the pause command.
    */
   private void setPauseCommand () {
      myCanvas.removeCommand(myNewCommand);
      myCanvas.removeCommand(myGoCommand);
      myCanvas.addCommand(myPauseCommand);
   }
   
   //----------------------------------------------------------------
   // Implementation of MIDlet.
   // These methods may be called by the application management
   // software at any time, so you always check fields for null
   // before calling methods on them.
   
   /**
    * Start the application.
    */
   public void startApp() throws MIDletStateChangeException {
      if(myCanvas != null) {
         if(myGameThread == null) {
            myGameThread = new GameThread(myCanvas);
            myCanvas.start();
            myGameThread.start();
         } else {
            myCanvas.removeCommand(myGoCommand);
            myCanvas.addCommand(myPauseCommand);
            myCanvas.flushKeys();
            myGameThread.resumeGame();
         }
      }
   }
   
   /**
   * Stop and throw out the garbage.
   */
   public void destroyApp(boolean unconditional)
         throws MIDletStateChangeException {
      if(myGameThread != null) {
         myGameThread.requestStop();
      }
      myGameThread = null;
      myCanvas = null;
      System.gc();
   }
   
   /**
    * Request the thread to pause.
    */
   public void pauseApp() {
      if(myCanvas != null) {
         setGoCommand();
      }
      If(myGameThread != null) {
         myGameThread.pauseGame();
      }
   }
   
   //----------------------------------------------------------------
   // Implementation of CommandListener
   /*
    * Respond to a command issued on the Canvas.
    * (either reset or exit).
    */
   public void commandAction(Command c, Displayable s) {
      if(c == myGoCommand) {
         myCanvas.removeCommand(myGoCommand);
         myCanvas.addCommand(myPauseCommand);
         myCanvas.flushKeys();
         myGameThread.resumeGame();
      } else if(c == myPauseCommand) {
         myCanvas.removeCommand(myPauseCommand);
         myCanvas.addCommand(myGoCommand);
         myGameThread.pauseGame();
      } else if(c == myNewCommand) {
         myCanvas.removeCommand(myNewCommand);
         myCanvas.addCommand(myPauseCommand);
         myCanvas.reset();
         myGameThread.resumeGame();
      } else if((c == myExitCommand) || (c == Alert.DISMISS_COMMAND)) {
         try {
            destroyApp(false);
            notifyDestroyed();
         } catch (MIDletStateChangeException ex) {
         }
      }
   }
   
   //-------------------------------------------------------
   // Error methods
   
   /**
    * Converts an exception to a message and displays
    * the message.
    */
   void errorMsg(Exception e) {
      if(e.getMessage() == null) {
         errorMsg(e.getClass().getName());
      } else {
         errorMsg(e.getClass().getName() + ":" + e.getMessage());
      }
   }
   
   /**
    * Displays an error message alert if something goes wrong.
    */
   void errorMsg(String msg) {
      Alert errorAlert = new Alert("error", msg, null, AlertType.ERROR);
      errorAlert.setCommandListener(this);
      errorAlert.setTimeout(Alert.FOREVER);
      Display.getDisplay(this).setCurrent(errorAlert);
   }
}

Using the Thread class

This game requires only the simplest use of the Thread class. But even in this simple case, I'd like to mention a few points.

In this case, it really is necessary to spawn a new thread. The animation in this game is always moving, even when the user doesn't press a button, so I need to have a game loop that repeats constantly until the end of the game. I can't use the main thread for the game loop because the application management software may need to use the main thread while my game is running. While testing the game in the emulator, I found that if I use the main thread for my game's animation loop, the emulator is unable to respond to keystrokes. Of course, in general, it's good practice to spawn a new thread when you plan to go into a loop that's to be repeated throughout the duration of your program's active life cycle.

Here's how my Thread subclass (called GameThread) works: Once the thread starts, it goes into the main loop (inside the while(true) block). The first step is to check if the Jump class has called requestStop() since the last cycle. If so, you break out of the loop, and the run() method returns. Otherwise, if the user hasn't paused the game, you prompt the GameCanvas to respond to the user's keystrokes and advance the game animation. Then you do a short pause of one millisecond. This is partially to be sure that the freshly painted graphics stay on the screen for an instant before the next paint, but it's also useful to help the keystroke query work correctly.

As mentioned, the information about the user's keystrokes is updated on another thread, so it's necessary to put a short wait inside your game loop to make sure that the other thread gets a turn and has the opportunity to update the key state's value in a timely fashion. This allows your game to respond immediately when the user presses a key. Even a millisecond will do the trick. (I earlier wrote a racecar game in which I neglected to put a wait in the main game loop, and I found that the car would go halfway around the track between the time I pressed the lane change key and the time the car actually changed lanes.)

Listing 2 shows the code for GameThread.java.

Listing 2. GameThread.java

package net.frog_parrot.jump;
/**
 * This class contains the loop that keeps the game running.
 *
 * @author Carol Hamer
 */
public class GameThread extends Thread {
   //---------------------------------------------------------
   // Fields
   /**
    * Whether the main thread would like this thread
    * to pause.
    */
private boolean myShouldPause;
   /**
    * Whether the main thread would like this thread
    * to stop.
    */
   private boolean myShouldStop;
   /**
    * A handle back to the graphical components.
    */
   private JumpCanvas myJumpCanvas;
   //----------------------------------------------------------
   // Initialization
   /**
    * Standard constructor.
    */
   GameThread(JumpCanvas canvas) {
      myJumpCanvas = canvas;
   }
   //----------------------------------------------------------
   // Actions
   /**
    * Pause the game.
    */
   void pauseGame() {
      myShouldPause = true;
   }
/**
    * Restart the game after a pause.
    */
   synchronized void resumeGame() {
      myShouldPause = false;
      notify();
   }
   /**
    * Stops the game.
    */
   synchronized void requestStop() {
      myShouldStop = true;
      notify();
   }
   /**
    * Start the game.
   */
   public void run() {
   // Flush any keystrokes that occurred before the
   // game started:
   myJumpCanvas.flushKeys();
   myShouldStop = false;
   myShouldPause = false;
   while(true) {
      if(myShouldStop) {
         break;
      }
      synchronized(this) {
            while(myShouldPause) {
               try {
                  wait();
               } catch(Exception e) {}
            }
      }
      myJumpCanvas.checkKeys();
      myJumpCanvas.advance();
      // You do a short pause to allow the other thread
      // to update the information about which keys are pressed:
      synchronized(this) {
         try {
            wait(1);
               } catch(Exception e) {}
            }
         }
      }
   }

Using the GameCanvas class

Now you'll look at the class that allows you to paint customized game graphics to the screen.

How GameCanvas differs from Canvas

The GameCanvas class represents the area of the screen that the device has allotted to your game. The javax.microedition.lcdui.game.GameCanvas class differs from its superclass javax.microedition.lcdui.Canvas in two important ways: graphics buffering and the ability to query key states. Both of these changes give the game developer enhanced control over precisely when the program deals with events such as keystrokes and screen repainting.

The graphics buffering allows all the graphical objects to be created behind the scenes and then flushed to the screen all at once when they're ready. This makes animation smoother. I've illustrated how to use it in the method advance() in Listing 3. (Recall that the method advance() is called from the main loop of my GameThread object.) Notice that to update and repaint the screen, all you need to do is call paint(getGraphics()) and then call flushGraphics(). To make your program more efficient, there's even a version of the flushGraphics() method that allows you to repaint just a subset of the screen if you know that only part has changed. As an experiment, I tried replacing the calls to paint(getGraphics()) and flushGraphics() with calls to repaint() and then serviceRepaints() as you might if your class extended Canvas instead of GameCanvas. In my simple examples, it didn't make much difference, but if your game has a lot of complicated graphics, the GameCanvas version will undoubtedly make a big difference.

The ability to query key states is helpful for the game's organization. When you extend the Canvas class directly, you must implement the keyPressed(int keyCode) if your game is interested in keystrokes. The application management software then calls this method when the user presses a button. But if your program is running on its own thread, this might happen at any point in your game's algorithm. If you're not careful about using synchronized blocks, this could potentially cause errors if one thread is updating data about the game's current state and the other is using that data to perform calculations. The program is simpler and easier to follow if you get the keystroke information when you want it by calling the GameCanvas method getKeyStates().

An additional advantage of the getKeyStates() method is that it can tell you if multiple keys are being pressed simultaneously. The keyCode that's passed to the keyPressed(int keyCode) method can tell you only about a single key and therefore will be called multiple times even if the user presses two keys at the same time.

1 2 3 4 5 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more