采用新的 MIDP 2.0 API来编写手机游戏给我们带来了很多乐趣. 对程序员来说 MIDP 2.0相对于MIDP 1.0最大的区别就是图像处理能力大大增强. 当然,使用MIDP 1.0 你也可以写出许多令人激动的游戏—那些对 Atari 2600 记忆犹新的人们对之津津乐道,尽管他们不是那么完美。midp2.0 也许还不能让你使用最新的游戏技术,但是它至少可以让手机中的超级马里或 thereabouts 成为现实

本文将通过一些简单游戏 MIDlet向你展示midp2的用法. 并且本文的读者被假定为熟悉java语言的j2me新手. 我的例子向你展示了一个在草原中跳跃的牛仔.这个游戏看起来的确很菜,不过它包含了你在编写大部分游戏时常用到的技术.

 

开始咯

如果你还没有下载j2me,先下载一个吧:http://java.sun.com/j2me/download.html.  Java 2 Micro Edition Wireless Toolkit 包含MIDlet 开发环境,模拟器, 还有一些例程.在这里我就不对怎么安装j2me在浪费笔墨了,因为随着环境的不同。安装不尽相同,而且联机文档中已经说的蛮清楚的了.

当你查看demo程序的时候, 你会发现它包括jar文件和一个jad文件. Jar包括了class文件, 资源文件,以及一个 manifest 文件.  jad 包含了加载MIDlets必须的一些属性:比如jar的大小, 包含MIDlet的 jar的url地址.下面是我的 Hello World 例程中所用到的jad文件内容:

MIDlet-1: Hello World, /icons/hello.png, net.frog_parrot.hello.Hello
MMIDlet-Description: Hello World for MIDP
MIDlet-Jar-URL: hello.jar
MIDlet-Name: Hello World
MIDlet-Permissions:
MIDlet-Vendor: frog-parrot.net
MIDlet-Version: 2.0
MicroEdition-Configuration: CLDC-1.0
MicroEdition-Profile: MIDP-2.0
MIDlet-Jar-Size: 3201
MIDlet-1 (以及MIDlet-2, 等.) 属性 给出MIDlet的名称, MIDlet's 图标的位置, 以及MIDlet类.前两个项目描述了在MIDlet菜单中改MIDlet条目将如何显示.当然 icon 必须存在于jar中,它的用法和 Class.getResource(String res)中res参数用法一样. 所以在本例中,jar根目录下的icons子目录中一定包含一个名为 的hello.png,它看起来是下面这个样子:


 MIDlet-Jar-Size 描述了对应的jar文件的字节数. 如果你使用wtk重新编译程序的话, 你必须手动修改这个属性(译注:不是吧,程序似乎自动会修改的吧) MIDlet-Jar-Size 属性与jar 文件不匹配的话程序不会运行(译注:大伙自己试试吧). (因为老是手动修改jar大小的确是非常麻烦的,所以作者在文章的最后附带了一个build用的script,大伙参考参考吧.)  MIDlet-Jar-URL 属性告诉系统从什么地方下载这个 MIDlet jar 。如果jar 文件和 jad 在同一个目录,那么只要写jar的名称就成了. 其他的属性都是说明程序自身的.

Hello World

为了说明 MIDlet 的结构,我写了这个 "Hello World". 该 MIDlet 在显示器上显示"Hello World!" ,当按下 "Toggle Msg" 按钮时,字符串被移除. 按下 "Exit" 按钮的话程序会退出.程序包括了两个类: MIDlet 类Hello以及Canvas 类HelloCanvas. 这真的的很简单,大伙儿可以看我在代码中的注释.

这是Hello.java的代码:

package net.frog_parrot.hello;
 
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
 
/**
 * This is the main class of the hello world demo.
 *
 * @author Carol Hamer
 */
public class Hello extends MIDlet implements CommandListener {
 
  /**
   * The canvas is the region of the screen that has been allotted
   * to the game.
   */
  HelloCanvas myCanvas;
 
  /**
   * The Command objects appear as buttons.
   */
  private Command exitCommand = new Command("Exit", Command.EXIT, 99);
 
  /**
   * The Command objects appear as buttons.
   */
  private Command newCommand = new Command("Toggle Msg", Command.SCREEN, 1);
 
  /**
   * Initialize the canvas and the commands.
   */
  public Hello() {
    myCanvas = new HelloCanvas(Display.getDisplay(this));
    myCanvas.addCommand(exitCommand);
    myCanvas.addCommand(newCommand);
    myCanvas.setCommandListener(this);
  }
 
  //----------------------------------------------------------------
  //  implementation of MIDlet
 
  /**
   * Start the application.
   */
  public void startApp() throws MIDletStateChangeException {
    myCanvas.start();
  }
 
  /**
   * If the MIDlet was using resources, it should release
   * them in this method.
   */
  public void destroyApp(boolean unconditional)
      throws MIDletStateChangeException {
  }
 
  /**
   * This method is called to notify the MIDlet to enter a paused
   * state.  The MIDlet should use this opportunity to release
   * shared resources.
   */
  public void pauseApp() {
  }
 
  //----------------------------------------------------------------
  //  implementation of CommandListener
 
  /*
   * Respond to a command issued on the Canvas.
   * (either reset or exit).
   */
  public void commandAction(Command c, Displayable s) {
    if(c == newCommand) {
      myCanvas.newHello();
    } else if(c == exitCommand) {
      try {
 destroyApp(false);
 notifyDestroyed();
      } catch (MIDletStateChangeException ex) {
      }
    }
  }
 
}
Here's the code for HelloCanvas.java:

package net.frog_parrot.hello;
 
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
 
/**
 * This class represents the region of the screen that has been allotted
 * to the game.
 *
 * @author Carol Hamer
 */
public class HelloCanvas extends javax.microedition.lcdui.game.GameCanvas {
 
  //---------------------------------------------------------
  //   fields
 
  /**
   * A handle to the screen of the device.
   */
  Display myDisplay;
 
  /**
   * whether or not the screen should currently display the
   * "hello world" message.
   */
  boolean mySayHello = true;
 
  //-----------------------------------------------------
  //    initialization and game state changes
 
  /**
   * Constructor merely sets the display.
   */
  public HelloCanvas(Display d) {
    super(false);
    myDisplay = d;
  }
 
  /**
   * This is called as soon as the application begins.
   */
  void start() {
    myDisplay.setCurrent(this);
    repaint();
  }
 
  /**
   * toggle the hello message.
   */
  void newHello() {
    mySayHello = !mySayHello;
    // paint the display
    repaint();
  }
 
  //-------------------------------------------------------
  //  graphics methods
 
  /**
   * clear the screen and display the hello world message if appropriate.
   */
  public void paint(Graphics g) {
    // x and y are the coordinates of the top corner of the
    // game's display area:
    int x = g.getClipX();
    int y = g.getClipY();
    // w and h are the width and height of the display area:
    int w = g.getClipWidth();
    int h = g.getClipHeight();
    // clear the screen (paint it white):
    g.setColor(0xffffff);
    g.fillRect(x, y, w, h);
    // display the hello world message if appropriate:.
    if(mySayHello) {
      Font font = g.getFont();
      int fontHeight = font.getHeight();
      int fontWidth = font.stringWidth("Hello World!");
      // set the text color to red:
      g.setColor(0x00ff0000);
      g.setFont(font);
      // write the string in the center of the screen
      g.drawString("Hello World!", (w - fontWidth)/2,
     (h - fontHeight)/2,
     g.TOP|g.LEFT);
    }
  }
 
}
The MIDlet Class

现在我们开始了.

 MIDlet 类包含了一些大部分MIDlets 都会包含的东东(比如开始和结束按钮) ,所以一个MIDlet的代码不要做多大的变化就可以移植成另外一个了. 下面我们将要看到的MIDlet 类(叫Jump来着) 和"Hello World" 的看起来差不多, 当然有些改进之处,我以后会解释的. 别忘了 MIDlet 要在 jad文件中申明.

该MIDlet 包含一些按钮. 用户只能暂停一个没有暂停的MIDlet, 并且只能让一个暂停的的MIDlet转到非暂停状态, 并且只能让一个已经结束的程序重新开始, 所以 "Go", "Pause", and "Play Again"三个按钮不能同时显示。 每次我都会去掉不需要的两个按钮. 当然,如果你需要的话,在MIDlet中多加入几个按钮也是可以的. 手机的 JVM 会决定怎么安放他们。比如wtk自带的模拟器会通过一个按钮的菜单来安放他们. Command的构造方法中使用诸如 Command.BACK 或者 Command.HELP 之类的常量参数可以告诉jvm如何把这些按钮和手机上的按键对应. 如果屏幕上布置了很多按钮的话,优先级高的按钮将会容易被用户找到一些.

学习下面的代码的时候还有一个地方需要注意,那就是destroyApp() 什么时候被调用.这个方法只有程序结束将要释放程序所占有的所有资源时被调用. 例程是很简单拉,它没有什么共享的资源需要释放,不过我还是希望你们记住,MIDlet占有的内存绝对值也许很少,但是对于手机这样的设备来说说不定却是非常重要的。所以记住退出程序的时候把你的MIDlet实例设置成null,以便它能被垃圾收集机制收集掉。调用 notifyDestroyed() 将会把控制权交还给手机,但是它却不一定会释放被MIDlet占有的资源 (除非关闭java功能或者 直接关机。看过《手机》的朋友一定还会想到,直接拔电池也是可以的,而且在数个小时之内,如果有人打你电话还无法接通:-). 所以我们不难想到,个别的资源说不定游戏已经退出了,它还没有被释放.为了避免这种站着茅坑不拉屎的现象, 我们必须记住,如果我们使用了什么static变量的 话,不用了就让它等于null

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 {
 
  //---------------------------------------------------------
  //   game object fields
 
  /**
   * 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);
 
  /**
   * the canvas that all of the game will be drawn on.
   */
  JumpCanvas myCanvas;
 
  /**
   * the thread that advances the cowboy.
   */
  GameThread myGameThread;
 
  //-----------------------------------------------------
  //    initialization and game state changes
 
  /**
   * Initialize the canvas and the commands.
   */
  public Jump() {
    myCanvas = new JumpCanvas(this);
    myCanvas.addCommand(myExitCommand);
    myCanvas.addCommand(myGoCommand);
    myCanvas.setCommandListener(this);
  }
 
  /**
   * 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.
   */
  void setGoCommand() {
    myCanvas.removeCommand(myPauseCommand);
    myCanvas.removeCommand(myNewCommand);
    myCanvas.addCommand(myGoCommand);
  }
 
  /**
   * Switch the command to the pause command.
   */
  void setPauseCommand() {
    myCanvas.removeCommand(myNewCommand);
    myCanvas.removeCommand(myGoCommand);
    myCanvas.addCommand(myPauseCommand);
  }
 
  //----------------------------------------------------------------
  //  implementation of MIDlet
 
  /**
   * Start the application.
   */
  public void startApp() throws MIDletStateChangeException {
    myGameThread = new GameThread(myCanvas);
    myCanvas.start();
  }
 
  /**
   * stop and throw out the garbage.
   */
  public void destroyApp(boolean unconditional)
      throws MIDletStateChangeException {
    myGameThread.requestStop();
    myGameThread = null;
    myCanvas = null;
    System.gc();
  }
 
  /**
   * request the thread to pause.
   */
  public void pauseApp() {
    setGoCommand();
    myGameThread.pause();
  }
 
  //----------------------------------------------------------------
  //  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);
      myGameThread.go();
    } else if(c == myPauseCommand) {
      myCanvas.removeCommand(myPauseCommand);
      myCanvas.addCommand(myGoCommand);
      myGameThread.go();
    } else if(c == myNewCommand) {
      myCanvas.removeCommand(myNewCommand);
      myCanvas.addCommand(myGoCommand);
      myGameThread.requestStop();
      myGameThread = new GameThread(myCanvas);
      System.gc();
      myCanvas.reset();
    } else if(c == myExitCommand) {
      try {
         destroyApp(false);
         notifyDestroyed();
      } catch (MIDletStateChangeException ex) {
      }
    }
  }
 
}
Thread 类(线程)

对于 MIDlets来说,线程的使用不是唯一的, 不过游戏程序员对它应该时相当熟悉的了.在例程中 run() 方法 包含了游戏的主循环, 它来检测 GameCanvas 中的按键事件,并且做出相应的反应

下面是 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 or not the main thread would like this thread
   * to pause.
   */
  boolean myShouldPause;
 
  /**
   * Whether or not the main thread would like this thread
   * to stop.
   */
  static boolean myShouldStop;
 
  /**
   * Whether or not this thread has been started.
   */
  boolean myAlreadyStarted;
 
  /**
   * A handle back to the graphical components.
   */
  JumpCanvas myJumpCanvas;
 
  //----------------------------------------------------------
  //   initialization
 
  /**
   * standard constructor.
   */
  GameThread(JumpCanvas canvas) {
    myJumpCanvas = canvas;
  }
 
  //----------------------------------------------------------
  //   actions
 
  /**
   * start or pause or unpause the game.
   */
  void go() {
    if(!myAlreadyStarted) {
      myAlreadyStarted = true;
      start();
    } else {
      myShouldPause = !myShouldPause;
    }
  }
 
  /**
   * pause the game.
   */
  void pause() {
    myShouldPause = true;
  }
 
  /**
   * stops the game.
   */
  static void requestStop() {
    myShouldStop = true;
  }
 
  /**
   * 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;
      }
      if(!myShouldPause) {
         myJumpCanvas.checkKeys();
         myJumpCanvas.advance();
      }
    }
  }
 
}