MIDP2.0开发J2ME游戏起步

  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();
 }
}