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.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(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.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) {
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.TOP|g.LEFT);
myGameOver = true;
}
}
我们已经讨论了 GameCanvas的特性, 不过我还是想粗略的讨论一下 Canvas , 即绘制显示器. 绘制通常发生在你重载的paint(Graphics g) 方法中. Graphics 对象可以查询屏幕的尺寸,并且往上面绘制一些简单的东西. Graphics 对象提供了丰富的方法使我们可以顺利的完成绘制. (和 java.awt.graphics 还是蛮相似的,除了 javax.microedition.lcdui.Graphics 在绘制一个形状的时候是直接调用绘制方法,而不是先创建一个对应的形状对象,因为手机的内存有限.)真的要仔细介绍他的用法的话,几页纸也讲不玩,所以在这里我主要是对我讲用到的一些部分重点的介绍. Java的帮助文档对javax.microedition.lcdui.Graphics 的介绍相当的完整而且明了, 所以你写游戏的时候一定要学会查考帮助文档.
你一定看到了,我把得分安放在下部,时间放置在上面. (为了简单起见,当牛仔超时了这个游戏就结束.) 随着牛仔的跑动,我希望背景从右到左的滚动 (不然这么晓得屏幕咋跑啊...) 但是我希望时间和得分的显示固定在一个位置不动. 为了得到这个效果,我让JumpCanvas 类绘制固定的东东,而运动的东东则交给 LayerManager全权负责 (后面会有更详细的介绍的).
请看paint(Graphics g)方法,首先我通过Graphics对象得到屏幕的大小,并且根据这些大小计算对象将被绘制的位置. 如果你很在意java"write once, run anywhere"的特性, 比起那些使用是指常量的绘制代码,动态的计算绘制位置是一个比较好的方法.即使如此,你的游戏代码在具有不同尺寸的显示器上运行起来仍然可能很滑稽(不是太大,就是太小). 如果屏幕的尺寸超过了一定的限度,与其让它滑稽的运行下去还不如直接抛出的异常直截了当.
在paint(Graphics g)方法计算完需要绘制的位置后, 我使用 g.fillRect 把屏幕绘制成上白下绿两部分,然后使用g.drawString来绘制时间以及得分。我计算出两个区域之间的大小,并且把它传给我定义的 LayerManager的子类.
LayerManager 类(千呼万唤始出来啊)
J2me游戏中最有趣的图形对象通常是有javax.microedition.lcdui.game.Layer来表现的.背景层可以使用 javax.microedition.lcdui.game.TiledLayer来实现, 游戏主角(包括他的敌人) 则是javax.microedition.lcdui.game.Sprite的实例, 不管怎么说,他们都是Layer的子类. LayerManager的纸则就是帮助你管理那些图层. 你添加Layers的顺序同时也决定了 LayerManager 绘制他们的顺序(最早添加的最后绘制,先进后出.) 上层的图像会挡住下层的图像,不过如果你创建了透明的图层区域的话,还是可以透视的.
可能 LayerManager 的最大特色就是你可以创建一个超大的图形区域,而绘制的时候只选择绘制其中的一部分. 这个绘制机制好比是走马灯,在一个很大的图片上蒙了一层开有一个矩形小孔的纸,我们移动这个纸就看到不同的内容了. 所有的绘制内容会被保存在LayerManager中, 而那个小洞自然就是用来的显示屏幕咯. 虚拟屏幕的存在为屏幕很小的手机编程提供了很大的便利. 如果你要你的主角在迷宫中探险的话,它这个机制一定帮了你不少忙. 不过这种机制会产生两个坐标系统. GameCanvas 的Graphics对象拥有一个坐标系统, LayerManager中的不同图层又按照 LayerManager的坐标系统被存放。记住 LayerManager.paint(Graphics g, int x, int y) 按照GameCanvas的坐标系统绘制,然而 LayerManager.setViewWindow(int x, int y, int width, int height)却是用LayerManager的坐标系统来设置可见区域的.
在我的例子中,背景是相当简单的(仅仅是草的重复)。不过因为我需要牛仔左右走动的时候始终处在屏幕的中间,所以我需要不停的改变LayerManager中的可显示部分. 通过在JumpManager类中的paint(Graphics g)方法中调用setViewWindow(int x, int y, int width, int height) 可以做到这点.我们来仔细的分析这中间的流程: GameThread 的主循环调用 JumpCanvas.checkKeys()方法查询按键状态后告诉 JumpManager 类牛仔是否需要左右移动或者跳起. JumpCanvas 通过调用setLeft(boolean left)或者jump()来通知 JumpManager 该干什么. 如果牛仔是跳起状态, 那么 JumpManager 调用 牛仔的精灵类的jump()方法. 如果是向左(或者右)走动,那么GameThread 让JumpCanvas 告诉 JumpManager把牛仔向左移动一个象素,同时把可见区域向相反的方向移动一个象素,依次来保证牛仔在屏幕上的位置看起来没有发生变化. 这两个动作是通过修改 myCurrentLeftX变量(控制 setViewWindow(int x, int y, int width, int height)的x坐标) 然后调用myCowboy.advance(gameTicks, myLeft)完成的. 当然我可以更加简单的直接绘制牛仔的图片而不把它加入到JumpManager中去, 但是把它加到一个图层中与现实世界更加贴切. 移动牛仔同时,我还要移动滚草的位置,并且变换滚草的当前帧以产生动画,最后还要检测碰撞,更进一步的细节我会在以后给大伙说的. 再这个例程中,移动完了所有的元素后, JumpManager 会wrap() 方法检查可见区域是否碰到了背景的边界了,如果是的画,还要调整一下显示,使得背景看起来是连续循环的的. 最后 JumpCanvas重新绘制屏幕然后再循环.
关于wrap()我还要再谈几句. 很不幸,LayerManager 没有提供通过重复一个图片而产生背景卷动的功能. LayerManager的绘制区域只有当传递到 setViewWindow(int x, int y, int width, int height) 的参数大于 Integer.MAX_VALUE才会有wrap效果, 不过这于事无补. 所以为了实现卷瓶,你只有自己动手写代码了.在我的例子中,程序每隔Grass.TILE_WIDTH*Grass.CYCLE象素会产生草的的循环卷动. 所以可见区域的x坐标 (myCurrentLeftX) 的值不会超过背景的宽度,并且当可见区域到头了,我会把可见区域连通其他需要绘制的对象一起搬回背景的中间,依次来防止玩家跑出边界
以下是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
* coordiantes 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
* coordiantes 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.
*/
Cowboy myCowboy;
/**
* the tumbleweeds that enter from the left.
*/
Tumbleweed[] myLeftTumbleweeds;
/**
* the tumbleweeds that enter from the right.
*/
Tumbleweed[] myRightTumbleweeds;
/**
* the object representing the grass in the background..
*/
Grass myGrass;
/**
* Whether or not the player is currently going left.
*/
boolean myLeft;
/**
* The leftmost x-coordinate that should be visible on the
* screen in terms of this objects internal coordinates.
*/
int myCurrentLeftX;
//-----------------------------------------------------
// gets/sets
/**
* This tells the player to turn left or right.
* @param left whether or not the turn is towards the left..
*/
void setLeft(boolean left) {
myLeft = left;
}
//-----------------------------------------------------
// initialization and game state changes
/**
* Constructor merely sets the data.
* @param x The x-coordinate of the place on the game canvas where
* the LayerManager window should appear, in terms of the
* coordiantes 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
* coordiantes 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) {
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);
}
/**
* 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.
* initialization code is included here because some
* of the screen dimensions are required for initialization.
*/
public void paint(Graphics g) throws Exception {
// 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);
}
// this is the main part of the method:
// we indicate which rectangular region of the LayerManager
// should be painted on the screen and then we paint
// it where it belongs. The call to paint() below
// prompts all of the appended layers to repaint themselves.
setViewWindow(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.
*/
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 remainaing 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 we move the view window
// (so we are showing a slightly different view of
// the manager's graphical area.)
if(myLeft) {
myCurrentLeftX--;
} else {
myCurrentLeftX++;
}
// now we 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 we check if we have reached an edge of the viewable
// area, and if so we 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();
}
}