GameCanvas实际上就是屏幕上一个可绘制区域. Javax.microedition.lcdui.game.GameCanvas 类与javax.microedition.lcdui.Canvas 有以下两点不一样: 图像缓冲以及可以查询按键的状态. 这些改进给游戏开发者更多的便利.
图像缓冲实现了所有的图形对象在后台创建,等他们全部预备好了,再一起绘制到屏幕上去.这使得动画更加流畅.在代码里我已经说明了怎么调用 advance() 方法. ( advance()在 GameThread 对象的主循环中调用.) 你所要做的就是调用paint(getGraphics()) 然后调用 flushGraphics(). 为了让你的代码更加高效,并且你知道屏幕上哪些部分需要重新绘制,你可以调用 flushGraphics()方法.作为实验,把 paint(getGraphics()) 和 flushGraphics() 的调用换成 repaint()以及 serviceRepaints()(假如你的类是继续自Canvas而不是GameCanvas的话).在我的代码中中,他们没有什么明显的区别,但是假如你的程序包含了很多复杂的图形的话,GameCanvas 无疑是一个明智的选择.
当你学习下面的代码的时候,你会发现当我刷新了屏幕以后 (在advance()方法中),我让线程停止了1毫秒. 这除了是为了让新绘制的图像稍稍停留一会, 更重的是它保证了按键查询的正确工作. 我前面已经提到, GameCanvas 和Canvas的按键状态的响应是不一样的. 在 Canvas时代, 假如你想知道按键状态,你必须实现keyPressed(int keyCode),每当有键被按下时,这个方法就被调用. 而 GameCanvas时代, 当你想知道某个键是否被调用的时候,直接调用 getKeyStates()方法就成了. 当然getKeyStates()的返回值会在另外一个线程中被更新,所以在你的游戏主循环中我们最好稍微登上一会,以保证这个值被更新,磨刀不误砍柴功嘛。
GameCanvas的两个方面的优越性是怎么提高绘制性能以及按键响应这个问题现在已经显而易见了。 让我们再回到 GameThread 类, 游戏的主循环首先向 GameCanvas 的子类 (叫做JumpCanvas) 查询按键状态 (参见 JumpCanvas.checkKeys() 方法). 按键事件处理好了以后, GameThread 的主循环调用JumpCanvas.advance() 来让 LayerManager 对图像做适当的更新 (下一节中将会具体介绍) 然后将它们绘制到屏幕上,最后等上一小会。
下面是 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 int GROUND_HEIGHT = 32;
/**
* a screen dimension.
*/
static int CORNER_X;
/**
* a screen dimension.
*/
static int CORNER_Y;
/**
* 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;
//---------------------------------------------------------
// game object fields
/**
* a handle to the display.
*/
Display myDisplay;
/**
* a handle to the MIDlet object (to keep track of buttons).
*/
Jump myJump;
/**
* the LayerManager that handles the game graphics.
*/
JumpManager myManager;
/**
* whether or not the game has ended.
*/
static boolean myGameOver;
/**
* the player's score.
*/
int myScore = 0;
/**
* How many ticks we start with.
*/
int myInitialGameTicks = 950;
/**
* this is saved to determine if the time string needs
* to be recomputed.
*/
int myOldGameTicks = myInitialGameTicks;
/**
* the number of game ticks that have passed.
*/
int myGameTicks = myOldGameTicks;
/**
* whether or not this has been painted once.
*/
boolean myInitialized;
/**
* The initial time string.
*/
static String myInitialString = "1:00";
/**
* we save the time string to avoid recreating it
* unnecessarily.
*/
String myTimeString = myInitialString;
//-----------------------------------------------------
// gets/sets
/**
* This is called when the game ends.
*/
static void setGameOver() {
myGameOver = true;
GameThread.requestStop();
}
/**
* Find out if the game has ended.
*/
static boolean getGameOver() {
return(myGameOver);
}
//-----------------------------------------------------
// initialization and game state changes
/**
* ConstrUCtor sets the data.
*/
public JumpCanvas(Jump midlet) {
super(false);
myDisplay = Display.getDisplay(midlet);
myJump = midlet;
}
/**
* 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();
}
//-------------------------------------------------------
// graphics methods
/**
* paint the game graphics on the screen.
*/
public void paint(Graphics g) {
// perform the calculations if necessary:
if(!myInitialized) {
CORNER_X = g.getClipX();
CORNER_Y = g.getClipY();
DISP_WIDTH = g.getClipWidth();
DISP_HEIGHT = g.getClipHeight();
FONT = g.getFont();
FONT_HEIGHT = FONT.getHeight();
SCORE_WIDTH = FONT.stringWidth("Score: 000");
TIME_WIDTH = FONT.stringWidth("Time: " + myInitialString);
myInitialized = true;
}
// clear the screen:
g.setColor(0xffffff);
g.fillRect(CORNER_X, CORNER_Y, DISP_WIDTH, DISP_HEIGHT);
g.setColor(0x0000ff00);
g.fillRect(CORNER_X, CORNER_Y + DISP_HEIGHT - GROUND_HEIGHT,
DISP_WIDTH, DISP_HEIGHT);
// create (if necessary) then paint the layer manager:
try {
if(myManager == null) {
myManager = new JumpManager(CORNER_X, CORNER_Y + FONT_HEIGHT*2,
DISP_WIDTH, DISP_HEIGHT - FONT_HEIGHT*2 - GROUND_HEIGHT);
}
myManager.paint(g);
} catch(Exception e) {
errorMsg(g, e);
}
// draw the time and score
g.setColor(0);
g.setFont(FONT);
g.drawString("Score: " + myScore,
(DISP_WIDTH - SCORE_WIDTH)/2,
DISP_HEIGHT + 5 - GROUND_HEIGHT, g.TOPg.LEFT);
g.drawString("Time: " + formatTime(),
(DISP_WIDTH - TIME_WIDTH)/2,
CORNER_Y + FONT_HEIGHT, g.TOPg.LEFT);
// write game over if the game is over
if(myGameOver) {
myJump.setNewCommand();
// clear the top region:
g.setColor(0xffffff);
g.fillRect(CORNER_X, CORNER_Y, DISP_WIDTH, FONT_HEIGHT*2 + 1);
int goWidth = FONT.stringWidth("Game Over");
g.setColor(0);
g.setFont(FONT);
g.drawString("Game Over", (DISP_WIDTH - goWidth)/2,
CORNER_Y + FONT_HEIGHT, g.TOPg.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) {
errorMsg(e);
}
// we do a very short pause to allow the other thread
// to update the information about which keys are pressed:
synchronized(this) {
try {
wait(1);
} catch(Exception 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();
}
}
}
//-------------------------------------------------------
// error methods
/**
* Converts an exception to a message and displays
* the message..
*/
void errorMsg(Exception e) {
errorMsg(getGraphics(), e);
flushGraphics();
}
/**
* Converts an exception to a message and displays
* the message..
*/
void errorMsg(Graphics g, Exception e) {
if(e.getMessage() == null) {
errorMsg(g, e.getClass().getName());
} else {
errorMsg(g, e.getClass().getName() + ":" + e.getMessage());
}
}
/**
* Displays an error message if something goes wrong.
*/
void errorMsg(Graphics g, String msg) {
// clear the screen
g.setColor(0xffffff);
g.fillRect(CORNER_X, CORNER_Y, DISP_WIDTH, DISP_HEIGHT);
int msgWidth = FONT.stringWidth(msg);
// write the message in red
g.setColor(0x00ff0000);
g.setFont(FONT);
g.drawString(msg, (DISP_WIDTH - msgWidth)/2,
(DISP_HEIGHT - FONT_HEIGHT)/2, g.TOPg.LEFT);
myGameOver = true;
}
}