MIDP 2.0 Games: a Step-by-Step Tutorial with Code Samples (Step 3)

The GameCanvas Class


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 repainting the screen.


The graphics buffering allows all of 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 the code below. (The method advance() is called from the main loop of my GameThread object.) Notice that all you need to do is call paint(getGraphics()) and then call flushGraphics(). To make your program more efficient there is even a version of the flushGraphics() method which 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.


If you’re following along in the code below, you’ll notice that after flushing the graphics (still in the method advance()), I have the thread wait 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 is also useful to help the keystroke query work correctly. As I mentioned above, the ability to query key states is one of the differences between GameCanvas and Canvas. With Canvas, if you want to know about keystroke events you must implement the keyPressed(int keyCode) method which the enveloping Java program will call when it wants to tell your program that a key has been pressed. With GameCanvas, you can call the method getKeyStates() whenever your program wants to know which keys have been pressed. Of course the value returned by getKeyStates() (telling you what key(s) have been pressed), is still updated on another thread, so it’s necessary to put a short wait inside your game loop to make sure that the key states value is updated in a timely fashion allowing 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…).


It’s easy to see how these two enhancements in GameCanvas improve control over the order of execution of painting and keystroke-related updates. Going back to my GameThread class, notice that the main game loop first tells my GameCanvas subclass (called JumpCanvas) to query the key states (see the method JumpCanvas.checkKeys() below for details). Then once the key events have been dealt with, the main loop of the GameThread class calls JumpCanvas.advance() which tells the LayerManager to make appropriate updates in the graphics (more on that in the next sections) and then paints the screen and then waits as explained above.


Here’s the code for JumpCanvas.java:


package net.frog_parrot.jump;


import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;


/**
 * This class is the display of the game.
 *
 * @author Carol Hamer
 */
public class JumpCanvas extends javax.microedition.lcdui.game.GameCanvas {


  //———————————————————
  //   dimension fields
  //  (constant after initialization)


  /**
   * the height of the green region below the ground.
   */
  static int GROUND_HEIGHT = 32;


  /**
   * a screen dimension.
   */
  static int CORNER_X;


  /**
   * a screen dimension.
   */
  static int CORNER_Y;


  /**
   * a screen dimension.
   */
  static int DISP_WIDTH;


  /**
   * a screen dimension.
   */
  static int DISP_HEIGHT;


  /**
   * a font dimension.
   */
  static int FONT_HEIGHT;


  /**
   * the default font.
   */
  static Font FONT;


  /**
   * a font dimension.
   */
  static int SCORE_WIDTH;


  /**
   * The width of the string that displays the time,
   * saved for placement of time display.
   */
  static int TIME_WIDTH;


  //———————————————————
  //   game object fields


  /**
   * a handle to the display.
   */
  Display myDisplay;


  /**
   * a handle to the MIDlet object (to keep track of buttons).
   */
  Jump myJump;


  /**
   * the LayerManager that handles the game graphics.
   */
  JumpManager myManager;


  /**
   * whether or not the game has ended.
   */
  static boolean myGameOver;


  /**
   * the player’s score.
   */
  int myScore = 0;


  /**
   * How many ticks we start with.
   */
  int myInitialGameTicks = 950;


  /**
   * this is saved to determine if the time string needs
   * to be recomputed.
   */
  int myOldGameTicks = myInitialGameTicks;


  /**
   * the number of game ticks that have passed.
   */
  int myGameTicks = myOldGameTicks;


  /**
   * whether or not this has been painted once.
   */
  boolean myInitialized;


  /**
   * The initial time string.
   */
  static String myInitialString = “1:00”;


  /**
   * we save the time string to avoid recreating it
   * unnecessarily.
   */
  String myTimeString = myInitialString;


  //—————————————————–
  //    gets/sets


  /**
   * This is called when the game ends.
   */
  static void setGameOver() {
    myGameOver = true;
    GameThread.requestStop();
  }


  /**
   * Find out if the game has ended.
   */
  static boolean getGameOver() {
    return(myGameOver);
  }


  //—————————————————–
  //    initialization and game state changes


  /**
   * Constructor sets the data.
   */
  public JumpCanvas(Jump midlet) {
    super(false);
    myDisplay = Display.getDisplay(midlet);
    myJump = midlet;
  }


  /**
   * This is called as soon as the application begins.
   */
  void start() {
    myGameOver = false;
    myDisplay.setCurrent(this);
    repaint();
  }


  /**
   * sets all variables back to their initial positions.
   */
  void reset() {
    myManager.reset();
    myScore = 0;
    myGameOver = false;
    myGameTicks = myInitialGameTicks;
    myOldGameTicks = myInitialGameTicks;
    repaint();
  }


  /**
   * clears the key states.
   */
  void flushKeys() {
    getKeyStates();
  }


  //——————————————————-
  //  graphics methods


  /**
   * paint the game graphics on the screen.
   */
  public void paint(Graphics g) {
    // perform the calculations if necessary:
    if(!myInitialized) {
      CORNER_X = g.getClipX();
      CORNER_Y = g.getClipY();
      DISP_WIDTH = g.getClipWidth();
      DISP_HEIGHT = g.getClipHeight();
      FONT = g.getFont();
      FONT_HEIGHT = FONT.getHeight();
      SCORE_WIDTH = FONT.stringWidth(“Score: 000”);
      TIME_WIDTH = FONT.stringWidth(“Time: ” + myInitialString);
      myInitialized = true;
    }
    // clear the screen:
    g.setColor(0xffffff);
    g.fillRect(CORNER_X, CORNER_Y, DISP_WIDTH, DISP_HEIGHT);
    g.setColor(0x0000ff00);
    g.fillRect(CORNER_X, CORNER_Y + DISP_HEIGHT – GROUND_HEIGHT,
        DISP_WIDTH, DISP_HEIGHT);
    // create (if necessary) then paint the layer manager:
    try {
      if(myManager == null) {
 myManager = new JumpManager(CORNER_X, CORNER_Y + FONT_HEIGHT*2,
      DISP_WIDTH, DISP_HEIGHT – FONT_HEIGHT*2 – GROUND_HEIGHT);
      }
      myManager.paint(g);
    } catch(Exception e) {
      errorMsg(g, e);
    }
    // draw the time and score
    g.setColor(0);
    g.setFont(FONT);
    g.drawString(“Score: ” + myScore,
   (DISP_WIDTH – SCORE_WIDTH)/2,
   DISP_HEIGHT + 5 – GROUND_HEIGHT, g.TOP|g.LEFT);
    g.drawString(“Time: ” + formatTime(),
     (DISP_WIDTH – TIME_WIDTH)/2,
     CORNER_Y + FONT_HEIGHT, g.TOP|g.LEFT);
    // write game over if the game is over
    if(myGameOver) {
      myJump.setNewCommand();
      // clear the top region:
      g.setColor(0xffffff);
      g.fillRect(CORNER_X, CORNER_Y, DISP_WIDTH, FONT_HEIGHT*2 + 1);
      int goWidth = FONT.stringWidth(“Game Over”);
      g.setColor(0);
      g.setFont(FONT);
      g.drawString(“Game Over”, (DISP_WIDTH – goWidth)/2,
           CORNER_Y + FONT_HEIGHT, g.TOP|g.LEFT);
    }
  }


  /**
   * a simple utility to make the number of ticks look like a time…
   */
  public String formatTime() {
    if((myGameTicks / 16) + 1 != myOldGameTicks) {
      myTimeString = “”;
      myOldGameTicks = (myGameTicks / 16) + 1;
      int smallPart = myOldGameTicks % 60;
      int bigPart = myOldGameTicks / 60;
      myTimeString += bigPart + “:”;
      if(smallPart / 10 < 1) {
 myTimeString += “0”;
      }
      myTimeString += smallPart;
    }
    return(myTimeString);
  }


  //——————————————————-
  //  game movements


  /**
   * Tell the layer manager to advance the layers and then
   * update the display.
   */
  void advance() {
    myGameTicks–;
    myScore += myManager.advance(myGameTicks);
    if(myGameTicks == 0) {
      setGameOver();
    }
    // paint the display
    try {
      paint(getGraphics());
      flushGraphics();
    } catch(Exception e) {
      errorMsg(e);
    }
    // we do a very short pause to allow the other thread
    // to update the information about which keys are pressed:
    synchronized(this) {
      try {
 wait(1);
      } catch(Exception e) {}
    }
  }


  /**
   * Respond to keystrokes.
   */
  public void checkKeys() {
    if(! myGameOver) {
      int keyState = getKeyStates();
      if((keyState & LEFT_PRESSED) != 0) {
 myManager.setLeft(true);
      }
      if((keyState & RIGHT_PRESSED) != 0) {
 myManager.setLeft(false);
      }
      if((keyState & UP_PRESSED) != 0) {
 myManager.jump();
      }
    }
  }


  //——————————————————-
  //  error methods


  /**
   * Converts an exception to a message and displays
   * the message..
   */
  void errorMsg(Exception e) {
    errorMsg(getGraphics(), e);
    flushGraphics();
  }


  /**
   * Converts an exception to a message and displays
   * the message..
   */
  void errorMsg(Graphics g, Exception e) {
    if(e.getMessage() == null) {
      errorMsg(g, e.getClass().getName());
    } else {
      errorMsg(g, e.getClass().getName() + “:” + e.getMessage());
    }
  }


  /**
   * Displays an error message if something goes wrong.
   */
  void errorMsg(Graphics g, String msg) {
    // clear the screen
    g.setColor(0xffffff);
    g.fillRect(CORNER_X, CORNER_Y, DISP_WIDTH, DISP_HEIGHT);
    int msgWidth = FONT.stringWidth(msg);
    // write the message in red
    g.setColor(0x00ff0000);
    g.setFont(FONT);
    g.drawString(msg, (DISP_WIDTH – msgWidth)/2,
   (DISP_HEIGHT – FONT_HEIGHT)/2, g.TOP|g.LEFT);
    myGameOver = true;
  }


}


 


Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.