Sprite类的使用
Sprite实例(以下简称Sprite)都代表填充了一个图象的图形对象。仅仅包含一个图象是Sprite和TiledLayer类之间的本质区别,TiledLayer类的实例代表一个由多个可被操作的图象覆盖的区域(Sprite类有其他额外的特征,但每次它操作一个图象而不是用图象填充图形区域是最显而易见的特点)。所以Sprite通常被用来实现小的、活动的游戏对象(比如你的太空船和将与之相撞的小行星),而TiledLayer更多地被用来做活动的背景图形。Sprite一个很酷的特性是,尽管Sprite一次只能表示一个图象,但它能很容易地在不同情景下显示不同的图象,实现由一系列图象构成的动画效果。
样例游戏中,牛仔在跑动时有三个不同的姿势图象,在跳跃时还有另外一个。一个给定的Sprite需要的所有图象都应当保存在一个图象文件中(为了找到图象文件被调用的地方,Class.getResource()方法中将图象文件地址以相同的格式作为参数在jar文件中查找)。多帧图象保存在一个简单Image对象中显然让我们感觉轻松不少,因为我们不必操作不同的Image对象以便决定Sprite在一个给定时刻显示哪一个Image对象。风滚草图象文件包含三帧图象,当它们顺序显示时实现了一个滚动的动画效果。
在某一时刻应该选择哪一帧显示,这个很难计算,取决于你的感受。首先,如果你的图象文件包含了多个图象,你要用constructor来构建一个Sprite,定义你计划的Sprite的长度和宽度(用象素)。而Image的长度和宽度应当是你准备传到constructor的长度和宽度参数的整数倍。换句话说,计算机会均匀地将Image分割成若干按你的尺寸定义的矩形。正如前边的例子看到的那样,你可以在它的横向和纵向分别设置子图象,为了方便,你甚至可以在一个多行多列的网格中设置它们。然后,定义各个独立帧,对它们进行编号,顺序是从左上角顶点开始,然后向右,接着是下一行,就象你通常的阅读习惯一样。最后,选择某一帧显示的时候,直接将帧编号作为参数传入并调用setFrame(int sequenceIndex)方法就可以了。
Sprite类有一些为了实现动画效果的附加支持,比如允许你用setFrameSequence(int[] sequence) 方法定义帧的显示顺序。如Listing 5所示,我为牛仔定义了一个帧顺序{1,2,3,2},而风滚草是{0,1,2}(注意对于风滚草,我定义的顺序恰恰是默认的帧顺序,所以不用特意在代码中设置)。要想将活动的Sprite从某一帧移动到下一帧,需要调用nextFrame()(与之相反,可以用prevFrame())。这样处理对类似我的风滚草这样的动画效果显然是很方便的,所有的有效帧都会被用到,而对于牛仔的活动这样处理有点不便,因为只有一个Image或者Images都不在动画的帧顺序之内。这是因为一旦一个帧顺序被设置,传至setFrame(int sequenceIndex) 方法的参数只能表示用于帧顺序的索引,而不是帧本身的索引。那意味着一旦我设置好牛仔的帧顺序{3,2,1,2},如果我调用setFrame(0)方法,将返回帧编号1,setFrame(1)返回2,setFrame(2)返回3,而setFrame(3)返回2。但当牛仔跳起时,我想让它返回0,然而这一帧已经无效了。所以当我的牛仔跳起时,我必须在调用setFrame(0)前设置帧顺序为空(null),然后将帧顺序设置回{1,2,3,2}。Listing 5 jump()和advance(int tickCount,boolean left)方法中显示这个逻辑。
除了通过调用不同的显示帧改变你的Sprite显示外,你也可以通过直接的转换比如旋转或者镜象来展现不同的Sprite。Listing 5 和Listing 6样例中的牛仔和风滚草活动都是可左可右,所以我这里用了镜象转换。
一旦开始镜象转换,你必须首先保存Sprite参考象素的轨迹,因为你转换你的Sprite后,参考象素就是为改变前的象素。你可能以为如果你的Sprite图象是一个正方形,转换后图象将会呆在屏幕中同样的位置。事实不是这样。举例来说明。想象一个面朝左方的站立的人,他的参考象素可以定义为他的脚趾顶端。在旋转90度后,他所处的位置恰好就象他被拌倒向前摔倒后的姿势。很明显,转换后你的参考象素(脚趾顶端)的位置事先已被设置为和以前相同。如果你想让Sprite转换后继续占据屏幕上同样的区域,那么你应当首先调用defineReferencePixel(int x, int y)方法,并且设置你的Sprite参考象素为Sprite中心,正如我在Listing 5中对牛仔的设置那样。(另一个避免Sprite转换后失去位置的技巧是在转换前调用getX()和getY()方法获得Sprite左上角的绝对坐标,然后在转换后调用setPosition()设置左上角坐标为以前的位置。)
搞明白defineReferencePixel(int x, int y) 中引用的坐标与Sprite的顶端角相关,但是传至setRefPixelPosition(int x, int y) 告诉Sprite的参考坐标位置的坐标参数是根据屏幕坐标来的。更准确地说,如果Sprite是直接被绘制到Canvas,则传至setRefPixelPosition(int x, int y) 的坐标参数指的是Canvas的坐标系统,但如果Sprite是被一个LayerManager类绘制,这些参数应该根据LayerManager坐标系来确定。(这些坐标系统如何协调工作在前已有论述,请参考。)
在那些用来设置、获得参考象素或者设置、获得左上角位置象素的方法中,调用的坐标参数指的是对应的Canvas或者LayberManager坐标系上的坐标参数。还有需要注意的是如果你要进行多个转换,稍后的转换应用于它的前一个图象,而不是它的当前状态。换句话说,如果我对某一行应用了两次镜象转换setTransform(TRANS_MIRROR) ,第二次转换并不能将Image转换回最初的位置;它仅仅重复镜象转换的动作。如果你打算将一个转换后的Sprite返回到它转换前的位置,需要调用setTransform(TRANS_NONE) 方法,具体请参考Cowboy.advance(int tickCount, boolean left) 方法的前几行。
Layer类(包括Sprite和TiledLayer)的另一个伟大的特征是支持你用相对距离而不是绝对距离来设置对象位置。如果你的Sprite需要移动3个象素,无论它当前的位置坐标是多少,你只需调用move(int x,int y)方法就可以,而不是与之相反的调用setRefPixelPosition(int x, int y) 方法设置Sprite的新坐标来实现。更有用的是collidesWith()方法,它允许你检测是否一个Sprite抢占了另一个Sprite,或者TiledLayer,甚至Image的区域。这个方法的结果保存了许多比较后获得的参数,很容易看到,特别是当你传递一个象素级参数”true”后,如果他们的不透明象素有重叠之处,它将认为这两个图层发生冲突。
在风滚草游戏中,在所有Sprites向前移动后,我检测了是否牛仔与任何风滚草发生冲突(这发生在被JumpManager.advance(int gameTicks)调用的Cowboy.checkCollision(Tumbleweed tumbleweed)方法中)。当然因为如果当前的风滚草处于不可视状态,校验函数会返回False,所以我可以避免再花时间校验牛仔与这些不可视风滚草的冲突。除此以外,在很多情况下,通过只和可能发生冲突的对象进行校验,你还可以进一步提高程序的效率。注意样例游戏中我没有庸人自绕地检查是否风滚草彼此之间或者检查是否任何对象和背景草丛发生冲突,那显然与游戏无关。在你进行象素级别的冲突检测时,你要保证你的图象有一个透明的背景。(这通常也是很有用的,以避免你的Sprite不会用背景颜色在另一个Sprite或者Image上画一个丑陋的矩形)。
Listing 5 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 final int WIDTH = 32; /** * The height of the cowboy's bounding rectangle. */ static final int HEIGHT = 48; /** * This is the order that the frames should be displayed * for the animation. */ static final int[] FRAME_SEQUENCE = { 3, 2, 1, 2 }; //--------------------------------------------------------- // Instance fields /** * The X coordinate of the cowboy where the cowboy starts * the game. */ private int myInitialX; /** * The Y coordinate of the cowboy when not jumping. */ private int myInitialY; /** * The jump index that indicates that no jump is * currently in progress. */ private int myNoJumpInt = -6; /** * Where the cowboy is in the jump sequence. */ private 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. */ private int myScoreThisJump = 0; //--------------------------------------------------------- // Initialization /** * Constructor initializes the image and animation. */ public Cowboy(int initialX, int initialY) throws Exception { super(Image.createImage("/images/cowboy.png"), WIDTH, HEIGHT); myInitialX = initialX; myInitialY = initialY; // You 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, you 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 you 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 toward the left. setTransform(TRANS_MIRROR); move(-1, 0); } else { // Use the (normal, untransformed) image of the cowboy // graphic when the cowboy is going toward 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 you are in the jump: myIsJumping++; if(myIsJumping < 0) { // myIsJumping starts negative, and while it's // still negative, the cowboy is going up. // Here you 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, you reset the cowboy to // his nonjumping position: myIsJumping = myNoJumpInt; setRefPixelPosition(getRefPixelX(), myInitialY); // You 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, // you 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); } } |
Listing 6. 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 final int WIDTH = 16; //--------------------------------------------------------- // Instance fields /** * Random number generator to randomly decide when to appear. */ private Random myRandom = new Random(); /** * Whether this tumbleweed has been jumped over. * This is used to calculate the score. */ private boolean myJumpedOver; /** * Whether this tumbleweed enters from the left. */ private boolean myLeft; /** * The Y coordinate of the tumbleweed. */ private int myY; //--------------------------------------------------------- // Initialization /** * Constructor initializes the image and animation. * @param left Whether this tumbleweed enters from the left. */ public Tumbleweed(boolean left) throws Exception { super(Image.createImage("/images/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 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, // you 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 // so 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, you 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), you increase the // cowboy's score and set myJumpedOver to true // so that no further points will be awarded // for this tumbleweed until it goes off the screen // 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类中缺乏大多数功能性相关的方法;TiledLayer没有转换,参考象素,甚至帧顺序。
当然, 同时管理多个图象使事情变得复杂,我将用TiledLayer的子类Grass来说明TiledLayer类的使用。该类在屏幕背景上显示一排前后随风舞动的绿草。为了使画面更加有趣,某些列拥有舞动的绿草,而另一些是低矮的草丛,仅仅是一层覆盖地面的绿色。
创建TiledLayer的第一步是决定你需要的格子的行列数。如果你不想让你的图层看上去是简单的矩形,那不是问题,因为没有用到的网格默认设置为空,这就避免了它们和别的图象一样排列。在我的样例中,如Listing 7所示,我只安排了一行,而列数是根据屏幕的宽度计算得到。
一旦你设置了要用到的行数和列数,你就可以用一个饰片来填充网格,方法是setCell(int col, int row, int tileIndex) ,“Sprite类”章中解释了tileIndex参数。如果你希望某些网格被活动图象填充,你需要通过调用createAnimatedTile(int staticTileIndex)方法创建一个活动饰片,该方法将返回装饰片的索引给你的新的饰片。你尽可以多做几个活动饰片,但要记住,如果你想让网格同时显示同样的动画,必须每一个饰片都要能在多个网格中使用。
在我的例子中,我只创建了一个活动饰片并且一直重用它,因为我想让我的所有活动绿草同时摇曳。网格被设置在Listing 7 Grass的创建方法中。为让饰片动起来,你无须使用象Sprite用到的事先定义好的帧顺序,所以你必须通过setAnimatedTile(int animatedTileIndex, int staticTileIndex)方法设置帧。这个方法设置了当前所有包含给定活动饰片的帧,从而,所有包含该活动饰片的网格相对应的animatedTileIndex将被变成当前参数staticTileIndex指定的图象。为了使动画更改变得简单,增加自己的帧顺序功能是必要的;请参考Grass.advace(int tickCount)方法看这个功能是如何实现的。
Listing 7. 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 final int TILE_WIDTH = 20; /** * This is the order that the frames should be displayed * for the animation. */ static final 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 final int CYCLE = 5; /** * The fixed Y coordinate of the strip of grass. */ static int TOP_Y; //--------------------------------------------------------- // Instance fields /** * Which tile you are currently on in the frame sequence. */ private int mySequenceIndex = 0; /** * The index to use in the static tiles array to get the * animated tile. */ private 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("/images/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. * @param left Whether the player is moving left. */ void advance(int tickCount) { if(tickCount % 2 == 0) { // Slow the animation down a little. mySequenceIndex++; mySequenceIndex %= 4; setAnimatedTile(myAnimatedTileIndex, FRAME_SEQUENCE[mySequenceIndex]); } } } |
现在,你已经完整地看到了一个简单但是基础的游戏,它说明了如何使用javax.microedition.lcdui.game包中的所有类。风滚草样例游戏还特别展示了如何充分利用MIDP2.0的图形和动画特性