一步一步学习midp2。0游戏编程(五)

Sprite(精灵类)

Sprite是用个用来显示图像的类 (每次一个). 该类和TiledLayer的区别是。Sprite是由一个图像(可以有好几帧,但是一次只有一个显示)组成 (当然Sprite还有其他的额外特性,不过每次只能只用一个图像而不是一次使用多个图像来填充屏幕是它的最主要特征.) 因而Sprite 被用来定义一些小的有动作的游戏对象(比如飞船和小行星对撞), TiledLayer 更被常用来构造生动的背景.

Sprite另一个让人激动的特性是:虽然它一次只能显示一个图像,但是我们可以为它定义一系列的图像,以便在不同的环境下构造不同的动画. 在我的例子中,牛仔有三帧图像描绘它的走路,一帧描绘牛仔的跳. 这些将要赋值给Sprite的图像需要被存在一个png文件中.这些帧被存在一个文件中的好处是你可以从管理多个Image对象的麻烦中解脱出来。下面是牛仔类的图片:

在某个特定的时刻决定显示哪一帧帧是非常直观的. 第一, 如果你的图像文件包括多个帧, 在构造Sprite 类的时候你需要指定精灵的高度和宽度(象素值). 图像的高度和宽度必须是精灵高宽的整数倍.换句话说,你要能正好让电脑把你的图像按照精灵的大小划分成几个类. 通过上面的例子你也看到了,至于这些帧是横着拍还是竖着排抑或横竖都有排成一个方阵都无所谓. 接着就可以指定帧数了, 左上放是编号0,然后从左到右,从上到下依次排列。你可以使用setFrame(int sequenceIndex) 选择哪一帧被显示,是要把它的编号作为参数传递就成了.

Sprite还可以让你使用方法setFrameSequence(int[] sequence)定义一个帧的序列.比如我可以为牛仔定义这么一个序列 { 1, 2, 3, 2 }而为滚草定义 { 0, 1, 2 } 序列. 使用nextFrame(),就可以让你的Sprite 动画前进了。(后退的话使用prevFrame()).对于象滚草这样使用了全部帧的情况的确很方便,不过对于象牛仔这样有的帧没有使用的情况就稍微有点复杂了。这是因为一个序列一旦被设置, setFrame(int sequenceIndex) 方法中的参数对应的帧数变成了序列数组中对应的参数而不是图片本身参数的顺序。比如我把牛仔的序列设置为 { 1, 2, 3, 2 },如果我调用setFrame(0)的话,序号为1的那帧被显示, setFrame(3)将会显示2. 因而如果让人物跳起的动画对应的第0帧就没有办法引用了.所以当我需要牛仔跳起时,我需要设置序列为null,然后再调用setFrame(0),接着再把序列设置成 { 1, 2, 3, 2 }. 你可以参考下面代码中的jump() 和 advance(int tickCount, boolean left).

当帧改变时,为了改变精灵的外观,你可对它去旋转或者镜像的操作. 牛仔和滚草都可以向左或者向右运动,所以我需要对图像进行镜像变换以得到相反方向的动作. 当你开始变换成,你需要保留住精灵的引用象素. 这是因为如果你变换你的精灵的时候, 引用象素是一个不变的象素. 你可以设想,你的精灵图像是一个矩形,当它进行变换以后,仍然是原来位置上的那个矩形。这是不一定的。为了说明.我们来设想一个例子,有一个向左站的人,他的引用象素在脚趾上对他进行90度旋转以后, 你会发现他向前倒在地上了.很明显是引用象素发生了作用 (好像一个图钉).如果你希望变换后仍然占有相同的矩形话,你需要首先调用 defineReferencePixel(int x, int y) 把你的精灵的引用象素设置成矩形的中心, 就想我在下面代码的构造方法中干的那样. 要注意 defineReferencePixel(int x, int y)中的坐标 是相对于精灵的左上角,然而setRefPixelPosition(int x, int y)使用的是整个屏幕的坐标系统. 更准确的说,被送到 setRefPixelPosition(int x, int y)中的坐标用的是精灵将被直接绘制到的Canvas的坐标系统, 但是如果精灵通过 LayerManager进行绘制, 那么使用的就是LayerManager的坐标系统了. (这些坐标的关系在在 LayerManager那部分已经讲解过了) 要特别注意的是,如果你对一个精灵图像进行了多重变化的话,后面的变化是相对原始状态而不是当前状态. 换句话说,如果你调用了两次 setTransform(TRANS_MIRROR)和调用一次的效果是一样的,而不是想你想象的那样再变换回去. 如果你想让你的变型还原,调用setTransform(TRANS_NONE). 在 Cowboy.advance(int tickCount, boolean left) 中有具体演示.

Layer(包括Sprites TiledLayers) 类的另外一个灵光人激动的特性就是可以让你把你的对象放置到一个对象之间相互关联的体系中而不是一个彼此没有联系的绝对体系. 如果你想让你的精灵Sprite 移动3个象素而不管它现在所处的环境的话, 你可以调用move(int x, int y) ,其中x,y分别代表这两个方向上的偏移量, 而不像 setRefPixelPosition(int x, int y)那样使用绝对坐标来定义精灵的新的位置. collidesWith()也是非常有用.他可以检测一个精灵是否和另外一个精灵或者TiledLayers 或者一个Image发生碰撞. 特别是如果你把pixelLevel参数设置成true的话,他会检测是不是两个不透明发生碰撞,而如果仅仅这两者的透明发生碰撞就被忽略。

在 "Tumbleweed" 游戏中, 让所有的Sprites播放后,我会检查牛仔是不是和滚草发生了碰撞. (具体检测在 Cowboy.checkCollision(Tumbleweed tumbleweed)方法中,JumpManager.advance(int gameTicks)会调用它)。我会检查牛仔所有滚草之间的碰撞,除了那些不可见的滚草,因为它们肯定会返回false. 在我的例子中,为了省点力气 你可以只检测那些有可能发生碰撞的物体而不是所有的物体两两之间都要检测。你可以注意到,在我的例子中我没有检测滚草之间的碰撞以及背景草皮的碰撞,因为那些碰撞和游戏的逻辑不太相关。如果你在检查一个象素级别的碰撞,你得确认你的图片有透明背景. (透明是很重要的,它让你可以在绘制图片的时候不必把那个那个丑陋的带有背景颜色的矩形也画出来)至于 怎么正确的创建图片参见附录 B.

这是 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);
  }
 
}
 
下面是Tumbleweed.java的代码:

package net.frog_parrot.jump;
 
import java.util.Random;
 
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
 
/**
 * This class represents the tumbleweeds that the player
 * must jump over.
 *
 * @author Carol Hamer
 */
public class Tumbleweed extends Sprite {
 
  //---------------------------------------------------------
  //   dimension fields
 
  /**
   * The width of the tumbleweed's bounding square.
   */
  static int WIDTH = 16;
 
  //---------------------------------------------------------
  //    instance fields
 
  /**
   * Random number generator to randomly decide when to appear.
   */
  Random myRandom = new Random();
 
  /**
   * whether or not this tumbleweed has been jumped over.
   * This is used to calculate the score.
   */
  boolean myJumpedOver;
 
  /**
   * whether or not this tumbleweed enters from the left.
   */
  boolean myLeft;
 
  /**
   * the Y coordinate of the tumbleweed.
   */
  int myY;
 
  //---------------------------------------------------------
  //   initialization
 
  /**
   * constructor initializes the image and animation.
   * @param left whether or not this tumbleweed enters from the left.
   */
  public Tumbleweed(boolean left) throws Exception {
    super(Image.createImage("/icons/tumbleweed.png"),
   WIDTH, WIDTH);
    myY = JumpManager.DISP_HEIGHT - WIDTH - 2;
    myLeft = left;
    if(!myLeft) {
      setTransform(TRANS_MIRROR);
    }
    myJumpedOver = false;
    setVisible(false);
  }
 
  //---------------------------------------------------------
  //   graphics
 
  /**
   * move the tumbleweed back to its initial (inactive) state.
   */
  void reset() {
    setVisible(false);
    myJumpedOver = false;
  }
 
  /**
   * alter the tumbleweed image appropriately for this frame..
   * @param left whether or not the player is moving left
   * @return how much the score should change by after this
   *         advance.
   */
  int advance(Cowboy cowboy, int tickCount, boolean left,
       int currentLeftBound, int currentRightBound) {
    int retVal = 0;
    // if the tumbleweed goes outside of the display
    // region, set it to invisible since it is
    // no longer in use.
    if((getRefPixelX() + WIDTH <= currentLeftBound) ||
       (getRefPixelX() - WIDTH >= currentRightBound)) {
      setVisible(false);
    }
    // If the tumbleweed is no longer in use (i.e. invisible)
    // it is given a 1 in 100 chance (per game loop)
    // of coming back into play:
    if(!isVisible()) {
      int rand = getRandomInt(100);
      if(rand == 3) {
 // when the tumbleweed comes back into play,
 // we reset the values to what they should
 // be in the active state:
 myJumpedOver = false;
 setVisible(true);
 // set the tumbleweed's position to the point
 // where it just barely appears on the screen
 // to that it can start approaching the cowboy:
 if(myLeft) {
   setRefPixelPosition(currentRightBound, myY);
   move(-1, 0);
 } else {
   setRefPixelPosition(currentLeftBound, myY);
   move(1, 0);
 }
      }
    } else {
      // when the tumbleweed is active, we advance the
      // rolling animation to the next frame and then
      // move the tumbleweed in the right direction across
      // the screen.
      if(tickCount % 2 == 0) { // slow the animation down a little
 nextFrame();
      }
      if(myLeft) {
 move(-3, 0);
 // if the cowboy just passed the tumbleweed
 // (without colliding with it) we increase the
 // cowboy's score and set myJumpedOver to true
 // so that no further points will be awarded
 // for this tumbleweed until it goes offscreen
 // and then is later reactivated:
 if((! myJumpedOver) &&
    (getRefPixelX() < cowboy.getRefPixelX())) {
   myJumpedOver = true;
   retVal = cowboy.increaseScoreThisJump();
 }
      } else {
 move(3, 0);
 if((! myJumpedOver) &&
    (getRefPixelX() > cowboy.getRefPixelX() + Cowboy.WIDTH)) {
   myJumpedOver = true;
   retVal = cowboy.increaseScoreThisJump();
 }
      }
    }
    return(retVal);
  }
 
  /**
   * Gets a random int between
   * zero and the param upper.
   */
  public int getRandomInt(int upper) {
    int retVal = myRandom.nextInt() % upper;
    if(retVal < 0) {
      retVal += upper;
    }
    return(retVal);
  }
 
}
 TiledLayer 类

前面已经说过了,  TiledLayer 和Sprite是非常类似的,除了TiledLayer 可以由一些重复的图像单元拼接而成. 两者之间的另外一个区别就是TiledLayer不能变型,引用象素,或者使用一系列图片中的一帧.

常识告诉我们,同时管理这么多的图像会使事情变得稍微复杂一些. 我将结合名为Grass的 TiledLayer类来进行讲解. 这个类实现了游戏进行时让背景上的草前后摆动的功能. 为了让这个例子有趣一些,我定义了两种背景,一个是可以摇摆的草,另一个是底层固定不变的绿色背景. 摇摆的草的图像如下:


注意: Sprite的编号是从0开始的, 但是 TiledLayer却是从1开始的 (我一开始因为没注意的问题得到了个IndexOutOfBoundsException 异常).在TiledLayer中,序号0表示一个空白的元素 (比如在某个位置你什么都不想画,那就把他设置成0). Sprite只有一个单元组成, 所以如果你向让它不显示这个单元,简单的设置成 setVisible(false)就可以了, 因而Sprite不需要一个特殊的编号来表示空白的单元. 序号的不一致应该不是什么大问题,不过如果你的动画显示不正确,我到建议你查查是不是序号用错了.

创建 TiledLayer的第一步是决定你需要定义几行几列. 如果你不希望的层被定义成一个矩形也是有办法 ,所有不用的单元会默认的设置为空白.在我的例子中,我把行数定义为1,并且根据屏幕的宽度计算列数.

一旦你定义了行列的规模,你就可以使用 setCell(int col, int row, int tileIndex)来定义任何一个单元的内容了. tileIndex 参数在Sprite 部分已经讲过了,并且上文我用了一段来说明两个类中index的区别. 如果你希望某个元素有动画效果,你需要使用 createAnimatedTile(int staticTileIndex)来定义一个动画元素, 这个方法将会返回一个分配给你的那个动画元素的编号. 你可以创建足够多的动画元素,但是如果你需要在屏幕上同时显示它们的时候,请保证它们都可用. 在我的例子中,我就创建了一个动画元素,我在例子中重复使用它们,因为我希望这些草同步舞动. 为了播放动画,你不需要想Sprite那样得到内置的帧序列,你只要使用setAnimatedTile(int animatedTileIndex, int staticTileIndex)方法就可以了. 这个方法为指定的元素设置当前帧,因而所有包含这个动画元素的单元的当前帧会随之变成刚刚指定的帧。参看一下Grass.advance(int tickCount)方法的代码,你会更好的掌握

这是我们最后一个类了, Grass.java:

package net.frog_parrot.jump;
 
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
 
/**
 * This class draws the background grass.
 *
 * @author Carol Hamer
 */
public class Grass extends TiledLayer {
 
  //---------------------------------------------------------
  //    dimension fields
  //  (constant after initialization)
 
  /**
   * The width of the square tiles that make up this layer..
   */
  static int TILE_WIDTH = 20;
 
  /**
   * This is the order that the frames should be displayed
   * for the animation.
   */
  static int[] FRAME_SEQUENCE = { 2, 3, 2, 4 };
 
  /**
   * This gives the number of squares of grass to put along
   * the bottom of the screen.
   */
  static int COLUMNS;
 
  /**
   * After how many tiles does the background repeat.
   */
  static int CYCLE = 5;
 
  /**
   * the fixed Y coordinate of the strip of grass.
   */
  static int TOP_Y;
 
  //---------------------------------------------------------
  //    instance fields
 
  /**
   * Which tile we are currently on in the frame sequence.
   */
  int mySequenceIndex = 0;
 
  /**
   * The index to use in the static tiles array to get the
   * animated tile..
   */
  int myAnimatedTileIndex;
 
  //---------------------------------------------------------
  //   gets / sets
 
  /**
   * Takes the width of the screen and sets my columns
   * to the correct corresponding number
   */
  static int setColumns(int screenWidth) {
    COLUMNS = ((screenWidth / 20) + 1)*3;
    return(COLUMNS);
  }
 
  //---------------------------------------------------------
  //   initialization
 
  /**
   * constructor initializes the image and animation.
   */
  public Grass() throws Exception {
    super(setColumns(JumpCanvas.DISP_WIDTH), 1,
   Image.createImage("/icons/grass.png"),
   TILE_WIDTH, TILE_WIDTH);
    TOP_Y = JumpManager.DISP_HEIGHT - TILE_WIDTH;
    setPosition(0, TOP_Y);
    myAnimatedTileIndex = createAnimatedTile(2);
    for(int i = 0; i < COLUMNS; i++) {
      if((i % CYCLE == 0) || (i % CYCLE == 2)) {
 setCell(i, 0, myAnimatedTileIndex);
      } else {
 setCell(i, 0, 1);
      }
    }
  }
 
  //---------------------------------------------------------
  //   graphics
 
  /**
   * sets the grass back to its initial position..
   */
  void reset() {
    setPosition(-(TILE_WIDTH*CYCLE), TOP_Y);
    mySequenceIndex = 0;
    setAnimatedTile(myAnimatedTileIndex, FRAME_SEQUENCE[mySequenceIndex]);
  }
 
  /**
   * alter the background image appropriately for this frame..
   */
  void advance(int tickCount) {
    if(tickCount % 2 == 0) { // slow the animation down a little
      mySequenceIndex++;
      mySequenceIndex %= 4;
      setAnimatedTile(myAnimatedTileIndex, FRAME_SEQUENCE[mySequenceIndex]);
    }
  }
 
}
打包

(译者:从这往下的内容对大部分读者不太实用,大阿家参考吧)

当我们拥有了所有需要的类后我们就需要编译它了,方法和前面提到的 "Hello World" 例子一样. 这些类需要编译,预处理,打包. (预处理是一个使字节码更容易被虚拟机【原文说是j2me application】确认的额外步骤,不过你不要要过多的担心这个,因为一般的工具都会自动帮你处理这步) 要确定所有的资源文件被放置到jar包中正确的位置. 这个例子中,我需要把cowboy.png, tumbleweed.png, and grass.png I放到一个名为icons的顶层目录中,以便我使用类似下面的这行代码来调用它们 Image.createImage("/icons/grass.png").

另外还必须放置把MANIFEST.MF文件放到正确的位置:

MIDlet-1: Hello World, /icons/hello.png, net.frog_parrot.hello.Hello
MIDlet-2: Tumbleweed, /icons/boot.png, net.frog_parrot.jump.Jump
MMIDlet-Description: Example games for MIDP
MIDlet-Name: Example Games
MIDlet-Permissions:
MIDlet-Vendor: frog-parrot.net
MIDlet-Version: 2.0
MicroEdition-Configuration: CLDC-1.0
MicroEdition-Profile: MIDP-2.0
如果你想使用你自定义的 MANIFEST.MF代替自动生成的文件,在使用jar命令的时候要把加上m选项.

另外你还需要一个jad文件,jump.jad是一个例子:

MIDlet-1: Hello, /icons/hello.png, net.frog_parrot.hello.Hello
MIDlet-2: Tumbleweed, /icons/boot.png, net.frog_parrot.jump.Jump
MMIDlet-Description: Example games for MIDP
MIDlet-Jar-URL: jump.jar
MIDlet-Name: Example Games
MIDlet-Permissions:
MIDlet-Vendor: frog-parrot.net
MIDlet-Version: 2.0
MicroEdition-Configuration: CLDC-1.0
MicroEdition-Profile: MIDP-2.0
MIDlet-Jar-Size: 17259
Don't forget to verify that the MIDlet-Jar-Size is correct if you're creating the jad file by hand. Also note that this jad file indicates that your jar file needs to contain the icon /icons/boot.png which is used in the game selection menu and looks like this:


当 jar 和 jad 都准备好了, 你就可以运行程序了. 我是使用WTK2自带的runmidlet 的脚本来运行的.

好运!