MIDP2.0(Mobile Internet Device Profile)技术进行游戏开发中用到的最重要的包是:javax.microedition.lcdui.game,本文通过对样例游戏Tumbleweed的代码分析,将展示MIDP2.0技术的几个主要部分。游戏的主要情景是一个牛仔跳着闪避风滚草,这是一个简单的游戏,但你可以从中学到将来写复杂游戏必须具备的大部分基础知识。
从MIDlet类开始
象通常一样,应用从MIDlet类开始。本例中,我的MIDlet子类是Jump。Jump类几乎是完全继承的MIDIet子类,唯一的不同是用到了另一个独立类GameThread,用来动态设置当前窗口允许的有效按钮,比如当游戏处于非暂停状态时,玩家才可以使用暂停这个有效按钮,而激活按钮有效则是在游戏处于暂停状态时候,与此类似游戏停止后,开始按钮才是有效按钮。
Listing 1是游戏的MIDlet子类——Jump.java源码。
Listing 1. Jump.java
package net.frog_parrot.jump; import javax.microedition.midlet.*; import javax.microedition.lcdui.*; /** * This is the main class of the Tumbleweed game. * * @author Carol Hamer */ public class Jump extends MIDlet implements CommandListener { //--------------------------------------------------------- // Commands /** * The command to end the game. */ private Command myExitCommand = new Command("Exit", Command.EXIT, 99); /** * The command to start moving when the game is paused. */ private Command myGoCommand = new Command("Go", Command.SCREEN, 1); /** * The command to pause the game. */ private Command myPauseCommand = new Command("Pause", Command.SCREEN, 1); /** * The command to start a new game. */ private Command myNewCommand = new Command("Play Again", Command.SCREEN, 1); //--------------------------------------------------------- // Game object fields /** * The canvas that all of the game will be drawn on. */ private JumpCanvas myCanvas; /** * The thread that advances the cowboy. */ private GameThread myGameThread; //----------------------------------------------------- // Initialization and game state changes /** * Initialize the canvas and the commands. */ public Jump() { try { myCanvas = new JumpCanvas(this); myCanvas.addCommand(myExitCommand); myCanvas.addCommand(myPauseCommand); myCanvas.setCommandListener(this); } catch(Exception e) { errorMsg(e); } } /** * Switch the command to the play again command. */ void setNewCommand () { myCanvas.removeCommand(myPauseCommand); myCanvas.removeCommand(myGoCommand); myCanvas.addCommand(myNewCommand); } /** * Switch the command to the go command. */ private void setGoCommand() { myCanvas.removeCommand(myPauseCommand); myCanvas.removeCommand(myNewCommand); myCanvas.addCommand(myGoCommand); } /** * Switch the command to the pause command. */ private void setPauseCommand () { myCanvas.removeCommand(myNewCommand); myCanvas.removeCommand(myGoCommand); myCanvas.addCommand(myPauseCommand); } //---------------------------------------------------------------- // Implementation of MIDlet. // These methods may be called by the application management // software at any time, so you always check fields for null // before calling methods on them. /** * Start the application. */ public void startApp() throws MIDletStateChangeException { if(myCanvas != null) { if(myGameThread == null) { myGameThread = new GameThread(myCanvas); myCanvas.start(); myGameThread.start(); } else { myCanvas.removeCommand(myGoCommand); myCanvas.addCommand(myPauseCommand); myCanvas.flushKeys(); myGameThread.resumeGame(); } } } /** * Stop and throw out the garbage. */ public void destroyApp(boolean unconditional) throws MIDletStateChangeException { if(myGameThread != null) { myGameThread.requestStop(); } myGameThread = null; myCanvas = null; System.gc(); } /** * Request the thread to pause. */ public void pauseApp() { if(myCanvas != null) { setGoCommand(); } If(myGameThread != null) { myGameThread.pauseGame(); } } //---------------------------------------------------------------- // Implementation of CommandListener /* * Respond to a command issued on the Canvas. * (either reset or exit). */ public void commandAction(Command c, Displayable s) { if(c == myGoCommand) { myCanvas.removeCommand(myGoCommand); myCanvas.addCommand(myPauseCommand); myCanvas.flushKeys(); myGameThread.resumeGame(); } else if(c == myPauseCommand) { myCanvas.removeCommand(myPauseCommand); myCanvas.addCommand(myGoCommand); myGameThread.pauseGame(); } else if(c == myNewCommand) { myCanvas.removeCommand(myNewCommand); myCanvas.addCommand(myPauseCommand); myCanvas.reset(); myGameThread.resumeGame(); } else if((c == myExitCommand) || (c == Alert.DISMISS_COMMAND)) { try { destroyApp(false); notifyDestroyed(); } catch (MIDletStateChangeException ex) { } } } //------------------------------------------------------- // Error methods /** * Converts an exception to a message and displays * the message. */ void errorMsg(Exception e) { if(e.getMessage() == null) { errorMsg(e.getClass().getName()); } else { errorMsg(e.getClass().getName() + ":" + e.getMessage()); } } /** * Displays an error message alert if something goes wrong. */ void errorMsg(String msg) { Alert errorAlert = new Alert("error", msg, null, AlertType.ERROR); errorAlert.setCommandListener(this); errorAlert.setTimeout(Alert.FOREVER); Display.getDisplay(this).setCurrent(errorAlert); } } |
线程类Thread的使用
游戏中仅仅涉及了线程类Thread的最简单应用,在这个最简单的例子中,有几点是需要特别说明的。
本例中,随时发起一个新线程是很必要的。例如游戏的背景动画,即使玩家没有触发任何按钮,它也一直处于移动状态,所以程序必须必须有一个循环逻辑来一直不停重复刷新屏幕,直到游戏结束。游戏设计的逻辑是不能让这个显示循环线程作为主线程,因为主线程必须是系统管理软件可以调用来控制游戏运行或者退出。在对决状态时测试游戏,我发现如果我用循环显示线程做主线程,对手就很难对按键做出及时反应。当然,通常当你计划进入一个将在代码运行整个生命周期做重复展示的循环时,发起一个新线程是一个很好的方法。
请看我的Thread子类的运行逻辑:一旦线程被发起,立刻进入主循环(while代码块)中。
第一个步骤是检查Jump类是否在上一次循环后调用了requestStop(),如果是,循环中断,返回运行run()方法。
否则,如果玩家没有暂停游戏,你要激发GameCanvas响应玩家的击键事件,接着继续游戏的动画效果,这时你需要循环线程一毫秒的暂停。这事实上会引起一帧画面在下一帧展示前会暂时停滞(1毫秒),但这可以使击键事件轮巡任务正常进行。
如上提及,玩家击键信息将被另一个线程修改,所以游戏显示循环需要设置一个短暂的等待,确保这个修改线程排队进入,有机会在极短的一瞬修改按键的状态值。这可以保证玩家按下键盘时得到迅速的响应,这就是游戏设计中常用到的1毫秒的把戏。
Listing 2 是GameThread.java源码。
Listing 2. GameThread.java
package net.frog_parrot.jump; /** * This class contains the loop that keeps the game running. * * @author Carol Hamer */ public class GameThread extends Thread { //--------------------------------------------------------- // Fields /** * Whether the main thread would like this thread * to pause. */ private boolean myShouldPause; /** * Whether the main thread would like this thread * to stop. */ private boolean myShouldStop; /** * A handle back to the graphical components. */ private JumpCanvas myJumpCanvas; //---------------------------------------------------------- // Initialization /** * Standard constructor. */ GameThread(JumpCanvas canvas) { myJumpCanvas = canvas; } //---------------------------------------------------------- // Actions /** * Pause the game. */ void pauseGame() { myShouldPause = true; } /** * Restart the game after a pause. */ synchronized void resumeGame() { myShouldPause = false; notify(); } /** * Stops the game. */ synchronized void requestStop() { myShouldStop = true; notify(); } /** * Start the game. */ public void run() { // Flush any keystrokes that occurred before the // game started: myJumpCanvas.flushKeys(); myShouldStop = false; myShouldPause = false; while(true) { if(myShouldStop) { break; } synchronized(this) { while(myShouldPause) { try { wait(); } catch(Exception e) {} } } myJumpCanvas.checkKeys(); myJumpCanvas.advance(); // You do a short pause to allow the other thread // to update the information about which keys are pressed: synchronized(this) { try { wait(1); } catch(Exception e) {} } } } } |
GameCanvas类
GameCanvas类主要是用来刷新屏幕显示自己设计的游戏背景图象。
GameCanvas类与Canvas类的不同
GameCanvas类能描绘设备分配给你的屏幕的所有区域。javax.microedition.lcdui.game.GameCanvas类不同于它的超类javax.microedition.lcdui.Canvas在于两个重要的方面:图形缓存和轮巡键值的能力。这两个变化给了游戏开发人员面对需要处理诸如击键事件、屏幕刷新事件时需要的更强大、精确的控制手段。
图形缓存的好处是它可以使图形对象在后端逐渐生成,然而在生成时可以瞬间显示,这样动画显示将会更加平滑。在Listing 3中的advance()方法中可以看到如何使用图形缓存。
(请回想方法advance()是在GameThread对象的主循环中被调用)。注意对于调整屏幕显示和重画屏幕,你所要做只是调用paint(getGraphics()),然后再调用flushGraphics().
为了使程序更有效率,如果你知道需要重画的只是屏幕的一部分,你可以使用flushGraphics()方法的另一个版本。作为一种经验,我尝试过用对repaint()方法的调用来替代对paint(getGraphics())和flushGraphics()的调用,再接着调用serviceRepaints()来实现屏幕重画,注意调用serviceRepaints()的前提是你的类必须是从Canvas类扩展而来,而不是GameCanvas。在本例简单的游戏调用来看,性能差别并不大,但如果你的游戏有大量复杂的图形,GameCanvas的使用毫无疑问将极大地增强性能。
轮巡按键状态的技巧对于游戏进程的管理是很重要的。你可以直接扩展Canvas类,同时如果你的游戏支持键盘操作,你必须实现keyPressed(int keyCode)接口。
接着玩家击键事件促使应用管理软件调用这个方法。但如果你的程序完全运行在自己的线程里,该方法可能根据游戏规则在任何点被调用。如果你忽略了样例中的同步处理代码块,这可能导致潜在的错误发生,例如一个线程正在修改与游戏当前状态值相关的数据而同时另一个线程正在利用这些数据进行计算的情况。样例程序很简单,你可以很容易跟踪到当你调用GameCanvas方法getKeyStates()时,是否获得了击键信息。
getKeystates()方法的一个额外利用是它可以告诉你是否多键被同时按下。一次仅有一个键值码被传入keyPressed(int keyCode)方法中,所以即使玩家同时按下多个键,对它来说也只是增加被调用的次数而不是一次传递多个键值。
在一个游戏中,每一次按键的精确时刻常常是非常重要的数据,所以Canvas类的keyPressed()方法其实缺失了很多有价值的信息。注意Listing 3中的checkKeys()方法,你可以看到getKeystates()方法的返回值中包含了所有按键信息。
你所要做的是将getKeyStates()方法的返回值和一个给定键值比如GameCanvas.LEFT_PRESSED进行“与”(&)运算,借此判断是否给定键值被按下。这是一个庞大的代码段,但是你可以找到它的主要逻辑脉络是,首先,主循环GameThread类告诉GameCanvas子类JumpCanvas去轮巡按键状态(细节请见Listing 3中的JumpCanvas.checkKeys()方法),接着一旦按键事件处理完毕,主循环GameThread类再调用JumpCanvas.advance()方法告诉LayerManager对图形做出相应的修改并最终在屏幕上画出来。
Listing 3是JumpCanvas.java的代码
Listing 3. 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 final int GROUND_HEIGHT = 32; /** * A screen dimension. */ static final int CORNER_X = 0; /** * A screen dimension. */ static final int CORNER_Y = 0; /** * 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; /** * Color constant */ public static final int BLACK = 0; /** * Color constant */ public static final int WHITE = 0xffffff; //--------------------------------------------------------- // Game object fields /** * A handle to the display. */ private Display myDisplay; /** * A handle to the MIDlet object (to keep track of buttons). */ private Jump myJump; /** * The LayerManager that handles the game graphics. */ private JumpManager myManager; /** * Whether the game has ended. */ private boolean myGameOver; /** * The player's score. */ private int myScore = 0; /** * How many ticks you start with. */ private int myInitialGameTicks = 950; /** * This is saved to determine if the time string needs * to be recomputed. */ private int myOldGameTicks = myInitialGameTicks; /** * The number of game ticks that have passed. */ private int myGameTicks = myOldGameTicks; /** * You save the time string to avoid recreating it * unnecessarily. */ private static String myInitialString = "1:00"; /** * You save the time string to avoid recreating it * unnecessarily. */ private String myTimeString = myInitialString; //----------------------------------------------------- // Gets/sets /** * This is called when the game ends. */ void setGameOver() { myGameOver = true; myJump.pauseApp(); } //----------------------------------------------------- // Initialization and game state changes /** * Constructor sets the data, performs dimension calculations, * and creates the graphical objects. */ public JumpCanvas(Jump midlet) throws Exception { super(false); myDisplay = Display.getDisplay(midlet); myJump = midlet; // Calculate the dimensions. DISP_WIDTH = getWidth(); DISP_HEIGHT = getHeight(); Display disp = Display.getDisplay(myJump); if(disp.numColors() < 256) { throw(new Exception("game requires 256 shades")); } if((DISP_WIDTH < 150) || (DISP_HEIGHT < 170)) { throw(new Exception("Screen too small")); } if((DISP_WIDTH > 250) || (DISP_HEIGHT > 250)) { throw(new Exception("Screen too large")); } FONT = getGraphics().getFont(); FONT_HEIGHT = FONT.getHeight(); SCORE_WIDTH = FONT.stringWidth("Score: 000"); TIME_WIDTH = FONT.stringWidth("Time: " + myInitialString); if(myManager == null) { myManager = new JumpManager(CORNER_X, CORNER_Y + FONT_HEIGHT*2, DISP_WIDTH, DISP_HEIGHT - FONT_HEIGHT*2 - GROUND_HEIGHT); } } /** * 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(); } /** * This version of the game does not deal with what happens * when the game is hidden, so I hope it won't be hidden... * see the version in the next chapter for how to implement * hideNotify and showNotify. */ protected void hideNotify() {} /** * This version of the game does not deal with what happens * when the game is hidden, so I hope it won't be hidden... * see the version in the next chapter for how to implement * hideNotify and showNotify. */ protected void showNotify() {} //------------------------------------------------------- // Graphics methods /** * Paint the game graphic on the screen. */ public void paint(Graphics g) { // Clear the screen: g.setColor(WHITE); g.fillRect(CORNER_X, CORNER_Y, DISP_WIDTH, DISP_HEIGHT); // Color the grass green: g.setColor(0, 255, 0); g.fillRect(CORNER_X, CORNER_Y + DISP_HEIGHT - GROUND_HEIGHT, DISP_WIDTH, DISP_HEIGHT); // Paint the layer manager: try { myManager.paint(g); } catch(Exception e) { myJump.errorMsg(e); } // Draw the time and score: g.setColor(BLACK); 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(WHITE); g.fillRect(CORNER_X, CORNER_Y, DISP_WIDTH, FONT_HEIGHT*2 + 1); int goWidth = FONT.stringWidth("Game Over"); g.setColor(BLACK); 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) { myJump.errorMsg(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(); } } } } |
用GameCanvas类处理Graphics对象
本节中,我将介绍Graphics对象类在游戏中被应用的主要方面。在Tumbleweed游戏中,我要画一个穿越草原的牛仔。在设计中,我将玩家的得分情况显示在屏幕底端,而游戏时间显示在顶端(为使样例不至太过复杂,我仅仅在玩家使用玩规定的时间后才自动终止游戏)。因为牛仔独自在走,我的想法是让他的背景向右或者向左滚动(否则在这个狭小的屏幕上他好象根本没走多远),而时间和分数的显示不动。
为了实现这个设计,我用JumpCanvas类来画屏幕顶端和底端稳定不动的显示条,而动态有趣的图形是用LayerManager类来实现的。
JumpCanva类创建时,你必须先分析要使用的显示屏幕。有一些显示屏幕信息来源于Graphics对象,一些来自于display对象,还有一些直接来源于GameCanvas类的方法。这些信息用来计算对象应当显示的位置,包括计算显示区域的尺寸,LayerManager子类(JumpManager)将在这些区域重复显示对象。如果你坚持维护Java“一处写就,处处运行”的特性,基于动态屏幕尺寸而不是基于常量尺寸设置屏幕显示区域显然更方便合理。
当然,如果从一种目标显示设备转向另一种游戏需要巨大的代码更改量,显然应该根据不同设备显示维护多个游戏版本,运行时根据显示设备信息调用游戏的不同版本,而不是将所有代码置于一个版本中。
在样例游戏中,如果实际显示的设备与游戏中定义的显示设备差别太大,将有一个异常抛出以便Jump类捕获并且显示警告给玩家。为了显得更专业,你应当确保给玩家的警告信息明确标明他针对当前的设备应该下载的版本号。
也许有点多余,但我后面仍然会告诉大家我认为顶端和底端区域合适的尺寸,paint(Graphics g)方法将描绘出顶端的白色和底端的绿色,g.drawString()方法用来记录使用的时间和分数。(不用问我为什么游戏的草原中都是颜色相同的绿草和风滚草;唯一的解释是我对Java的了解当然远甚于西部荒原。)
LayerManager类
一个MIDP游戏中富有乐趣的图形对象常常是用javax.microedition.lcdui.game.Layer类的子类来展示。背景层是javax.microedition.lcdui.game.TiledLayer的实例化,游戏的主人公(和他的敌人)是javax.microedition.lcdui.game.Sprite的实例,这两个类都是Layer的子类。LayerManager类用来组织所有的图形对象层,追加你的图层对象到管理器LayerManager的顺序,决定了他们运行时被描绘的次序(后进先出,最先追加的最后显示)。规则是上面的图层将覆盖下面的图层,不过你可以通过创建包含透明区域的图象文件(上层)来部分地显示下层图层中你想要的部分。
LayerManager类实际中最有用的也许是你可以创建一个远大于显示屏幕的图形对象,然后选择它将在屏幕中被显示的部分显示出来。你可以想象事先做出一幅巨大的、精心描绘的图画,然后用一张纸去覆盖它,而纸上有一个方孔在图画上自由移动。巨幅的整张图画代表你保存在LayberManager中的显示信息,而方孔是任一给定时刻在屏幕上显示图画的窗口。游戏代码设计中,一个比实际屏幕大得多的虚拟显示屏幕对于通常运行在小屏幕显示设备上的游戏是极其重要的,它将为你节省大量的时间和工作。
举个例子,比如你的游戏中包含主人公在一个复杂的地牢中摸索前进的场景,最麻烦的是你必须处理两个独立坐标系统。GameCanva的图形对象有一个坐标系,但是根据LayerManager坐标系的要求,图形中的不同图层又需要被置于LayerManager中。所以,一定要牢记LayerManager.paint(Graphics g, int x, int y)方法根据GameCanvas的坐标系在屏幕上描绘图层,而LayerManager.setViewWindow(int x, int y, int width, int height)方法根据LayerManager坐标系的要求设置LayerManager的可视矩形的显示属性。
在样例中,我设计了一个简单的背景(仅仅是一系列重复显示的草丛),但我想让牛仔从右向左行走的时候总是显示在屏幕中央,所以我需要不断改变LayerManager的图形可显示区域。这项工作是通过在LayerManager的子类JumpManager类中方法paint(Graphics g)中调用setViewWindow(int x, int y, int width, int height)方法完成的。更准确地说,逻辑是这样的:GameThread中的主循环调用JumpCanvas.checkKeys()来检查按键状态,并且通知JumpManager类牛仔此时应该向左、向右还是应该跳跃了。JumpCanva类通过调用setLeft(boolean left)或者jump()方法将信息传至JumpManager。如果信息表明牛仔此时应该向左走(向右与之类似),那么当GameThead调用JumpCanvas告诉JumpManager继续的时候(循环的下一步),JumpManager就告诉牛仔对象向左移动一个象素,同时通过向左移动视窗一个象素来保持牛仔在屏幕中央。你可以通过增加字段myCurrentLeftX的值(这个作为X坐标值传至setViewWindow(int x,int y,int width,int height)),接着调用myCowboy.advance(gameTicks, myLeft)来实现这两个动作。
当然,我可以不移动牛仔,也不把他追加到LayerManager,而只是独立地描绘他,这样也可以确保牛仔在屏幕中央。但显然通过将所有的移动对象置于相对静止的一系列图层中然后将视窗集中在牛仔对象上,借此保持所有对象的运动轨迹的方法要更容易。在通知牛仔前进的同时,风滚草对象Sprites和青草对象TiledLayer也也同时移动,然后检测牛仔是否被风滚草撞倒(在以下章节里我将说明实现这个功能的更多细节)。在移动了所有游戏对象后,JumpManager类调用wrap()方法来检查是否前端窗口到达了背景窗口的边缘,如果是,移动所有的有些对象以便视窗能继续在任一方向无限显示,接着JumpCanvas类重画每一个对象,再次执行游戏主循环。
Wrap()方法需要多说几句。很不幸,如果你想让你的简单背景不确定地重复显示,LayerManager类并不提供现成的隐藏方法. 当传至方法setViewWindow(int x, int y, int width, int height)的坐标参数值大于Integer. MAX_VALUE 时,LayerManager的图形区域将被隐藏,但这似乎对我们的想法没有任何帮助。因此,你必须写自己的函数来避免牛仔Sprite对象离开背景区域。
样例中,背景草丛总是在间隔数值Grass.TILE_WIDTH*Grass.CYCLE后被重画,所以任何时候视窗的X坐标值(myCurrentLeftX)都是背景图形宽度的整数倍。这样每次重画时,我都可以将视窗移回到显示中心,并且同时在相同的方向上移动Sprites对象,这样显然能平滑地阻止牛仔到达背景边界。
Listing 4 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 * coordinates 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 * coordinates 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. */ private Cowboy myCowboy; /** * The tumbleweeds that enter from the left. */ private Tumbleweed[] myLeftTumbleweeds; /** * The tumbleweeds that enter from the right. */ private Tumbleweed[] myRightTumbleweeds; /** * The object representing the grass in the background. */ private Grass myGrass; /** * Whether the player is currently going left. */ private boolean myLeft; /** * The leftmost X coordinate that should be visible on the * screen in terms of this objects internal coordinates. */ private int myCurrentLeftX; //----------------------------------------------------- // Gets/sets /** * This tells the player to turn left or right. * @param left whether the turn is toward the left. */ void setLeft(boolean left) { myLeft = left; } //----------------------------------------------------- // Initialization and game state changes /** * Constructor sets the data and constructs the graphical objects. * @param x The X coordinate of the place on the game canvas where * the LayerManager window should appear, in terms of the * coordinates 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 * coordinates 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) throws Exception { 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); // 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); } } /** * 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. */ public void paint(Graphics g) { etViewWindow(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. */ private 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 remaining 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 you move the view window // (so you are showing a slightly different view of // the manager's graphical area). if(myLeft) { myCurrentLeftX--; } else { myCurrentLeftX++; } // Now you 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 you check if you have reached an edge of the viewable // area, and if so, you 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(); } } |
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的图形和动画特性