MIDP规范的出现使得我们在手机上开发Java游戏成为可能。今天我们要实现的是一个简单的拼图游戏。这个拼图游戏是一个3x3的拼图,由9个分割的小图片构成。这样,在手机上,就可以用按键1-9对应每个图片。需要移动某个图片时,用户只需要按下对应的数字键即可,非常方便。当然,对于键盘设计不规则的手机来说,就只能委屈了。当用户按下0键时,则显示整个原始图片。
虽然MIDP提供了许多高级和低级的UI API接口,但是整个MIDP应用程序的结构设计仍然至关重要,一个灵活的框架能大大降低游戏开发的复杂度。
MVC模式几乎是UI应用程序开发的标准模式了,通过Model-View-Controller的分工合作,使得整个应用程序的不同功能部分被分离开来,从而降低开发难度。
MVC有MVC1和MVC2两种模式,其不同之处在于Model能否主动通知View。在普通的Windows窗口程序中,Model可以主动通知View是否需要Update,因此应使用MVC1;在Web程序中,由于HTTP协议的限制,服务器端的Model无法主动通知View(如JSP页面),因此只能使用MVC2,由Controller取得Model并渲染View。
在窗口应用程序中,View通常仅有一个,但Model可能有很多;而在Web程序中,Model通常被放在服务器端,每一个JSP页面都是一个View,因此View有很多个。
微软的MFC框架也是一个基于MVC模式的框架,其View-Document框架是专门针对桌面应用程序设计的,因此,我们在MIDP程序中也可借鉴其思想。
在MIDP程序中,MIDlet起着Controller的作用,每个Screen或者Canvas就是一个View,而Model可以用一个单独的类来表示,用于存储程序运行中的数据。对于这个拼图游戏来说,设计以下几个类:
- PuzzleMIDlet:控制整个游戏的生命周期,也是应用程序的入口;
- MainCanvas:绘制游戏的主屏幕,完成所有绘图操作;
- Document:存储游戏运行过程中的数据,并负责通知屏幕更新。
当用户通过MainCanvas输入命令后(例如,按下0-9的某个键),将可能引起Document数据的更新,如果需要更新屏幕,则Document应通知View更新显示,这是一个Observer模式的典型应用。
由于这个拼图游戏不需要频繁地更新画面,因此,连多线程也不必使用了,这样就大大简化了游戏逻辑的设计。下面是这个拼图游戏运行在真实手机上的效果图:
由于公司的手机还停留在CF62 / MIDP1.0的水平,因此,只好用MIDP1.0来编写这个拼图游戏了。不过好在我们的重点不是在如何绘制Canvas上,因此,MIDP2.0中提供的新的Game API绝大部分都用不上。
下面,我们开始设计每个类,并实现整个完整的游戏逻辑。
设计Document类
Document类需要保存游戏运行中所有的状态数据,对于这个拼图游戏来说,我们设计以下成员变量:
Updatable updatable;
int state;
Image[] images = new Image[9];
int[][] current = new int[3][3];
int hiddenX, hiddenY;
int steps; // 移动的步数
MainCanvas需要实现Updatable接口,因此,Document保存了一个View的引用,在恰当的时候,Document可以调用updatable.update()方法通知View需要重绘。这样,MainCanvas和Document就实现了Observer模式。
游戏中,state用于存储游戏状态,一共有3种状态:
- PUZZLE_STATE:表示用户正在进行拼图中;
- IMAGE_STATE:表示用户正在查看原始图片;
- FINISH_STATE:表示用户已经完成拼图。
images数组按次序存储原始图片,我们把这个90x90大小的原始图片切割成9个30x30的小图片,并依次编号0-8:
current[3][3]是一个二维数组,存储Image在images[]数组中的索引号,这样就可以从current[][]中获得对应的Image对象。
hiddenX和hiddenY用来标识空白方格的位置。仅当位于(hiddenX, hiddenY)上下左右的方格可以移动。
初始化current
为了打乱一个拼好的方格,我们需要一个算法来随机打乱9个方格。在我们想出这个算法前,最简单的方法便是用一个可拼好的数据来写死current[][],使得我们能集中精力先把游戏的框架搭起来:
current = new int[][] {
{2, 7, 5},
{1, 0, 6},
{4, 3, 8}
}
然后设定hiddenX=2, hiddenY=2,使得右下角current[2][2]的方格被隐藏。
要取得某个方格对应的Image对象,我们用
public Image getCurrentImage(int x, int y) {
if( (x==hiddenX) && (y==hiddenY) )
return null;
return images[current[x][y]];
}
对于位于(hiddenX, hiddenY)位置的方格,返回null表示不显示该方格。
如何判断拼图是否完成?当current[][]数组的内容按照{0, 1, 2}, {3, 4, 5}, {6, 7, 8}排列时,表示该拼图已经拼好,因此,判断代码非常简单:
public boolean isFinish() {
for(int i=0; i<3; i++) {
for(int j=0; j<3; j++) {
if(current[i][j]!=(i*3+j))
return false;
}
}
return true;
}
当用户移动某个方格时,Document接收方格位置(x, y)并负责判断能否移动,如果能,更新current[][]的数据和hiddenX, hiddenY,并返回true表示数据已更新,否则返回false表示不可移动。
public boolean move(int x, int y) {
// 如果用户试图移动隐藏方格,直接返回false:
if(hiddenX==x && hiddenY==y)
return false;
// 如果方格位于(hiddexX, hiddenY)的相邻位置,
// 交换该方格(x, y)和(hiddenX, hiddenY)的相关数据:
boolean moved = false;
if( ((x-1)==hiddenX) && (y==hiddenY) ) {
sweep(x, y);
moved = true;
}
if( ((x+1)==hiddenX) && (y==hiddenY) ) {
sweep(x, y);
moved = true;
}
if( (x==hiddenX) && ((y-1)==hiddenY) ) {
sweep(x, y);
moved = true;
}
if( (x==hiddenX) && ((y+1)==hiddenY) ) {
sweep(x, y);
moved = true;
}
if(moved) {
steps++;
if(isFinish()) {
// TODO...
}
updatable.update();
}
}
private void sweep(int x, int y) {
int temp = current[x][y];
current[x][y] = current[hiddenX][hiddenY];
current[hiddenX][hiddenY] = temp;
hiddenX = x;
hiddenY = y;
}
至此,Document类基本完成。Document不涉及任何显示功能,仅仅存储和更新数据,并在恰当的时候通知View更新显示。
实现View
在MIDP中,View就是Screen或者Canvas,在这个游戏中,我们应该使用Canvas,定义:
public class MainCanvas extends Canvas implements CommandListener, Updatable { ... }
在构造方法中,初始化Document:
public MainCanvas(String imageName) {
// 读图像:
Image[] images = new Image[9];
for(int i=0; i<9; i++) {
try {
images[i] = Image.createImage("/image/" + i + ".png");
}
catch(IOException ioe) {}
}
document = new Document(this, images, 2, 2);
}
在paint()方法中,MainCanvas从Document中获得数据,然后更新画面:
protected void paint(Graphics g) {
g.fillRect(0,0,getWidth(),getHeight());
// 获得当前状态:
int state = document.getState();
if(state==Document.PUZZLE_STATE) {
for(int x=0; x<3; x++) {
for(int y=0; y<3; y++) {
Image image = document.getImage(x, y);
if(image!=null) {
g.drawImage(image, y*IMAGE_WIDTH, x*IMAGE_WIDTH, Graphics.LEFT|Graphics.TOP);
}
else {
g.setColor(0x000000);
g.fillRect(y*IMAGE_WIDTH, x*IMAGE_WIDTH, IMAGE_WIDTH, IMAGE_WIDTH);
}
}
}
// draw line:
g.setColor(0xffffff);
for(int i=0; i<=3; i++) {
g.drawLine(0, i*IMAGE_WIDTH, 3*IMAGE_WIDTH, i*IMAGE_WIDTH);
g.drawLine(i*IMAGE_WIDTH, 0, i*IMAGE_WIDTH, 3*IMAGE_WIDTH);
}
}
else {
// TODO...
}
}
当用户按下某个键时,MainCanvas的keyPressed()方法被执行,然后将用户输入数据传递给Document:
protected void keyPressed(int keyCode) {
switch(keyCode) {
case KEY_NUM1:
document.move(0,0);
break;
case KEY_NUM2:
document.move(0,1);
break;
case KEY_NUM3:
document.move(0,2);
break;
// TODO: case KEY_NUM4, 5, 6...
}
}
然后,Document可能更新自身内部状态,如果需要重绘画面,Document将调用update()回调方法来通知View更新画面。因此,MainCanvas必须实现Updatable接口的update()回调方法:
public void update() {
repaint();
}
至此,View已基本实现,我们再添加一个用作启动的MIDlet,即可实现整个游戏的基本框架。
游戏源码和二进制包下载: