J2ME游戏开发实例讲解

接下来,是实现paint()函数,我们打算在这一部分中反复的重画背景、地图和选择区域,这个函数必须处理好区域被选中之后的画笔颜色的切换,具体讲就是在没有选中任何区域时要用黑色画笔,当选重要移动的区域时使用绿色画笔,当选择要移动到的区域时改用红色画笔(当然附加一张流程图是必不可少的)。

再下面要实现的setRange ()函数和setMoveRange()函数,这两个函数用来设置要移动的区域和要移动到的区域,我的思路就是利用前面在Images类中介绍过的地图组合标记常量,当移动到地图组合标记常量时,根据该点地图中的值做逆向变换找到相应的地图标记常量,然后设置相应的loc、SelectArea和 MoveArea,其中setMoveRange()函数还用到了一个辅助函数isInRange(),isInRange()函数是用来判断给定的点是否在已选中的要移动的区域之内,如果isInRange()的返回值是假并且该点处的值不是空白就表明要移动到的区域侵犯了其他以被占用的区域。有了 setRange()和setMoveRange()函数,Move()函数就水到渠成了,Move()函数将要移动的区域移动到要移动到的区域,在移动过程中分为三步进行:

 第一.复制要移动的区域;

 第二.将复制出的要移动区域复制到要移动到的区域(这两步分开进行的目的是防止在复制过程中覆盖掉要移动的区域);

 第三.用isInRange2()判断给定的点是否在要移动到的区域内,将不在要移动到的区域内的点设置成空白。

 下面我们详细的分析一下keyPressed()函数的实现方法:首先,keyPressed()函数要处理按键的上下左右和选中(Fire),在处理时需要用Canvas类的getGameAction函数来将按键的键值转换成游戏的方向,这样可以提高游戏的兼容性(因为不同的J2ME实现,其方向键的键值不一定是相同的)。

 接下来,分别处理四个方向和选中.当按下向上时,先判断是否已经选定了要移动的区域(即 this.selected是否为真),如果没有选中要移动区域则让光标向上移动一格,然后调用setRange()函数设置选择要移动的区域,再调用 repaint()函数刷新屏幕,否则如果已经选中了要移动的区域,就让光标向上移动一格,然后调用setMoveRange()函数判断是否能够向上移动已选中的区域,如果能移动就调用repaint()函数刷新屏幕,如果不能移动就让光标向下退回到原来的位置。

 当按下向下时,先判断是否已经选定了要移动的区域,如果没有选中要移动的区域则判断当前所处的区域是否为两个格高,如果是两个格高则向下移动两格,如果是一个格高则向下移动一格,接着再调用setRange()函数设置选择要移动的区域,而后调用repaint()函数刷新屏幕,否则如果已经选中了要移动的区域,就让光标向下移动一格,然后调用setMoveRange()函数判断是否能够向下移动已选中的区域,如果能移动就调用repaint()函数刷新屏幕,如果不能移动就让光标向上退回到原来的位置.按下向左时情况完全类似向上的情况,按下向右时情况完全类似向下的情况,因此这里不再赘述,详细情况请参见程序的源代码。

 当按下选中键时,先判断是否已经选中了要移动的区域,如果已经选中了要移动的区域就调用Move()函数完成由要移动的区域到要移动到的区域的移动过程,接着调用repaint()函数刷新屏幕,然后将已选择标记置成false,继续调用win()函数判断是否完成了任务,否则如果还没有选定要移动的区域则再判断当前选中区域是否为空白,如果不是空白就将选中标记置成true,然后刷新屏幕.这里介绍一个技巧,在开发程序遇到复杂的逻辑的时候,可以构造一格打印函数来将所关心的数据结构打印出来以利调试,这里我们就构造一个PrintGrid()函数,这个函数纯粹是为了调试之用,效果这得不错.至此我们完成了编码前的全部工作。

 六.编码

 整个项目共有五个类,有四个类的代码前面已经介绍过了,而且是在其他项目中使用过的相对成熟的代码.现在只需全力去实现Displayable1类.Displayable1类的代码如下:

 

  package huarongroad;

 import javax.microedition.lcdui.*;

 public class Displayable1 extends Canvas implements CommandListener {

  private int[] loc = new int[2]; <A href="file://光">file://光</A>标的当前位置,0是水平位置,1是竖直位置
   private int[] SelectArea = new int[4];//被选定的区域,即要移动的区域
  private int[] MoveArea = new int[4];//要移动到的区域
  private Map MyMap = new Map();//地图类
  private boolean selected;//是否已经选中要移动区域的标志
  private int level;//但前的关面
  public Displayable1() {//构造函数
   try {
    jbInit();//JBuilder定义的初始化函数
   }catch (Exception e) {
    e.printStackTrace();
   }
  }
  private void Init_game(){
//   初始化游戏,读取地图,设置选择区域,清空要移动到的区域
   this.loc = MyMap.read_map(this.level);//读取地图文件,并返回光标的初始位置
//   0为水平位置,1为竖直位置
   this.SelectArea[0] = this.loc[0];//初始化选中的区域
   this.SelectArea[1] = this.loc[1];
   this.SelectArea[2] = 1;
   this.SelectArea[3] = 1;
   this.MoveArea[0] = -1;//初始化要移动到的区域
   this.MoveArea[1] = -1;
   this.MoveArea[2] = 0;
   this.MoveArea[3] = 0;
  }
  private void jbInit() throws Exception {//JBuilder定义的初始化函数
   <A href="file://初">file://初</A>始化实例变量
    this.selected = false;//设置没有被选中的要移动区域
   this.level = 1;
   Images.init();//初始化图片常量
   Init_game();//初始化游戏,读取地图,设置选择区域,清空要移动到的区域
   setCommandListener(this);//添加命令监听,这是Displayable的实例方法
   addCommand(new Command("Exit", Command.EXIT, 1));//添加“退出”按钮
  }

  public void commandAction(Command command, Displayable displayable) {
//   命令处理函数
   if (command.getCommandType() == Command.EXIT) {//处理“退出”
    MIDlet1.quitApp();
   }
  }

  protected void paint(Graphics g) {
//   画图函数,用于绘制用户画面,即显示图片,勾画选中区域和要移动到的区域
   try {
    g.drawImage(Images.image_Frame, 0, 0,
      Graphics.TOP | Graphics.LEFT);//画背景
    MyMap.draw_map(g);//按照地图内容画图
    if ( this.selected )
     g.setColor(0,255,0);//如果被选中,改用绿色画出被选中的区域
    g.drawRect(this.SelectArea[0] * Images.UNIT + Images.LEFT,
      this.SelectArea[1] * Images.UNIT + Images.TOP,
      this.SelectArea[2] * Images.UNIT,
      this.SelectArea[3] * Images.UNIT);//画出选择区域,
    <A href="file://如">file://如</A>果被选中,就用绿色
     <A href="file://否">file://否</A>则,使用黑色
      g.setColor(255,255,255);//恢复画笔颜色
    if (this.selected) {//已经选中了要移动的区域
     g.setColor(255, 0, 255);//改用红色
     g.drawRect(this.MoveArea[0] * Images.UNIT + Images.LEFT,
       this.MoveArea[1] * Images.UNIT + Images.TOP,
       this.MoveArea[2] * Images.UNIT,
       this.MoveArea[3] * Images.UNIT);//画出要移动到的区域
     g.setColor(255, 255, 255);//恢复画笔颜色
    }
   }catch (Exception ex) {
   }
   System.out.println(Runtime.getRuntime().freeMemory());
   System.out.println(Runtime.getRuntime().totalMemory());
  }

  private void setRange() {
//   设置移动后能够选中的区域
//   调整当前光标位置到地图的主位置,即记录人物信息的位置
   if (this.MyMap.Grid[this.loc[1]][this.loc[0]] == Images.DLEFT) {
    this.loc[0] -= 1;//向左调
   }else if (this.MyMap.Grid[this.loc[1]][this.loc[0]] == Images.DUP) {
    this.loc[1] -= 1;//向上调
   }else if (this.MyMap.Grid[this.loc[1]][this.loc[0]] == Images.DLEFTUP) {
    this.loc[0] -= 1;//向左调
    this.loc[1] -= 1;//向上调
   }
   this.SelectArea[0] = this.loc[0];//设置光标的水平位置
   this.SelectArea[1] = this.loc[1];//设置光标的竖直位置
//   设置光标的宽度
   if (this.loc[0] + 1 < Images.WIDTH) {
    this.SelectArea[2] = this.MyMap.Grid[this.loc[1]][this.loc[0] + 1] != (byte) ‘1’ ?
      1 : 2;
   }else {
    this.SelectArea[2] = 1;
   }
//   设置光标的高度
   if (this.loc[1] + 1 < Images.HEIGHT) {
    this.SelectArea[3] = this.MyMap.Grid[this.loc[1] + 1][this.loc[0]] != (byte) ‘2’ ?
      1 : 2;
   }else {
    this.SelectArea[3] = 1;
   }
  }

  private boolean setMoveRange() {
//   设置要移动到的区域,能够移动返回true,否则返回false
   for (int i = 0; i < this.SelectArea[2]; i++) {
    for (int j = 0; j < this.SelectArea[3]; j++) {
     if (this.loc[1] + j >= Images.HEIGHT ||
       this.loc[0] + i >= Images.WIDTH ||
       (!isInRange(this.loc[0] + i, this.loc[1] + j) &&
         this.MyMap.Grid[this.loc[1] + j][this.loc[0] + i] !=
          Images.BLANK)) {
      return false;
     }
    }
   }
   this.MoveArea[0] = this.loc[0];
   this.MoveArea[1] = this.loc[1];
   this.MoveArea[2] = this.SelectArea[2];
   this.MoveArea[3] = this.SelectArea[3];
   return true;
  }

  private boolean isInRange(int x, int y) {
//   判断给定的(x,y)点是否在选定区域之内,x是水平坐标,y是竖直坐标
   if (x >= this.SelectArea[0] &&
     x < this.SelectArea[0] + this.SelectArea[2] &&
     y >= this.SelectArea[1] &&
     y < this.SelectArea[1] + this.SelectArea[3]) {
    return true;
   }else {
    return false;
   }
  }

  private boolean isInRange2(int x, int y) {
//   判断给定的(x,y)点是否在要移动到的区域之内,x是水平坐标,y是竖直坐标
   if (x >= this.MoveArea[0] &&
     x < this.MoveArea[0] + this.MoveArea[2] &&
     y >= this.MoveArea[1] &&
     y < this.MoveArea[1] + this.MoveArea[3]) {
    return true;
   }else {
    return false;
   }
  }

  protected void keyPressed(int keyCode) {
//   处理按下键盘的事件,这是Canvas的实例方法
   switch (getGameAction(keyCode)) {//将按键的值转化成方向常量
   case Canvas.UP://向上
    if (!this.selected) {//还没有选定要移动的区域
     if (this.loc[1] – 1 >= 0) {//向上还有移动空间
      this.loc[1]–;//向上移动一下
      setRange();//设置光标移动的区域,该函数能将光标移动到地图主位置
      repaint();//重新绘图
     }
    }else {//已经选定了要移动的区域
     if (this.loc[1] – 1 >= 0) {//向上还有移动空间
      this.loc[1]–;//向上移动一下
      if (setMoveRange()) {//能够移动,该函数能够设置要移动到的区域
       repaint();//重新绘图
      }else {//不能移动
       this.loc[1]++;//退回来
      }
     }
    }
    break;
   case Canvas.DOWN://向下
    if (!this.selected) {//还没有选定要移动的区域
     if (this.loc[1] + 1 < Images.HEIGHT) {//向下还有移动空间
      if (this.MyMap.Grid[this.loc[1] + 1][this.loc[0]] ==
       Images.DUP){//该图片有两个格高
       this.loc[1]++;//向下移动一下
       if (this.loc[1] + 1 < Images.HEIGHT) {//向下还有
        <A href="file://移">file://移</A>动空间
         this.loc[1]++;//向下移动一下
        setRange();//设置光标移动的区域,
        <A href="file://该">file://该</A>函数能将光标移动到地图主位置
         repaint();//重新绘图
       }else {//向下没有移动空间
        this.loc[1]–;//退回来
       }
      }else {//该图片只有一个格高
       this.loc[1]++;//向下移动一下
       setRange();//设置光标移动的区域,
       <A href="file://该">file://该</A>函数能将光标移动到地图主位置
        repaint();//重新绘图
      }
     }else {
     }
    }else {//已经选定了要移动的区域
     if (this.loc[1] + 1 < Images.HEIGHT) {//向下还有移动空间
      this.loc[1]++;//向下移动一下
      if (setMoveRange()) {//能够移动,该函数能够设置要移动到的区域
       repaint();//重新绘图
      }else {//不能移动
       this.loc[1]–;//退回来
      }
     }
    }
    break;
   case Canvas.LEFT://向左
    if (!this.selected) {//还没有选定要移动的区域
     if (this.loc[0] – 1 >= 0) {//向左还有移动空间
      this.loc[0]–;//向左移动一下
      setRange();//设置光标移动的区域,该函数能将光标移动到地图主位置
      repaint();//重新绘图
     }
    }else {//已经选定了要移动的区域
     if (this.loc[0] – 1 >= 0) {//向左还有移动空间
      this.loc[0]–;//向左移动一下
      if (setMoveRange()) {//能够移动,该函数能够设置要移动到的区域
       repaint();//重新绘图
      }else {//不能移动
       this.loc[0]++;//退回来
      }
     }
    }
    break;
   case Canvas.RIGHT://向右
    if (!this.selected) {//还没有选定要移动的区域
     if (this.loc[0] + 1 < Images.WIDTH) {//向右还有移动空间
      if (this.MyMap.Grid[this.loc[1]][this.loc[0] + 1] ==
       Images.DLEFT) {//该图片有两个格宽
       this.loc[0]++;//向右移动一下
       if (this.loc[0] + 1 < Images.WIDTH) {//向右还有
        <A href="file://移">file://移</A>动空间
         this.loc[0]++;//向右移动一下
        setRange();//设置光标移动的区域,
        <A href="file://该">file://该</A>函数能将光标移动到地图主位置
         repaint();//重新绘图
       }else {//向右没有移动空间
        this.loc[0]–;//退回来
       }
      }else {//该图片只有一个格宽
       this.loc[0]++;//向右移动一下
       setRange();//设置光标移动的区域,
       <A href="file://该">file://该</A>函数能将光标移动到地图主位置
        repaint();//重新绘图
      }
     }else {
     }
    }else {//已经选定了要移动的区域
     if (this.loc[0] + 1 < Images.WIDTH) {//向右还有移动空间
      this.loc[0]++;//向右移动一下
      if (setMoveRange()) {//能够移动,该函数能够设置要移动到的区域
       repaint();//重新绘图
      }else {//不能移动
       this.loc[0]–;//退回来
      }
     }
    }
    break;
   case Canvas.FIRE:
    if (this.selected) {//已经选定了要移动的区域
     Move();//将要移动的区域移动到刚选中的区域
     repaint();//重新绘图
     this.selected = false;//清除已选定要移动区域的标志
     if ( win()) {
      System.out.println("win");
     }
    }else {//还没有选定要移动的区域
     if (this.MyMap.Grid[this.loc[1]][this.loc[0]] ==
      Images.BLANK) {//要移到的位置是一个空白
     }else {//要移到的位置不是空白
      this.selected = true;//设置已选定要移动区域的标志
     }
     repaint();//重新绘图
    }
    break;
   }
  }

  private boolean win(){
   <A href="file://判">file://判</A>断是否已经救出了曹操
    if ( this.MyMap.Grid[Images.HEIGHT – 2 ][Images.WIDTH – 3 ] == Images.CAOCAO )
     return true;
    else
     return false;
  }

  private void PrintGrid(String a) {
   <A href="file://打">file://打</A>印当前地图的内容,用于调试
    System.out.println(a);
   for (int i = 0; i < Images.HEIGHT; i++) {
    for (int j = 0; j < Images.WIDTH; j++) {
     System.out.print( (char)this.MyMap.Grid[i][j]);
    }
    System.out.println("");
   }
  }

  private void Move() {
   <A href="file://将">file://将</A>要移动的区域移动到刚选中的区域
    if (this.MoveArea[0] == -1 || this.MoveArea[1] == -1 ||
      this.SelectArea[0] == -1 || this.SelectArea[1] == -1) {//没有选中区域
    }else {//已经选中了要移动的区域和要移动到的区域
     byte[][] temp = new byte[this.SelectArea[3]][this.SelectArea[2]];
     <A href="file://复">file://复</A>制要移动的区域,因为这块区域可能会被覆盖掉
      for (int i = 0; i < this.SelectArea[2]; i++) {
       for (int j = 0; j < this.SelectArea[3]; j++) {
        temp[j][i] =
         this.MyMap.Grid[this.SelectArea[1] +j]
                         [this.SelectArea[0] + i];
       }
      }
     <A href="file://PrintGrid">file://PrintGrid</A>("1"); // 调试信息
      <A href="file://将">file://将</A>要移动的区域移动到刚选中的区域(即要移动到的区域)
       for (int i = 0; i < this.SelectArea[2]; i++) {
        for (int j = 0; j < this.SelectArea[3]; j++) {
         this.MyMap.Grid[this.MoveArea[1] + j]
                         [this.MoveArea[0] + i] = temp[j][i];
        }
       }
     <A href="file://PrintGrid">file://PrintGrid</A>("2");// 调试信息
      <A href="file://将">file://将</A>要移动的区域中无用内容置成空白
       for (int i = 0; i < this.SelectArea[3]; i++) {
        for (int j = 0; j < this.SelectArea[2]; j++) {
         if (!isInRange2(this.SelectArea[0] + j,
           this.SelectArea[1] + i)) {//该点是不在要移动到
          <A href="file://的">file://的</A>区域之内,需置空
           this.MyMap.Grid[this.SelectArea[1] + i]
                           [this.SelectArea[0] + j] = Images.BLANK;
         }else {
         }
        }
       }
     <A href="file://PrintGrid">file://PrintGrid</A>("3");// 调试信息
      this.SelectArea[0] = this.MoveArea[0];//重置选中位置的水平坐标
     this.SelectArea[1] = this.MoveArea[1];//重置选中位置的竖直坐标
     this.MoveArea[0] = -1;//清空要移动到的位置
     this.MoveArea[1] = -1;//清空要移动到的位置
     this.MoveArea[2] = 0;//清空要移动到的位置
     this.MoveArea[3] = 0;//清空要移动到的位置
    }
  }
 }

 

 代码的相关分析,在详细设计阶段已经讲过,代码中有比较相近的注释,请读者自行研读分析.将全部的代码写好,用wtk2.0自带的 Ktoolbar工具建立一个工程,接下来把去不源文件放到正确位置下,然后点击build,再点run,就完成了程序的编写.当然如果有错误还要修改和调试.

 七、测试

 作为一个真正的产品要经过单体测试、结合测试和系统测试。由于项目本身简单,而且大部分代码已经是相对成熟的,我们跳过单体测试;又由于笔者的实际环境所限,无法搞到Java手机,无法架设OTA服务器,因此我们也只能放弃系统测试。那么就让我们开始结合测试吧。测试之前要先出一个测试式样书,也就是测试的计划。我们将它简化一下,只测试如下几种情况:第一、对各种形状的区域的选择和移动;第二、临近边界区域的选择和移动;第三、同一区域的反复选择和反复移动;第四、非法选择和非法移动。有了测试的目标,接下来的工作就是用wtk2.0自带的Run MIDP Application工具进行测试。打开这个工具,加载huarongRoad的jad文件,程序就会自动运行,选择launch上MIDlet1这个程序,华容道游戏就会跃然屏幕之上,接下来的工作就是左三点.右三点,拇指扭扭,来做测试。测试过程中发现任何的问题,立刻发一个bug票给自己,然后就又是痛苦的调试和修正bug,如此如此。

 八.发布

 谈到发布,其实是个关键,再好的产品不能很好的发布出去也只是个产品而已,变不成商品也就得不到回报.由于笔者的条件所限,这里只能是纸上谈兵,不过还是希望能够使读者对这一过程有所了解(网上的资料也很多)。

 J2ME的程序发布一般都是通过OTA(Over The Air),你只需要一台有公网IP的主机和一个普通的web Server就可以了(尽管要求很低,但笔者还是没有),这里我们以apache为例介绍一下OTA服务的配置,首先是安装好了apache服务器,然后在conf目录下找到mime.types文件,在该文件中加入如下两行

 application/java-archive jar
 text/vnd.sun.j2me.app-descriptor jad

 然后重起apache服务器就可以了。接下来的工作就是修改jad文件中MIDlet-Jar-URL:后面的参数,将它改为URL的绝对路径,即<A href="http://***/">http://***/</A>huarongroad.jar(其中***是你的域名或IP地址)。在下面就是用java手机下载jad文件,它会自动部署相应的jar文件并加载它。剩下的工作就和在模拟器上操作是一样的了。

 九、项目总结

 

 至此,我们已经完成了一个J2ME游戏的全部开发过程,程序中涉及到了调研、分析、设计、编码、测试和发布等方面的问题,其实在实际的工作中还有很多更为具体的问题,毕竟技术只在软件开发过程中占据很有限的一部分,这里限于篇幅的限制无法一一具体展开。今后,笔者计划再写一篇使用J2ME开发手机屏保的文章,借此机会向读者展示J2ME动画技术;然后再写一篇J2ME网络应用的文章,做一个类似开心辞典那样的知识问答游戏,以便向读者展示 J2ME的网络技术;待这两方面的技术交待清楚之后,我将引领读者制作一个稍大一些的游戏。