介绍
本文通过一个简单的midlet游戏程序示例,简要介绍了midlet图形编程,以期能对开发者深入理解midp图形编程和开发复杂的移动游戏有所帮助。
一. midlet图形
1、midlet 图形简述
移动信息设备描述(mobile information device profile ,midp)定义了一套应用编程接口(api),用于运行在midp容器中的midlet应用程序。这套api本身是建立在有限连接设备配置(connected limited device configuration ,cldc)应用编程接口的基础上的。midp用户界面应用编程接口类并不是基于java抽象窗口工具包(abstract window toolkit ,awt)设计。它们是专为手机和呼机这样的小型移动信息设备而设计的,这类设备的特点是只有很小的屏幕和键盘。当一个程序员在编写midp图形应用程序的时候,他可能只能使用midp或cldc应用编程接口。
midp的中心抽象是屏幕,这句话的含义是midp的用户界面设计是基于屏幕的(screen-based)。也就是说,screen类封装了设备特定的图形和用户交互,所有的用户界面组件都位于屏幕上,并且一次只显示一个屏幕,并且只能浏览或使用这个屏幕上的条目。由屏幕来处理所有的用户界面事件。并只把高级事件传送给应用。之所以采取这种面向屏幕(screen-oriented) 的方式,主要是因为移动设备的显示屏幕和键盘实是种类太多了,几乎每个厂家都多多少少有所不同。图1是基于屏幕的midp图形用户界面的一些例子。
图1:基于屏幕的midp 图形用户界面
midp 应用编程接口具有高级用户界面类和低级用户界面类。高级用户界面类(例如form、list、textbox、textfield、alert,及ticker)可被适配到设备上:支持图像、文本、文本输入域、单选按钮等。低级用户界面类(canvas类)允许开发者根据需要绘制任意图形。midlet可以运行在各种不同尺寸的彩色、不同灰度等级或黑白屏幕的手机上。高级用户界面类是通用用户界面元素的抽象,它的用途在于提高midlet跨不同设备的移植性,并且可以使用本地设备的外观表现。低级应用编程接口则能够更直接地控制显示内容,但是midlet设计者应该确保其在不同设备(显示尺寸、键盘、色彩等)上的可移植性。上面的例子既用到了高级应用编程接口又用到了低级应用编程接口。
所有的midp图形用户界面类都是javax.microedition.lcdui程序包的一部分。
2、midlet屏幕
midp有两种主要的屏幕类型:
a 高级屏幕
它包括简单的高级屏幕类,例如list和textbox。用户不能添加额外的图形用户界面组件到这种类型的屏幕中。九宫格midlet示例程序使用的屏幕是继承于名为choosepiecescreen的list类,用于游戏者在游戏开始时选择棋子。
一般的form屏幕类和list类很相像,但是它允许使用额外的图形元素,例如:图像、只读文本域、可编辑文本域、可编辑数据域、标尺和选项组。form条目可以任意地被添加或删除。九宫格例程中没有使用form类。
b 低级屏幕
canvas(画布)屏幕(和graphics、image类) 可以用来编写基于低级应用编程接口的用户界面。这些类给予midlet程序员很大程度的绘画灵活性。程序员可以绘制各种类型的图形元素,例如:线、弧、矩形、圆角矩形、圆、文字(不同颜色、字体、大小)、位图剪辑等等。大部分的游戏midlet是使用基于画布屏幕类的主图形用户界面元素编写的。
一个midlet用户界面通常包含一个或多个屏幕。因为每次只能显示一个屏幕,因此midlet具有良好设计的结构是非常重要的进行,这样就能更加容易地处理屏幕之间内容的切换。
下面的代码段说明了在一个midlet中切换屏幕的方法,基于屏幕类和对应的midlet回调。
代码段1:
class mymidlet extends midlet
{
private firstscreen firstscreen;
private secondscreen secondscreen;
public mymidlet()
{
…
}
public void startapp()
{
displayable current = display.getdisplay(this).getcurrent();
if (current == null)
{
firstscreen = new firstscreen(this, …);
display.getdisplay(this).setcurrent(firstscreen);
//显示应用程序的第一个用户界面屏幕
}
else
{
display.getdisplay(this).setcurrent(current);
}
}
// firstscreen 回调切换到下一个屏幕
public void firstscreendone()
{
…
secondscreen = new secondscreen(this, …);
display.getdisplay(this).setcurrent(secondscreen);
}
// secondscreen回调终止应用程序
public void secondscreenquit()
{
…
destroyapp(false);
notifydestroyed();
}
…
}
这个midlet使用了两个屏幕类(firstscreen和secondscreen)作为用户界面。当开始执行midlet的时候,它设置当前显示屏幕为firstscreen。当需要从firstscreen切换到secondscreen的时候,firstscreen 调用父midlet方法firstscreendone(参见下面的代码)。firstscreendone方法创建并设置secondscreen为当前显示的屏幕。
代码段2: 包含midlet回调的firstscreen示例
class firstscreen extends form implements commandlistener {
private mymidlet midlet;
public firstscreen(mymidlet midlet)
{
this.midlet = midlet;
…
}
public void commandaction(command c)
{
if (c == cmdquit)
{
parent.firstscreendone();
}
…
}
…
}
3、midp用户界面应用编程接口
保证基于高级应用编程接口类的用户界面对象的可移植性和适用性是midp设备的职责。
另一方面,像canvas和graphics这样的低级类为程序员提供了更大的自由空间让其控制其用户界面的视觉表现,并且监听低级键盘事件。程序员还要负责确保应用程序在不同特性(例如显示尺寸、彩色或黑白,以及不同键盘类型)的移动设备上的可移植性。比如说,有可能需要使用getwidth()和getheight()方法调节用户界面外观使其适应一个或更多设备的可用canvas尺寸。
下面的九宫格midlet例程将介绍:
简单应用高级应用编程接口;
使用低级应用编程接口来绘制线、弧、字符串和图像等图形;
不同显示尺寸的移动设备之间的midlet移植问题
键盘代码与游戏动作之间的映射
本章概述了midp图形用户界面的设计,如果想得到更进一步的信息,请参阅
http://java.sun.com/products/midp/ 。
二. 示例:九宫格(tictactoemidlet)
1、设计
概述
这个示例应用程序是一个简单的midlet,允许游戏者与midlet程序之间玩一种称为九宫格的人机游戏。这个例程说明:
使用高级和低级用户界面组件
在多显示屏幕之间进行切换
处理简单的命令
动态适配显示尺寸
处理键盘事件
游戏者首先选择使用哪种棋子(用圆和叉表示),然后开始游戏。游戏者和midlet谁是先手是随机决定的。每走一步棋之后,程序都要检查游戏状态,判断游戏是否已经结束。游戏的几种可能结果是:游戏者赢,midlet程序赢,或者平局。在应用程序运行期间,双方的得分都能显示出来。游戏者可以随时开始新游戏或者退出游戏。
图2:所示的屏幕快照是游戏中的midlet用户界面。
图2:游戏屏幕的先后顺序
2、九宫格midlet
下面是九宫格midlet的类模式图:
图3:九宫格midlet类图
当midlet启动方法startapp()时,将创建闪烁屏幕和第一个游戏屏幕(choosepiecescreen)。闪烁屏幕显示4秒之后,第一个游戏屏幕开始显示。choosepiecescreen让游戏者选择使用哪种棋子(圆还是叉)。当游戏者做出选择之后,他可以使用ok键确认。这会使choosepiecescreen回调主midlet的choicepiecescreendone()方法。
choosepiecescreen是使用高级应用编程接口list类实现的。
图4:choosepiecescreen是一个高级用户界面list子类
choosepiecescreendone()回调创建并显示下一个屏幕,这个屏幕在此应用程序中作为游戏的主屏(gamescreen)。
每当轮到游戏者下棋的时候,游戏者使用gamescreen的箭头键和select按钮来选择想要走的空格。每一回合之后,应用程序都会检查游戏的状态,检查其是否符合游戏结束条件并显示游戏结果。游戏者通过点击gamescreen的quit命令结束游戏,或使用new命令开始新一轮游戏。quit(结束)命令调用tictactoemidlet的quit()方法,然后midlet就会调用destroyapp()方法来终止整个midlet程序。
游戏程序逻辑被封装在一个单独的game类中。本文只关注midlet的图形设计,而对游戏程序逻辑不作深入探讨。如果要与现有的applet java程序作比较,请参阅http://java.sun.com/applets/jdk/1.0/demo/tictactoe/tictactoe.java 和http://java.sun.com/products/jfc/tsc/articles/tictactoe/index.html 中的游戏程序逻辑。
gamescreen通过使用低级canvas和graphics类来实现。它使用canvas、image和graphics对象来绘制图形。
gamescreen首先初始化基于画布尺寸的显示面板。这可让midlet能够运行在不同显示屏幕的移动设备上。在本例中还使用了一个image对象用来表示游戏面板。然后gamescreen根据游戏者在choosepiecescreen中所做的选择为游戏者和midlet分配棋子。游戏然后进行初始化(包括随机决定谁是先手),然后游戏就开始了。
为了使gamescreen能够被移植,midlet的键盘代码必须被映射到游戏动作上,如:up、down、left、right和fire,用于具有不同键盘的移动设备。每当一个键被按下的时候,keypressed()方法就会判断这是一个方向键还是一个fire/select键。如果按下的键是方向键,光标就会相应地移动,帮助游戏者可视化地选择一个空格放入棋子。select键用来选择一个空格放入棋子。如果探测到满足游戏结束的条件,就会显示一条信息宣布游戏的获胜者和本轮游戏的得分。(见下图)
图5: gamescreen 是一个低级 canvas(画布)子类
3、tictactoemidlet.java
tictactoemidlet非常简单:它处理midlet的生命周期事件。它根据需要创建屏幕对象并且处理来自屏幕的回调。choosepiecescreendone回调被用来创建gamescreen。quit方法则被gamescreen用来结束游戏。
package example.tictactoe;
import java.io.ioexception;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.io.*;
public class tictactoemidlet extends midlet {
private choosepiecescreen choosepiecescreen;
private gamescreen gamescreen;
public tictactoemidlet()
{
}
public void startapp() {
displayable current = display.getdisplay(this).getcurrent();
if (current == null) {
// first time we′ve been called
// get the logo image
image logo = null;
try
{
logo = image.createimage("/tictactoe.png");
}
catch (ioexception e) {
// just use null image
}
alert splashscreen = new alert(null, "tic-tac-toe forum nokia", logo, alerttype.info);
splashscreen.settimeout(4000);
// 4 seconds
choosepiecescreen = new choosepiecescreen(this);
display.getdisplay(this).setcurrent(splashscreen, choosepiecescreen);
}
else
{
display.getdisplay(this).setcurrent(current);
}
}
public void pauseapp() {
}
public void destroyapp(boolean unconditional) {
}
public void quit()
{
destroyapp(false);
notifydestroyed();
}
public void choosepiecescreendone(boolean isplayercircle)
{
gamescreen = new gamescreen(this, isplayercircle);
display.getdisplay(this).setcurrent(gamescreen);
}
}
4、choosepiecescreen.java
choosepiecescreen是一个基于高级应用编程接口窗体的屏幕,允许游戏者选择圆或叉作为棋子。当游戏者按下ok键时,它使用midlet的回调方法choosepiecescreendone来处理游戏者的选择。
package example.tictactoe;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.io.*;
public class choosepiecescreen extends list implements commandlistener
{
private static final string circle_text = "circle";
private static final string cross_text = "cross";
private final tictactoemidlet midlet;
private final command quitcommand;
public choosepiecescreen(tictactoemidlet midlet) {
super("choose your piece", list.implicit);
this.midlet = midlet;
append(circle_text, loadimage("/circle.png"));
append(cross_text, loadimage("/cross.png"));
quitcommand = new command("quit", command.exit, 2);
addcommand(quitcommand);
setcommandlistener(this);
}
public void commandaction(command c, displayable d) {
boolean isplayercircle = getstring(getselectedindex()).equals(circle_text);
if (c == list.select_command) {
midlet.choosepiecescreendone(isplayercircle);
}
else
// quit command
{
midlet.quit();
}
}
private image loadimage(string imagefile)
{
image image = null;
try
{
image = image.createimage(imagefile);
}
catch (exception e)
{
// use a ′null′ image in the choice list (i.e. text only choices).
}
return image;
}
}
5、gamescreen.java
gamescreen使用了一个低级应用编程接口canvas屏幕,和image、graphics类来绘制游戏面板、棋子,以及游戏的最终结果状态。要获取更详细的信息,请参阅各种绘画方法和drawcircle、drawcross、drawpiece、drawplayercursor、drawboard等方法。这个屏幕使用midlet的quit回调方法来指示游戏结束。
此屏幕可适应各种可用显示性能(高、宽、色彩等)。此外还要注意到可以使用四向导航键,也可以使用双向导航键来移动光标。
它使用了封装了主游戏程序逻辑的game类。
package example.tictactoe;
import java.util.random;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
class gamescreen extends canvas implements commandlistener {
private static final int black = 0x00000000;
private static final int white = 0x00ffffff;
private static final int red = 0x00ff0000;
private static final int blue = 0x000000ff;
private static final int no_move = -1;
private final tictactoemidlet midlet;
private final game game;
private final command exitcommand;
private final command newgamecommand;
private final random random = new random();
private int screenwidth, screenheight;
private int boardcellsize, boardsize, boardtop, boardleft;
private boolean playeriscircle;
private boolean computeriscircle;
private int precursorposition, cursorposition;
private int computermove = no_move;
private int playermove = no_move;
private int computergameswontally = 0;
private int playergameswontally = 0;
private boolean isrestart;
public gamescreen(tictactoemidlet midlet, boolean playeriscircle) {
this.midlet = midlet;
this.playeriscircle = playeriscircle;
computeriscircle = !playeriscircle;
game = new game(random);
initializeboard();
// configure screen commands
exitcommand = new command("exit", command.exit, 1);
newgamecommand = new command("new", command.screen, 2);
addcommand(exitcommand);
addcommand(newgamecommand);
setcommandlistener(this);
// begin the game play initialize();
}
// initialize the game and game screen. also used for game restarts.
private void initialize() {
game.initialize();
precursorposition = cursorposition = 0;
playermove = no_move;
boolean computerfirst = ((random.nextint() & 1) == 0);
if (computerfirst) {
computermove = game.makecomputermove();
}
else
{
computermove = no_move;
}
isrestart = true;
repaint();
}
public void paint(graphics g) {
if (game.isgameover()) {
paintgameover(g);
}
else {
paintgame(g);
}
}
private void paintgame(graphics g) {
if (isrestart) {
// clean the canvas
g.setcolor(white);
g.fillrect(0, 0, screenwidth, screenheight);
drawboard(g);
isrestart = false;
}
drawcursor(g);
if (playermove != no_move) {
drawpiece(g, playeriscircle, playermove);
}
if (computermove != no_move) {
drawpiece(g, computeriscircle, computermove);
}
}
private void paintgameover(graphics g)
{
string statusmsg = null;
if(game.iscomputerwinner()) {
statusmsg = "i win !";
computergameswontally++;
}
else if (game.isplayerwinner()) {
statusmsg = "you win";
playergameswontally++;
}
else {
statusmsg = "stalemate";
}
string tallymsg = "you:" + playergameswontally + " me:" + computergameswontally;
font font = font.getfont(font.face_system, font.style_plain, font.size_medium);
int strheight = font.getheight();
int statusmsgwidth = font.stringwidth(statusmsg);
int tallymsgwidth = font.stringwidth(tallymsg);
int strwidth = tallymsgwidth;
if (statusmsgwidth > tallymsgwidth)
{
strwidth = statusmsgwidth;
}
// get the
{
x, y
}
position for painting the strings. int x = (screenwidth - strwidth) / 2;
x = x < 0 ? 0 : x;
int y = (screenheight - 2 * strheight) / 2;
y = y < 0 ? 0 : y;
// clean the canvas
g.setcolor(white);
g.fillrect(0, 0, screenwidth, screenheight);
// paint the strings′ text
g.setcolor(black);
g.drawstring(statusmsg, x, y, (graphics.top | graphics.left));
g.drawstring(tallymsg, x, (y + 1 + strheight), (graphics.top | graphics.left));
}
public void commandaction(command c, displayable d) {
if (c == exitcommand) {
midlet.quit();
}
else if (c == newgamecommand) {
initialize();
}
}
private void initializeboard() {
screenwidth = getwidth();
screenheight = getheight();
if (screenwidth > screenheight) {
boardcellsize = (screenheight - 2) / 3;
boardleft = (screenwidth - (boardcellsize * 3)) / 2;
boardtop = 1;
}
else {
boardcellsize = (screenwidth - 2) / 3;
boardleft = 1;
boardtop = (screenheight - boardcellsize * 3) / 2;
}
}
protected void keypressed(int keycode) {
// can′t continue playing until the player restarts
if (game.isgameover()) {
return;
}
int gameaction = getgameaction(keycode);
switch (gameaction) {
case fire: doplayermove();
break;
case right: domovecursor(1, 0);
break;
case down: domovecursor(0, 1);
break;
case left: domovecursor(-1, 0);
break;
case up: domovecursor(0, -1);
break;
default: break;
}
}
private void doplayermove() {
if (game.isfree(cursorposition)) {
// player move game.
makeplayermove(cursorposition);
playermove = cursorposition;
// computer move
if (!game.isgameover()) {
computermove = game.makecomputermove();
}
repaint();
}
}
private void domovecursor(int dx, int dy) {
int newcursorposition = cursorposition + dx + 3 * dy;
if ((newcursorposition >= 0) && (newcursorposition < 9))
{
precursorposition = cursorposition;
cursorposition = newcursorposition;
repaint();
}
}
// draw a circle or cross piece on the board
private void drawpiece(graphics g, boolean iscircle, int pos) {
int x = ((pos % 3) * boardcellsize) + 3;
int y = ((pos / 3) * boardcellsize) + 3;
if (iscircle) {
drawcircle(g, x, y);
}
else {
drawcross(g, x, y);
}
}
// draw blue circle onto the board image
private void drawcircle(graphics g, int x, int y) {
g.setcolor(blue);
g.fillarc(x + boardleft, y + boardtop, boardcellsize - 4, boardcellsize - 4, 0, 360);
g.setcolor(white);
g.fillarc(x + 4 + boardleft, y + 4 + boardtop, boardcellsize - 4 - 8, boardcellsize - 4 - 8, 0, 360);
}
// draw red cross onto the board image
private void drawcross(graphics g, int x, int y) {
g.setcolor(red);
for (int i = 0;
i < 4;
i++) {
g.drawline(x + 1 + i + boardleft, y + boardtop, x + boardcellsize - 4 - 4 + i + boardleft, y + boardcellsize - 5 + boardtop);
g.drawline(x + 1 + i + boardleft, y + boardcellsize - 5 + boardtop, x + boardcellsize - 4 - 4 + i + boardleft, y + boardtop);
}
}
// visually indicates a player selected square on the board image
private void drawcursor(graphics g) {
// draw cursor at selected player square.
g.setcolor(white);
g.drawrect(((precursorposition % 3) * boardcellsize) + 2 + boardleft, ((precursorposition/3) * boardcellsize) + 2 + boardtop, boardcellsize - 3, boardcellsize - 3);
// draw cursor at selected player square.
g.setcolor(black);
g.drawrect(((cursorposition % 3) * boardcellsize) + 2 + boardleft, ((cursorposition/3) * boardcellsize) + 2 + boardtop, boardcellsize - 3, boardcellsize - 3);
}
private void drawboard(graphics g) {
// clean the board
g.setcolor(white);
g.fillrect(0, 0, screenwidth, screenheight);
// draw the board
g.setcolor(black);
for (int i = 0;
i < 4;
i++) {
g.fillrect(boardleft, boardcellsize * i + boardtop, (boardcellsize * 3) + 2, 2);
g.fillrect(boardcellsize * i + boardleft, boardtop, 2, boardcellsize * 3);
}
}
}
6、game.java
这个类封装了九宫格游戏的主要的游戏程序逻辑。前面我们也说过,游戏程序逻辑本身并不在本例程重点讨论的范围之内,本文主要是介绍midp图形编程的基础知识。游戏程序逻辑的wins数组部分来自http://java.sun.com/applets/jdk/1.0/demo/tictactoe/tictactoe.java 这个经典例程。
注意游戏程序逻辑是独立于游戏用户界面的(参见类gamescreen),并且可以使用其它实现方法替代。
package example.tictactoe;
import java.util.random;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
// the game logic for tictactoe
class game {
private static final int[] wins = {
// horizontals
bit(0) | bit(1) | bit(2),
bit(3) | bit(4) | bit(5),
bit(6) | bit(7) | bit(8),
// verticals
bit(0) | bit(3) | bit(6),
bit(1) | bit(4) | bit(7),
bit(2) | bit(5) | bit(8),
// diagonals
bit(0) | bit(4) | bit(8),
bit(2) | bit(4) | bit(6) }
;
private static final int drawn_game = bit(0) | bit(1) | bit(2) | bit(3) | bit(4) | bit(5) | bit(6) | bit(7) | bit(8);
private int playerstate;
private int computerstate;
private random random;
game(random random) {
this.random = random;
initialize();
}
void initialize() {
playerstate = 0;
computerstate = 0;
}
boolean isfree(int position) {
int bit = bit(position);
return (((playerstate & bit) == 0) && ((computerstate & bit) == 0));
}
// the ′contract′ is that caller will always make valid moves.
// we don′t check that it′s the player′s turn.
void makeplayermove(int position) {
playerstate |= bit(position);
}
// the ′contract′ is that we will be called only when there is still
// at least one free square.
int makecomputermove() {
int move = getwinningcomputermove();
if (move == -1) {
// can′t win
move = getrequiredblockingcomputermove();
if (move == -1) {
// don′t need to block
move = getrandomcomputermove();
}
}
computerstate |= bit(move);
return move;
}
boolean isgameover() {
return isplayerwinner() | iscomputerwinner() | isgamedrawn();
}
boolean isplayerwinner() {
return iswin(playerstate);
}
boolean iscomputerwinner() {
return iswin(computerstate);
}
boolean isgamedrawn() {
return (playerstate | computerstate) == drawn_game;
}
// return a winning move if there is at least one, otherwise return -1
private int getwinningcomputermove() {
int move = -1;
for (int i = 0;
i < 9;
++i) {
if (isfree(i) && iswin(computerstate | bit(i))) {
move = i;
break;
}
}
return move;
}
// return a required blocking move if there is at least one (more
// than one and we′ve inevitably lost), otherwise return -1
private int getrequiredblockingcomputermove() {
int move = -1;
for (int i = 0;
i < 9;
++i) {
if (isfree(i) && iswin(playerstate | bit(i))) {
move = i;
break;
}
}
return move;
}
// return a random move in a free square, // or return -1 if none are available private int getrandomcomputermove() {
int move = -1;
// determine how many possible moves there are int numfreesquares = 0;
for (int i = 0;
i < 9;
++i) {
if (isfree(i)) {
numfreesquares++;
}
}
// if there is at least one possible move, pick randomly
if (numfreesquares > 0) {
// shift twice to get rid of sign bit, then modulo numfreesquares
int pick = ((random.nextint()<<1)>>>1) % numfreesquares;
// now find the chosen free square by counting pick down to zero
for (int i = 0;
i < 9;
++i) {
if (isfree(i)) {
if (pick == 0) {
move = i;
break;
}
pick--;
}
}
}
return move;
}
private static boolean iswin(int state) {
boolean iswinner = false;
for (int i = 0;
i < wins.length;
++i) {
if ((state & wins[i]) == wins[i]) {
iswinner = true;
break;
}
}
return iswinner;
}
private static int bit(int i) {
return 1 << i;
}
}
7、tictactoe.jad
下面是九宫格midlet的应用程序描述文件。
midlet-name: tictactoe
midlet-vendor: forum nokia midlet-version: 1.1.1
midlet-jar-size: 11409
midlet-jar-url: tictactoe.jar
midlet-1: tictactoe, /tictactoe.png, example.tictactoe.tictactoemidlet
闽公网安备 35060202000074号