MIDP 2.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 players 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 wont 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 wont 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();
}
}
}
}