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

Now that I’ve discussed the special functions of a GameCanvas, I’d like to go over the main function of a Canvas in general, namely painting the screen. This usually takes place in the method paint(Graphics g) which you can override. The Graphics object can be queried for screen dimensions and then can be used to draw strings, images, and simple geometric objects such as rectangles. The Graphics object has a rich selection methods for drawing things on the screen. (It’s similar to the java.awt.graphics package except that since javax.microedition.lcdui.Graphics is intended for small devices it saves memory by having each shape be drawn by a simple method call instead of creating individual objects.) It would take several pages for me to go over all of the things that you can draw with the Graphics object, so here I will stick to discussing the parts of the class that I used. Plus the JavaDoc for javax.microedition.lcdui.Graphics is quite thorough and clear, so you should definitely look it over when writing your game.


In my example game “Tumbleweed” I need to draw a cowboy walking through a prairie jumping over tumbleweeds. The screen during the game looks like this:



As you can see I’ve put the score on the bottom and the time remaining on the top. (To simplify the game I just have it end when the player runs out of time.) As the cowboy is walking along, I would like his background to scroll to the right or to the left (otherwise he won’t have very far to go on such a small screen…) but I would like the time and the score to stay in place. To accomplish this I have my JumpCanvas class take care of painting the stable strip on the top and the bottom of the screen and I delegate the interesting graphics to the LayerManager (more details on that in the next section).


Looking in the paint(Graphics g) method, you see that the first step is to use the graphics object to get the screen dimensions and then use that information to calculate where the objects should be placed. If you’re interested in maintaining Java’s “write once, run anywhere” philosophy, it is obviously better to base the screen layout on the (dynamically determined) dimensions of the current screen rather than basing the dimensions on fixed constants. Even so your game will likely look strange on a screen that is significantly larger or smaller than the one you wrote the game on. You may want to throw an Exception if the screen size is outside of a reasonable max or min.


At the risk of belaboring the obvious, I’ll point out that in the paint(Graphics g) method, after calculating the appropriate sizes for the top and bottom regions, I paint the top one white and the bottom one green with g.fillRect and then I use g.drawString to add the time and the score. (Don’t ask me why my prairie has both green grass AND tumbleweeds: my only excuse is that I know more about Java than I know about the wild west…) Then I calculate the size of the region between them and pass it along to my subclass of LayerManager.


The LayerManager Class


The interesting graphical objects in a J2ME game are usually represented by subclasses of the javax.microedition.lcdui.game.Layer class. The background layers could be instances of javax.microedition.lcdui.game.TiledLayer and the player (and his enemies) would likely be instances of javax.microedition.lcdui.game.Sprite, both of which are subclasses of Layer. The LayerManager class helps you to organize all of these graphical layers. The order in which you append your Layers to your LayerManager determines the order in which they will be painted. (The first one appended is the last one painted.) The top layers will cover the lower layers although you can allow parts of the lower layers to show through by creating image files that have transparent regions.


Probably the most useful aspect of the LayerManager class is that you can create a graphical painting that is much larger than the screen and then choose which section of it will appear on the screen. Imagine drawing a huge and elaborate drawing and then covering it with a piece of paper that has a small rectangular hole that you can move around. The whole drawing represents what you can stock into the LayerManager, and the hole is the window showing the part that appears on the screen at any given time. Allowing the possibility of a virtual screen that is much larger than the actual screen is extremely helpful for games on devices with very small screens. It will save you huge amounts of time and effort if for example your game involves a player exploring an elaborate dungeon. The confusing part is that this means that you have to deal with two separate coordinate systems. The Graphics object of the GameCanvas has one coordinate system, but the various Layers need to be placed in the LayerManager according to the LayerManager’s coordinate system. So keep in mind that the method LayerManager.paint(Graphics g, int x, int y) paints the layer on the screen according to the coordinates of the GameCanvas whereas the method LayerManager.setViewWindow(int x, int y, int width, int height) sets the visible rectangle of the LayerManager in terms of the LayerManager’s coordinate system.


In my example I have a very simple background (it’s just a repeating series of patches of grass), but I would like the cowboy to stay in the middle of the screen as he goes to the right and left, so I need to continuously change which part of the LayerManager’s graphical area is visible. I do this by calling the method setViewWindow(int x, int y, int width, int height) from the paint(Graphics g) method of my subclass of LayerManager (called JumpManager). More precisely, what happens is the following: The main loop in the GameThread calls JumpCanvas.checkKeys() which queries the key states and tells the JumpManager class whether the cowboy should be walking to the right or to the left and whether he should be jumping. JumpCanvas passes this information along to JumpManager by calling the methods setLeft(boolean left) or jump(). If the message is to jump, the JumpManager calls jump() on the cowboy Sprite. If the message is that the cowboy is going to the left (or similarly to the right), then when the GameThread calls the JumpCanvas to tell the JumpManager to advance (in the next step of the loop), the JumpManager tells the cowboy sprite to move one pixel to the left and compensates by moving the view window one pixel to the right to keep the cowboy in the center of the screen. These two actions are accomplished by incrementing the field myCurrentLeftX (which is the x-coordinate that is sent to the method setViewWindow(int x, int y, int width, int height)) and then calling myCowboy.advance(gameTicks, myLeft). Of course I could keep the cowboy centered by not moving him and not appending him to the LayerManager but rather painting him separately afterwards, but it’s easier to keep track of everything by putting all of the moving graphics on one set of layers and then keeping the view window focused on the cowboy Sprite. While telling the cowboy to advance his position, I also have the tumbleweed Sprites advance their positions and I have the grass TiledLayer advance its animation and then I check if the cowboy has collided with any tumbleweeds, but I will go into more detail on those steps in the following sections. After moving the game pieces around, the JumpManager calls the method wrap() to see if the view window has reached the edge of the background, and if so, to move all of the game objects so that the background appears to continue indefinitely in both directions. Then the JumpCanvas repaints everything and then the game loop begins again.


I’ll just add a few words here about the method wrap(). The class LayerManager unfortunately does not have a built in wrapping capability for the case in which you have a simple background that you would like to have repeat indefinitely. The LayerManager’s graphical area will appear to wrap when the coordinates sent to setViewWindow(int x, int y, int width, int height) exceed the value Integer.MAX_VALUE, but that is unlikely to help you. Thus you have to write your own functions to prevent the player Sprite from leaving the region that contains background graphics. In my example, the background grass repeats after the number of pixels given by Grass.TILE_WIDTH*Grass.CYCLE. So whenever the x-coordinate of the view window (myCurrentLeftX) is an integer multiple of the length of the background, I move the view window back to the center and also move all of the Sprites in the same direction which seamlessly prevents the player from reaching the edge.


Here’s the code for JumpManager.java:


package net.frog_parrot.jump;


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


/**
 * This handles the graphics objects.
 *
 * @author Carol Hamer
 */
public class JumpManager extends javax.microedition.lcdui.game.LayerManager {


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


  /**
   * The x-coordinate of the place on the game canvas where
   * the LayerManager window should appear, in terms of the
   * coordiantes of the game canvas.
   */
  static int CANVAS_X;


  /**
   * The y-coordinate of the place on the game canvas where
   * the LayerManager window should appear, in terms of the
   * coordiantes of the game canvas.
   */
  static int CANVAS_Y;


  /**
   * The width of the display window.
   */
  static int DISP_WIDTH;


  /**
   * The height of this object’s graphical region. This is
   * the same as the height of the visible part because
   * in this game the layer manager’s visible part scrolls
   * only left and right but not up and down.
   */
  static int DISP_HEIGHT;


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


  /**
   * the player’s object.
   */
  Cowboy myCowboy;


  /**
   * the tumbleweeds that enter from the left.
   */
  Tumbleweed[] myLeftTumbleweeds;


  /**
   * the tumbleweeds that enter from the right.
   */
  Tumbleweed[] myRightTumbleweeds;


  /**
   * the object representing the grass in the background..
   */
  Grass myGrass;


  /**
   * Whether or not the player is currently going left.
   */
  boolean myLeft;


  /**
   * The leftmost x-coordinate that should be visible on the
   * screen in terms of this objects internal coordinates.
   */
  int myCurrentLeftX;


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


  /**
   * This tells the player to turn left or right.
   * @param left whether or not the turn is towards the left..
   */
  void setLeft(boolean left) {
    myLeft = left;
  }


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


  /**
   * Constructor merely sets the data.
   * @param x The x-coordinate of the place on the game canvas where
   * the LayerManager window should appear, in terms of the
   * coordiantes of the game canvas.
   * @param y The y-coordinate of the place on the game canvas where
   * the LayerManager window should appear, in terms of the
   * coordiantes of the game canvas.
   * @param width the width of the region that is to be
   * occupied by the LayoutManager.
   * @param height the height of the region that is to be
   * occupied by the LayoutManager.
   */
  public JumpManager(int x, int y, int width, int height) {
    CANVAS_X = x;
    CANVAS_Y = y;
    DISP_WIDTH = width;
    DISP_HEIGHT = height;
    myCurrentLeftX = Grass.CYCLE*Grass.TILE_WIDTH;
    setViewWindow(0, 0, DISP_WIDTH, DISP_HEIGHT);
  }


  /**
   * sets all variables back to their initial positions.
   */
  void reset() {
    if(myGrass != null) {
      myGrass.reset();
    }
    if(myCowboy != null) {
      myCowboy.reset();
    }
    if(myLeftTumbleweeds != null) {
      for(int i = 0; i < myLeftTumbleweeds.length; i++) {
 myLeftTumbleweeds[i].reset();
      }
    }
    if(myRightTumbleweeds != null) {
      for(int i = 0; i < myRightTumbleweeds.length; i++) {
 myRightTumbleweeds[i].reset();
      }
    }
    myLeft = false;
    myCurrentLeftX = Grass.CYCLE*Grass.TILE_WIDTH;
  }


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


  /**
   * paint the game graphic on the screen.
   * initialization code is included here because some
   * of the screen dimensions are required for initialization.
   */
  public void paint(Graphics g) throws Exception {
    // create the player:
    if(myCowboy == null) {
      myCowboy = new Cowboy(myCurrentLeftX + DISP_WIDTH/2,
       DISP_HEIGHT – Cowboy.HEIGHT – 2);
      append(myCowboy);
    }
    // create the tumbleweeds to jump over:
    if(myLeftTumbleweeds == null) {
      myLeftTumbleweeds = new Tumbleweed[2];
      for(int i = 0; i < myLeftTumbleweeds.length; i++) {
 myLeftTumbleweeds[i] = new Tumbleweed(true);
 append(myLeftTumbleweeds[i]);
      }
    }
    if(myRightTumbleweeds == null) {
      myRightTumbleweeds = new Tumbleweed[2];
      for(int i = 0; i < myRightTumbleweeds.length; i++) {
 myRightTumbleweeds[i] = new Tumbleweed(false);
 append(myRightTumbleweeds[i]);
      }
    }
    // create the background object:
    if(myGrass == null) {
      myGrass = new Grass();
      append(myGrass);
    }
    // this is the main part of the method:
    // we indicate which rectangular region of the LayerManager
    // should be painted on the screen and then we paint
    // it where it belongs.  The call to paint() below
    // prompts all of the appended layers to repaint themselves.
    setViewWindow(myCurrentLeftX, 0, DISP_WIDTH, DISP_HEIGHT);
    paint(g, CANVAS_X, CANVAS_Y);
  }


  /**
   * If the cowboy gets to the end of the graphical region,
   * move all of the pieces so that the screen appears to wrap.
   */
  void wrap() {
    if(myCurrentLeftX % (Grass.TILE_WIDTH*Grass.CYCLE) == 0) {
      if(myLeft) {
 myCowboy.move(Grass.TILE_WIDTH*Grass.CYCLE, 0);
 myCurrentLeftX += (Grass.TILE_WIDTH*Grass.CYCLE);
 for(int i = 0; i < myLeftTumbleweeds.length; i++) {
   myLeftTumbleweeds[i].move(Grass.TILE_WIDTH*Grass.CYCLE, 0);
 }
 for(int i = 0; i < myRightTumbleweeds.length; i++) {
   myRightTumbleweeds[i].move(Grass.TILE_WIDTH*Grass.CYCLE, 0);
 }
      } else {
 myCowboy.move(-(Grass.TILE_WIDTH*Grass.CYCLE), 0);
 myCurrentLeftX -= (Grass.TILE_WIDTH*Grass.CYCLE);
 for(int i = 0; i < myLeftTumbleweeds.length; i++) {
   myLeftTumbleweeds[i].move(-Grass.TILE_WIDTH*Grass.CYCLE, 0);
 }
 for(int i = 0; i < myRightTumbleweeds.length; i++) {
   myRightTumbleweeds[i].move(-Grass.TILE_WIDTH*Grass.CYCLE, 0);
 }
      }
    }
  }


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


  /**
   * Tell all of the moving components to advance.
   * @param gameTicks the remainaing number of times that
   *        the main loop of the game will be executed
   *        before the game ends.
   * @return the change in the score after the pieces
   *         have advanced.
   */
  int advance(int gameTicks) {
    int retVal = 0;
    // first we move the view window
    // (so we are showing a slightly different view of
    // the manager’s graphical area.)
    if(myLeft) {
      myCurrentLeftX–;
    } else {
      myCurrentLeftX++;
    }
    // now we tell the game objects to move accordingly.
    myGrass.advance(gameTicks);
    myCowboy.advance(gameTicks, myLeft);
    for(int i = 0; i < myLeftTumbleweeds.length; i++) {
      retVal += myLeftTumbleweeds[i].advance(myCowboy, gameTicks,
      myLeft, myCurrentLeftX, myCurrentLeftX + DISP_WIDTH);
      retVal -= myCowboy.checkCollision(myLeftTumbleweeds[i]);
    }
    for(int i = 0; i < myLeftTumbleweeds.length; i++) {
      retVal += myRightTumbleweeds[i].advance(myCowboy, gameTicks,
           myLeft, myCurrentLeftX, myCurrentLeftX + DISP_WIDTH);
      retVal -= myCowboy.checkCollision(myRightTumbleweeds[i]);
    }
    // now we check if we have reached an edge of the viewable
    // area, and if so we move the view area and all of the
    // game objects so that the game appears to wrap.
    wrap();
    return(retVal);
  }


  /**
   * Tell the cowboy to jump..
   */
  void jump() {
    myCowboy.jump();
  }


}


 

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.