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

The Sprite Class


A Sprite is a graphical object represented by one image (at a time). The fact that a Sprite is composed of only one image is the principal difference between a Sprite and a TiledLayer, which is a region that is covered with images that can be manipulated. (The Sprite class has a few extra features, but the fact that it uses one image rather than filling an area with images is the most obvious difference.) So a Sprite is generally used for small, active game objects (such as your spaceship and the asteroids that are coming to crash into it) and a TiledLayer would be more likely to be used for an animated background.

One cool feature of Sprite is that even though a Sprite is represented by only one image at a time, it can be easily represented by different images under different circumstances, including by a series of images that make up an animation. In my example game the cowboy has three different images in which he is walking and one in which he is jumping. All of the images used for a given Sprite need to be stored together in a single image file. (To indicate where to find the image file to use, send the address of the image within the jar in exactly the same format that is used to find resources in the method Class.getResource(); see the Putting it Together section for more details.) The fact that multiple frames are stored in a single Image object is a convenience that means you don’t have to manipulate multiple Image objects to determine which face your Sprite is wearing at any given time. Here’s the image file for the cowboy:

 


And here is the image file for the tumbleweed which is animated to appear to be rolling:

 
The way to select which frame is shown at any given time is very intuitive. First, if your image file is composed of multiple images (as these two are), you should construct the Sprite with the constructor that specifies the width and height (in pixels) that you would like your Sprite to be. The width and height of the Image should be integer multiples of the width and height that you send to the constructor. In other words, the computer should be able to divide up your image file evenly into rectangles of the size you specify. As you can see from the examples above, the sub-images may be arranged horizontally or vertically. They can even be arranged in a grid with multiple rows and columns. Then to identify the individual frames, they are numbered starting with zero at the top left corner and continuing to the right and then continuing on to the lower rows, in exactly the same order in which you are reading the letters on this page. To select which frame is currently displayed, use the method setFrame(int sequenceIndex) sending the frame number as an argument.

The Sprite class has some added support for animation by allowing you to define a frame sequence with the method setFrameSequence(int[] sequence). As you can see in the code below, I’ve set a frame sequence of { 1, 2, 3, 2 } for my cowboy and { 0, 1, 2 } for my tumbleweed. To advance your Sprite’s animation from one frame to the next, you use the method nextFrame() (or if you prefer prevFrame()). This is very convenient in cases like my tumbleweed where all of the available frames are used in the animation. It’s slightly less convenient in cases like my cowboy that have an image or images that fall outside of the frame sequence of the animation. This is because once a frame sequence has been set, the argument to the method setFrame(int sequenceIndex) gives the index of an entry in the frame sequence instead of giving the index of the frame itself. What that means is that once I’ve set my cowboy’s frame sequence to { 1, 2, 3, 2 }, if I call setFrame(0) it will show frame number 1, setFrame(1) will show frame number 2, setFrame(2) will show frame number 3, and setFrame(3) will show frame number 2. But when the cowboy is jumping, I would like it to show frame number 0 which is no longer accessible. So when my cowboy jumps, I have to set my frame sequence to null before calling setFrame(0), and then I have to set my frame sequence back to the animation sequence { 1, 2, 3, 2 } afterwards. This is illustrated in the code below in the methods jump() and advance(int tickCount, boolean left).

In addition to changing your Sprite’s appearance by changing frames, you can change it by applying simple transforms such as rotations or mirror images. Both my cowboy and my tumbleweed below can be going either left or right, so of course I need to use the mirror image transform to change from one direction to the other. Once you start applying transforms, you need to keep track of the Sprite’s reference pixel. This is because when you transform your Sprite, the reference pixel is the pixel that does not move. You might expect that if your Sprite’s image is square then after a transformation the Sprite’s image will continue to occupy the same square of area on the screen. This is not the case. The best way to illustrate what happens is to imagine an example Sprite of a standing person facing left whose reference pixel has been defined to be the tip of his toe. Then after applying a 90-degree rotation, your person will be in the spot he would be in if he had tripped and fallen forward. Clearly this has its applications if your Sprite has a special pixel (such as the tip of an arrow) that should stay put after a transformation. But if you want your Sprite to continue to occupy the same space on the screen after a transformation, then you should first call defineReferencePixel(int x, int y) and set your Sprite’s reference pixel to the center of the Sprite, as I did in the Cowboy constructor below. Be aware that the coordinates in defineReferencePixel(int x, int y) are relative to the top corner of the Sprite whereas the coordinates sent to setRefPixelPosition(int x, int y) tell where to place the Sprite’s reference pixel on the screen in terms of the screen’s coordinates. To be more precise, the coordinates sent to setRefPixelPosition(int x, int y) refer to the coordinate system of the Canvas if the Sprite is painted directly onto the Canvas, but if the Sprite is painted by a LayerManager, these coordinates should be given in terms of the LayerManager’s coordinate system. (How these coordinate systems fit together is explained in the LayerManager section above.) The coordinates in the various methods to set and get the position of the reference pixel or to set and get the position of the top left corner pixel refer to the coordinates of the appropriate Canvas or LayerManager. Also note that if you perform multiple transformations, the later trasformations are applied to the original image and not to its current state. In other words, if I apply setTransform(TRANS_MIRROR) twice in a row, the second transform will not mirror my image back to its original position, it will just repeat the action of setting the Image to being a mirror image of the original Image. If you want to set a transformed Sprite back to normal, use setTransform(TRANS_NONE). This is illustrated in the top of the Cowboy.advance(int tickCount, boolean left) method.

Another great feature of the Layer class (including both Sprites and TiledLayers) is the support it gives you for placing your objects in relative terms instead of in absolute terms. If your Sprite needs to move over three pixels regardless of where it currently is, you can just call move(int x, int y) sending it the x and y distances it should move from its current position, as opposed to calling setRefPixelPosition(int x, int y) with the absolute coordinates of the Sprite’s new location. Even more useful is the set of collidesWith() methods. This allows you to check if a Sprite is occupying the same space as another Sprite or TiledLayer or even an Image. It’s easy to see that this saves you quite a number of comparisons, especially since when you send the pixelLevel argument as true, it will only consider the two Layers as having collided if their opaque pixels overlap.

In my “Tumbleweed” game, after advancing all of the Sprites, I check if the cowboy has collided with any tumbleweeds. (This happens in the Cowboy.checkCollision(Tumbleweed tumbleweed) method which is called from JumpManager.advance(int gameTicks).) I check the collisions between the cowboy and all of the tumbleweeds each time because it automatically returns false for any tumbleweeds that aren’t currently visible anyway, so I’m not really being wasteful by checking the cowboy against tunbleweeds that are not currently in use. In many cases, however, you can save some effort by only checking for collisions that you know are possible rather than checking all of the Sprites against each other. Note that in my example I don’t bother to check if the tumbleweeds collide with each other or if anything collides with the background grass because that is irrelevant. If you’re checking for pixel-level collisions, you will want to be sure that your images have a transparent background. (This is also helpful in general so that your Sprite doesn’t paint an ugly rectangle of background color over another Sprite or Image.) Some discussion of creating the image files correctly is included in Appendix B.

Here’s the code for Cowboy.java:

package net.frog_parrot.jump;

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

/**
 * This class represents the player.
 *
 * @author Carol Hamer
 */
public class Cowboy extends Sprite {

  //———————————————————
  //    dimension fields

  /**
   * The width of the cowboy’s bounding rectangle.
   */
  static int WIDTH = 32;

  /**
   * The height of the cowboy’s bounding rectangle.
   */
  static int HEIGHT = 48;

  /**
   * This is the order that the frames should be displayed
   * for the animation.
   */
  static int[] FRAME_SEQUENCE = { 3, 2, 1, 2 };

  //———————————————————
  //    instance fields

  /**
   * the X coordinate of the cowboy where the cowboy starts
   * the game.
   */
  int myInitialX;

  /**
   * the Y coordinate of the cowboy when not jumping.
   */
  int myInitialY;

  /**
   * The jump index that indicates that no jump is
   * currently in progress..
   */
  int myNoJumpInt = -6;

  /**
   * Where the cowboy is in the jump sequence.
   */
  int myIsJumping = myNoJumpInt;

  /**
   * If the cowboy is currently jumping, this keeps track
   * of how many points have been scored so far during
   * the jump.  This helps the calculation of bonus points since
   * the points being scored depend on how many tumbleweeds
   * are jumped in a single jump.
   */
  int myScoreThisJump = 0;

  //———————————————————
  //   initialization

  /**
   * constructor initializes the image and animation.
   */
  public Cowboy(int initialX, int initialY) throws Exception {
    super(Image.createImage(“/icons/cowboy.png”),
   WIDTH, HEIGHT);
    myInitialX = initialX;
    myInitialY = initialY;
    // we define the reference pixel to be in the middle
    // of the cowboy image so that when the cowboy turns
    // from right to left (and vice versa) he does not
    // appear to move to a different location.
    defineReferencePixel(WIDTH/2, 0);
    setRefPixelPosition(myInitialX, myInitialY);
    setFrameSequence(FRAME_SEQUENCE);
  }

  //———————————————————
  //   game methods

  /**
   * If the cowboy has landed on a tumbleweed, we decrease
   * the score.
   */
  int checkCollision(Tumbleweed tumbleweed) {
    int retVal = 0;
    if(collidesWith(tumbleweed, true)) {
      retVal = 1;
      // once the cowboy has collided with the tumbleweed,
      // that tumbleweed is done for now, so we call reset
      // which makes it invisible and ready to be reused.
      tumbleweed.reset();
    }
    return(retVal);
  }

  /**
   * set the cowboy back to its initial position.
   */
  void reset() {
    myIsJumping = myNoJumpInt;
    setRefPixelPosition(myInitialX, myInitialY);
    setFrameSequence(FRAME_SEQUENCE);
    myScoreThisJump = 0;
    // at first the cowboy faces right:
    setTransform(TRANS_NONE);
  }

  //———————————————————
  //   graphics

  /**
   * alter the cowboy image appropriately for this frame..
   */
  void advance(int tickCount, boolean left) {
    if(left) {
      // use the mirror image of the cowboy graphic when
      // the cowboy is going towards the left.
      setTransform(TRANS_MIRROR);
      move(-1, 0);
    } else {
      // use the (normal, untransformed) image of the cowboy
      // graphic when the cowboy is going towards the right.
      setTransform(TRANS_NONE);
      move(1, 0);
    }
    // this section advances the animation:
    // every third time through the loop, the cowboy
    // image is changed to the next image in the walking
    // animation sequence:
    if(tickCount % 3 == 0) { // slow the animation down a little
      if(myIsJumping == myNoJumpInt) {
 // if he’s not jumping, set the image to the next
 // frame in the walking animation:
 nextFrame();
      } else {
 // if he’s jumping, advance the jump:
 // the jump continues for several passes through
 // the main game loop, and myIsJumping keeps track
 // of where we are in the jump:
 myIsJumping++;
 if(myIsJumping < 0) {
   // myIsJumping starts negative, and while it’s
   // still negative, the cowboy is going up. 
   // here we use a shift to make the cowboy go up a
   // lot in the beginning of the jump, and ascend
   // more and more slowly as he reaches his highest
   // position:
   setRefPixelPosition(getRefPixelX(),
         getRefPixelY() – (2 << (-myIsJumping)));
 } else {
   // once myIsJumping is negative, the cowboy starts
   // going back down until he reaches the end of the
   // jump sequence:
   if(myIsJumping != -myNoJumpInt – 1) {
     setRefPixelPosition(getRefPixelX(),
    getRefPixelY() + (2 << myIsJumping));
   } else {
     // once the jump is done, we reset the cowboy to
     // his non-jumping position:
     myIsJumping = myNoJumpInt;
     setRefPixelPosition(getRefPixelX(), myInitialY);
     // we set the image back to being the walking
     // animation sequence rather than the jumping image:
     setFrameSequence(FRAME_SEQUENCE);
     // myScoreThisJump keeps track of how many points
     // were scored during the current jump (to keep
     // track of the bonus points earned for jumping
     // multiple tumbleweeds).  Once the current jump is done,
     // we set it back to zero. 
     myScoreThisJump = 0;
   }
 }
      }
    }
  }

  /**
   * makes the cowboy jump.
   */
  void jump() {
    if(myIsJumping == myNoJumpInt) {
      myIsJumping++;
      // switch the cowboy to use the jumping image
      // rather than the walking animation images:
      setFrameSequence(null);
      setFrame(0);
    }
  }

  /**
   * This is called whenever the cowboy clears a tumbleweed
   * so that more points are scored when more tumbleweeds
   * are cleared in a single jump.
   */
  int increaseScoreThisJump() {
    if(myScoreThisJump == 0) {
      myScoreThisJump++;
    } else {
      myScoreThisJump *= 2;
    }
    return(myScoreThisJump);
  }

}

 

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.