一个J2ME拼图游戏的开发

廖雪峰 / 文章 / ... / Reads: 3402 Edit

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模式的典型应用。

由于这个拼图游戏不需要频繁地更新画面,因此,连多线程也不必使用了,这样就大大简化了游戏逻辑的设计。下面是这个拼图游戏运行在真实手机上的效果图:

j2me-puzzle-snapshot

由于公司的手机还停留在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:

j2me-puzzle-res

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,即可实现整个游戏的基本框架。

游戏源码和二进制包下载:

http://javaeedev.googlecode.com/files/Puzzle.zip

Comments

Make a comment

Author: 廖雪峰

Publish at: ...

关注公众号不定期领红包:

关注微博获取实时动态: