走出J2ME游戏开发迷宫--J2ME游戏开发框架及实施

走出 J2ME 游戏开发迷宫-- J2ME 游戏开发框架及实施

 

 

今天手机的强大功能已经远远超出了“服务于人类的沟通和交流”这个范畴。在手机操作系统开始提供越来越强大的平台功能时,各种手机应用程序应该已经开始丰富人们的生活。面对五花八门的手机应用开发平台,作为手机应用的开发人员,当然希望自己所提供的应用程序能够在各种不同的手机中运行。很幸运地, J2ME 使这一些成为了可能。从 MIDP1.0 MIDP2.0 J2ME 平台已经为手机应用程序的开发提供了方便的 API ,而只有 API 并不足以组成一个完美的应用,如何设计应用框架并进行实施才是完美应用的灵魂和力量。

 

本文将以一个迷宫游戏的应用开发来说明如何设计游戏框架,并在此框架上实现图形操作,人工互动以及最为简单的人工智能。该游戏框架将基于 MIDP1.0 ,作为 MIDP2.0 的前一版本, MIDP1.0 提供更有限的 API MIDP2.0 专门提供了一套 GAME API ),同时在不同的手机上具有极佳的兼容性,取决于文章的性质, MIDP1.0 将更有利于读者,同时,本文假设读者对 MIDP1.0 有基本的了解,如果还不具备相关知识,可以在文章最后找到相关的支持。

 

游戏框架

 

 

作为一个游戏的开发设计者,或者作为一个游戏公司的设计师,不会为每一个游戏项目设计一个开发框架,那样做的成本太高,通常的做法是为某一类型的游戏(如 RPG ACT 等等),或者某几种类型的游戏提供一个统一的开发框架,开发人员接手到一款新的游戏设计后,只需要选择相应的开发框架(可能需要稍加改动),就可以熟练的基于该框架进行游戏的开发实施,甚至可以重用部分旧代码。那么,下面就让我们以一名设计者的角度去思考一个迷宫游戏需要些什么,如何进行设计。

 

首先,对于一个迷宫游戏,我们可以列出以下的基本需求:

1) 游戏需要有一个初始的介绍性界面;

2) 游戏需要能够自行随机生成迷宫;

3) 游戏需要有不同的难度级别;

4) 游戏需要能够由玩家互动参与;

5) 玩家在游戏中可以选择一个机器人进行比赛。

还有什么吗?是的,如果有以上这些需求存在,那么这个游戏就应该有一个游戏菜单,这是第 6 个基本需求。还有 ...... 是的,还会有很多,但让我们暂时打住,记得吗?我们的游戏公司刚刚成立,我们的第一个任务是设计一个游戏开发框架,在这个时候考虑得过细是不太合适的。

 

有了以上的需求列表,我们开始考虑一个游戏都由哪几部分组成,换句话说,有哪几个阶段,我们借鉴一下玩家的视角,当然,设计者需要考虑更多的因素,讨论之后,我们得出了以下阶段。

1) 游戏初始阶段:对于玩家而言是一个初始界面,对于设计者而言,许多游戏数据的初始化工作将在这里完成;

2) 游戏设置阶段:对于玩家而言这里是菜单选择界面,对于设计者而言,这里所进行的设置将会影响到游戏实体运行时的表现;

3) 游戏运行阶段:对于玩家而言这里是天堂或者地狱,对于设计者而言,这里需要根据游戏内容,类型,情节等等进行精心的设计编码;

3) 游戏结束阶段:对于玩家而言这里是游戏告诉自己是 Winner 还是 Loser ,对于设计者而言,这里需要对游戏和玩家进行总结,可能的话还需要说明或致谢。

 

这些阶段对于玩家来说都很具体,而对于设计者而言则每一部分都足够抽象,如果没有一个游戏框架,以上四个阶段很可能被合并,也可能被拆分,从而每次新的游戏开发项目都成为了一次新的长征。所以,我们将以这四个阶段来进行我们的游戏框架设计。于是,我们终于开始接触到 MIDP1.0 了。

 

每个 J2ME 的应用都必须有一个类继承实现 javax.microedition.midlet.MIDLet 这个抽象类,从而也就可以由应用管理软件对其进行生命周期管理等操作。尽管该类的地位非常重要,但却并不是我们游戏框架中的一部分。是的,每个游戏项目都可以根据自身特殊的需要生成这个类,如独特的名称,独特的启动行为,独特的退出行为等等。毕竟游戏的核心不是在这里。那么游戏的核心是在那里呢?对于 J2ME 而言,我们可以认为 javax.microedition.lcdui.Canvas 最适合担当这个重任,游戏的图形及文字表现,玩家的交互响应都可以由该类一手包办,确实也有很多的 J2ME 手机游戏公司用该类的子类实现来完成所有我们以上所提到的四个阶段。但直接的后果就是全部代码都位于一个类文件中,不易于编码,实现及管理,需要很强的过程语言编写能力,基本放弃了 Java 语言的面向对象特性。特别是对于初学者而言,不容易学习和维护。

 

实际以上所提到的这些缺点也正是我们在游戏开发中所需要学习的优点。在所有的手机设备中,可用的资源总是有限的,以 Nokia S40 系列设备为例,应用程序可用内存为 210KB 左右,而每一个 J2ME 应用的发布 Jar 文件不能够大于 64KB CPU 的速度远不及台式 PC ,这些限制都决定了我们所完成的 J2ME 应用在为了实现尽可能强大的功能时,不得不做到以下几点:

1) 保持 J2ME 应用中类的数量尽可能的少;

2) 尽可能使用过程方法进行实现;

3) 如果可能则重用能够重新赋值的对象。

 

但本文中所提到的游戏框架并不准备以极端的方式来进行设计,于是也就有了我们前面所讨论出来的四个阶段,它们对整个游戏过程进行了分割,以分而治之的方式来降低管理实现的复杂度,同时尽可能提高以阶段为单位的复用。在我们的框架中,这四个阶段根据应用的需要可有可无,且均代表四部分相对独立的过程。从下图 1 中我们终于可以看到这个完整而简单的游戏框架。

1 :游戏框架及迷宫游戏实现类图

 

 

需要说明的是,该类图中只有两个类是组成游戏框架的元素, com.savorjava.game.Action com.savorjava.game.CoreCanvas 。其他的类是为实现迷宫游戏,从而基于该框架的必要成份。是不是觉得框架的组成过于简单了?对于 PC 应用的设计而言,对于由两个类所组成是框架是不可思议的,而对于手机 J2ME 应用而言,却是再适合不过了。该框架中甚至不包括 MIDlet ,因此我们可以称之为半生产框架,在这个框架中我们只关心两项工作:各个不同阶段的工作以及如果调度切换这些工作。对于受限资源的 J2ME 手机应用开发来说,这就是我们可以接受的功能和能够允许的资源消耗。

 

com.savorjava.game.Action 作为一个抽象类,声明实现两个接口: Runnable CommandListener ,并提供了两个主要的行为方法:

public void perform(Graphics g)

对传入的 Graphics 对象进行操作,是该 Action 主要过程体,抱括应用运算以及结果绘制

public void interrupt(int key)

Canvas 所接收到的按键活动进行响应处理,通知并改变应用运算

由这两个方法以及要求实现的两个接口我们可以看出游戏中某个阶段,即某部分过程的处理都由该 Action 进行处理,处理的内容包括了图形文字表现以及玩家的交互响应。至此,大家可以知道在我们即将设计实施的迷宫游戏中,最多只会存在四个 Action 的实现子类,对应着我们上面的四个阶段分析。

同时, com.savorjava.game.Action 还提供了两个类变量:

protected CoreCanvas canvas = null;

 

所有 Action 的子类都需要持有一个 CoreCanvas 的引用

protected boolean acting = true;

 

acting 决定了该 Action 是否应该继续运行,一般在线程的 run 方法中决定是否结束该线程

 

 

接下来,我们再来看看 com.savorjava.game.CoreCanvas ,该类是 javax.microedition.lcdui.Canvas 的子类,在我们的游戏框架中,它不是负责图形文字表现以及玩家的交互响应的核心,而是负责调度四个阶段的 Action 子类(如果存在的话)。换句话说, CoreCanvas 在某一时刻决定只有一个阶段的 Action 在运行,它通过与 Action 之间的信息传递来决定 Action 的调度。因此, CoreCanvas 本身也声明实现了 Runnable 接口,它的运行将贯穿整个游戏的生命周期。以下是 CoreCanvas 中的线程部分代码:

   /**

 

     * core allocation: switch actions according to game status while status is changed.

 

     */

 

   public void run() {

          while(true)

          {

                 if(statusChanged)

                 {

                        switch(gameStatus)

                        {

                               case GS_INITIAL :

                               {

                                      if(initialAction != null)

                                      {

                                             statusChanged = false;

                                             currentAction = initialAction;

                                             initialThread.start();

                                      }else

                                      {

                                             gameStatus = GS_START;

                                      }

                                      break;

                               }

                               case GS_START :

                               {

                                      if(startAction != null)

                                      {

                                             statusChanged = false;

                                             currentAction = startAction;

                                             startThread.start();

                                      }else

                                      {

                                             gameStatus = GS_RUN;

                                      }

                                      break;

                               }

                               case GS_RUN :

                               {

                                      if(runAction != null)

                                      {

                                             statusChanged = false;

                                             currentAction = runAction;

                                             runThread.start();

                                      }else

                                      {

                                             gameStatus = GS_END;

                                      }

                                      break;

                               }

                               case GS_END :

                               {

                                      if(runAction != null)

                                      {

                                             statusChanged = false;

                                             currentAction = endAction;

                                             endThread.start();

                                      }

                                      break;

                               }

                        }

                 }

                 try{

                     Thread.sleep(sleepTime);

                 }catch(InterruptedException ie)

                 {

                       

                 }

          }

         

   }    

这段代码基于实现了所有的调度机制,简单而有效。在 run 这个线程方法中,我们可以看出该线程不会自动结束,在它的无限循环中,完成以下工作:

1) 根据 statusChanged 标志查看是否游戏的阶段发生了改变,如果发生了改变则处理 2 ,否则处理 5

 

2) 根据 gameStatus 决定应该激活四个阶段中的哪一个 Action ,如果 Action 存在则处理 3 ,否则处理 4

3) statusChanged 置为 false ,再将该 Action 设置为当前 Action 并启动其线程;

4) 自动走向下一阶段的 Action 处理,即将 gameStatus 设置为下一阶段的状态。

5) 调度线程进入指定时间的睡眠。

 

CoreCanvas 为了能够与注册到自身的 Action 进行信息传递,提供了以下的回调方法:

   public void setGameStatus(int gameStatus) {

          // stop current action.

 

          currentAction.setActing(false);

          this.gameStatus = gameStatus;

          // time to garbage collect.

 

          System.gc();

          // notice core allcation cavas: status have been changed.

 

          statusChanged = true;

   }

当一个 Action 认为自身阶段的工作完成以后,需要调用该方法,以通知 CoreCanvas 进行下一次的调度工作。为了做到这一点, CoreCanvas 在该方法中进行以下处理:

1) 将当前 Action acting 设置为 false ,通知相关线程终止;

2) gameStatus 切换成该 Action 要求的新阶段值;

3) 进行垃圾回收,将该 Action 的阶段工作中所占用的内存释放;

4) 通知调度线程需要进行调度工作了。

 

在这里需要强调的是,在 J2ME 应用是,及时以及何时进行垃圾回收是非常重要的,其基本原则是尽快回收但不要选择对用户响应造成太大损失的时间点。

 

游戏迷宫

 

至此,我们已经基本了解了这个游戏框架的组成及运作,那么剩下的工作就是如何将游戏的各个阶段以 Action 来实现。根据图 1 所示,我们知道在这个即将实施的迷宫游戏中,只存在三个阶段的 Action ,即初始( MazeInitialAction ),设置( MazeStartAction )和运行( MazeRunAction )。是的,框架的灵活性允许我们只实现四个阶段中的若干阶段,甚至于不实现任何一个阶段的 Action ,当然,这样做是没有意义的。

 

根据迷宫的需求以及框架设计,我们现在需要完成三个 Action 的设计与实施。以怎样的顺序和方式进行这三个 Action 的工作取决于团队的结构,人手,可能还涉及个人的兴趣。我个人认为先进行游戏核心,也就是 MazeRunAction 阶段的设计是最重要的,因为在该阶段的设计实施中可能产生对其他阶段的依赖和影响。而且我们这个虚拟公司的人手的确太少,为迷宫打下地基应该是当务之急。

 

根据需求中的第二点:游戏需要能够自行随机生成迷宫。那么我们就来好好研究一下迷宫生成算法吧。对于一个二维四方迷宫而言,其本质是由 n*m 的四方小房间所组成的。每两间相邻的小房间都被墙壁所隔开,如下图 2 是一个 5*5 的初始迷宫,所有的房间之间都没有通路,被墙壁完全隔开。

2 :初始迷宫

 

 

如何保证每次都在这个初始迷宫上生成一个随机不同的真实迷宫呢?在这里,我们给迷宫下一个比较完整的定义:迷宫由一组房间组成,其中任意两个房间之间有且仅有一条通路。这句话是不是有点熟悉?是的,运用最小生成树的深度优先(宽度优先也可以,但生成的迷宫不理想)算法可以为这样一个初始迷宫生成一个真实迷宫,当然为了达到随机生成的目的,我们需要借助于随机数。以下是为完成这一算法所需要的步骤:

 

1) 在初始迷宫中随机的找出一个起始房间;

 

2) 在这个起始房间的邻居中找到一个没有被访问过的房间;

 

3) 如果找到,就移入到新的房间,并将两房间之间的墙壁推倒;如果没有找到,就退回到上一个房间;

 

4) 重复 2 3 ,直到所有的房间都被访问到为止。

 

看起来很简单,不是吗?当所有的房间都被访问过以后,我们的迷宫也就生成了,且每任意两个房间之间有且仅有一条通路。于是,在 MazeRunAction 中就有了这样的两个方法:

 

   /**

 

     * initial all rooms in the maze according to size of maze, 

 

     * walls and borders will be up which present by 1.

 

     */

 

   private void initialRooms()

 

   {

 

          int xSize = canvas.getWidth();

 

          int ySize = canvas.getHeight();

 

          short minSize = (short)(Utility.min(xSize, ySize));

 

          maxGrid = (short)((minSize - 1) / gridSize);

 

          rooms = new int[grid][grid];

 

          // initial all the rooms: no visited and all the wall up.

 

          for(int i = 0; i < grid; i++)

 

                 for(int j = 0; j < grid; j++)

 

                 {

 

                        rooms[i][j] = 0x000F;

 

                 }

 

          // add border for the rooms which on the four sides.

 

          for(int i = 0; i < grid; i++)

 

          {

 

                 rooms[0][i] |= 0x0040;

 

                 rooms[grid - 1][i] |= 0x0010;

 

                 rooms[i][0] |= 0x0080;

 

                 rooms[i][grid - 1] |= 0x0020;

 

          }

 

          hacker[0] = 0;

 

          hacker[1] = 0;

 

          roomsNeedDraw.removeAllElements();

 

          robot[0] = 0;

 

          robot[1] = 0;

 

          robot[2] = EAST;

 

          robot[3] = WEST;

 

          robotMemory.removeAllElements();

 

 

          System.gc();

 

   }

 

  

 

 

 

   /**

 

     * getting a random neighbor room according to indicated room.

 

     * @param rooms

 

     * @param x

 

     * @param y

 

     * @return

 

     */

 

   private int[] getRandomNeighbor(int[][] rooms, int x, int y)

 

   {

 

          int direction = random.nextInt() % 4;

 

          if(direction < 0)

 

          {

 

                 direction = -direction;

 

          }

 

          int[] neighbor = null;

 

          for(int i = 0; i < 4; i++)

 

          {

 

                 switch(direction)

 

                 {

 

                        case 0: // EAST

 

                        {

 

                               if((x + 1 < grid) &&

 

                                             (rooms[x + 1][y] & 0x1000) == 0 &&

 

                                             (rooms[x][y] & 0x0010) == 0)

 

                               {

 

                                      neighbor = new int[2];

 

                                      neighbor[0] = x + 1;

 

                                      neighbor[1] = y;

 

                                      rooms[x][y] &= ~EAST;

 

                                      rooms[x + 1][y] &= ~WEST;

 

                               }

 

                               break;

 

                        }

 

                        case 1: // SOUTH

 

                        {

 

                               if((y + 1 < grid) &&

 

                                             (rooms[x][y + 1] & 0x1000) == 0 &&

 

                                             (rooms[x][y] & 0x0020) == 0)

 

                               {

 

                                      neighbor = new int[2];

 

                                      neighbor[0] = x;

 

                                      neighbor[1] = y + 1;

 

                                      rooms[x][y] &= ~SOUTH;

 

                                      rooms[x][y + 1] &= ~NORTH;

 

                               }

 

                               break;

 

                        }

 

                        case 2: // WEST

 

                        {

 

                               if((x - 1 >= 0) &&

 

                                             (rooms[x - 1][y] & 0x1000) == 0 &&

 

                                             (rooms[x][y] & 0x0040) == 0)

 

                               {

 

                                      neighbor = new int[2];

 

                                      neighbor[0] = x - 1;

 

                                      neighbor[1] = y;

 

                                      rooms[x][y] &= ~WEST;

 

                                      rooms[x - 1][y] &= ~EAST;

 

                               }

 

                               break;

 

                        }

 

                        case 3: // NORTH

 

                        {

 

                               if((y - 1 >= 0) &&

 

                                             (rooms[x][y - 1] & 0x1000) == 0 &&

 

                                             (rooms[x][y] & 0x0080) == 0)

 

                               {

 

                                      neighbor = new int[2];

 

                                      neighbor[0] = x;

 

                                      neighbor[1] = y - 1;

 

                                      rooms[x][y] &= ~NORTH;

 

                                      rooms[x][y - 1] &= ~SOUTH;

 

                               }

 

                               break;

 

                        }

 

                 }

 

                 if(neighbor != null)

 

                 {

 

                        break;

 

                 }

 

                 if(++ direction >= 4)

 

                 {

 

                        direction = 0;

 

                 }

 

          }

 

          return neighbor;

 

   }

 

initialRooms 方法用于根据指定的大小生成初始迷宫,而 getRandomNeighbor 则是根据指定的房间得到下一个相关且未访问的房间。这两个方法主要是在与代表房间相关信息的整数在打交道,在这里,我们使用一个整数来代表一个房间,而不是用一个新的对象类来描述房间,原因在前面已经提到过了:尽可能的减少类的数量;尽可能的减少内存的消耗。如果你希望你的 n*n 迷宫的 n 值尽可能的大,那么整数和位操作是你最好的选择。在 Java 语言中,一个整数由 32 个位所组成,我们需要定义这 32 个位分别代表了什么含义。

 

 

位段

 

说明

 

0~3

 

分别代表该房间东,南,西,北四个方向的墙壁, 0 表示该方向墙壁不存在, 1 表示存在

 

4~7

 

分别代表该房间东,南,西,北四个方向的墙壁是否位于迷宫的外墙边缘,这些边缘的外墙是不可推倒的,我们可不希望迷宫四周的围墙出现缺口

 

8~11

 

记录玩家是否在东,南,西,北某个方向上经过这个房间

 

12~15

 

12 位表示玩家是否访问过该房间, 13 表示机器人是否访问过该房间,其他两位保留,实际上在优化过程中, 12 13 位已经不需要了

 

16~19

 

8~11 位相同,不过记录的是机器人的路径

 

20~31

 

为将来的扩展保留

 

 

在清楚了以上各位的含义之后,我们所需要完成的工作就是如何响应玩家的操作,让玩家的代表图像在迷宫中正确的行进,而不是穿墙而过。相信大家还记得 Action 中的 interrupt 方法吧, CoreCanvas 不再处理自身所接收到的按键事件,而是简单地将其发送给当前 Action 去处理。因此,在 MazeRunAction 中我们需要处理这些按键事件,内容无非就是修改相关房间的整数值。而 MazeRunAction 的处理线程会不断地操作 Canvas 进行图形的绘制。

 

 

机器人的行动基本不用由按建事件来处理,这个最为简单的人工智能以 MazeRunAction 的处理线程为自己的心跳进行迷宫的穿行。原理也非常简单:从出发点开始,根据东,南,西,北这四个方向的顺序寻找出路,找到则前进,四个方向上都找不到或都曾经去过了则回退到上一个房间。迷宫图形的绘制同样由 MazeRunAction 的处理线程操作 Canvas 进行。

 

 

为便于管理和控制,可以在运行阶段这个 Action 中再划分子阶段,并用不同的状态来标识,我们正是这样做的,在下面的代码中,展示了机器人在不同状态下的运作,由此大家可以了解到玩家部分的处理代码也是与之类似的。

 

   private void robotRun()

 

   {

 

          // robot stop running while arrived terminal point.

 

          if(robot[0] == grid - 1 && robot[1] == grid - 1)

 

          {

 

                 runingFlag = false;

 

                 robotMemory.removeAllElements();

 

                 System.gc();

 

                 return;

 

          }

 

          // go to easy neighbor if possible.

 

          if((rooms[robot[0]][robot[1]] & EAST) == 0 &&

 

                        robot[2] == EAST &&

 

                        robot[3] != EAST)

 

          {

 

                 // change room information.

 

                 rooms[robot[0]][robot[1]] |= 0x10000;

 

                 rooms[robot[0] + 1][robot[1]] |= 0x40000;

 

                 // record path which robot walk through.

 

                 robotMemory.push(new short[]{robot[0], robot[1], robot[2], robot[3]});

 

                 // record room which need be re-drawn

 

                 roomsNeedDraw.push(new int[]{robot[0], robot[1]});

 

                 robot[0] ++;

 

                 robot[2] = EAST;

 

                 robot[3] = WEST;

 

          }else if((rooms[robot[0]][robot[1]] & SOUTH) == 0 &&

 

                        robot[2] == SOUTH &&

 

                        robot[3] != SOUTH)

 

          {

 

                 rooms[robot[0]][robot[1]] |= 0x20000;

 

                 rooms[robot[0]][robot[1] + 1] |= 0x80000;

 

                 robotMemory.push(new short[]{robot[0], robot[1], robot[2], robot[3]});

 

                 roomsNeedDraw.push(new int[]{robot[0], robot[1]});

 

                 robot[1] ++;

 

                 robot[2] = EAST;

 

                 robot[3] = NORTH;

 

          }else if((rooms[robot[0]][robot[1]] & WEST) == 0 &&

 

                        robot[2] == WEST &&

 

                        robot[3] != WEST)

 

          {

 

                 rooms[robot[0]][robot[1]] |= 0x40000;

 

                 rooms[robot[0] - 1][robot[1]] |= 0x10000;

 

                 robotMemory.push(new short[]{robot[0], robot[1], robot[2], robot[3]});

 

                 roomsNeedDraw.push(new int[]{robot[0], robot[1]});

 

                 robot[0] --;

 

                 robot[2] = EAST;

 

                 robot[3] = EAST;

 

          }else if((rooms[robot[0]][robot[1]] & NORTH) == 0 &&

 

                        robot[2] == NORTH &&

 

                        robot[3] != NORTH)

 

          {

 

                 rooms[robot[0]][robot[1]] |= 0x80000;

 

                 rooms[robot[0]][robot[1] - 1] |= 0x20000;

 

                 robotMemory.push(new short[]{robot[0], robot[1], robot[2], robot[3]});

 

                 roomsNeedDraw.push(new int[]{robot[0], robot[1]});

 

                 robot[1] --;

 

                 robot[2] = EAST;

 

                 robot[3] = SOUTH;

 

          }else

 

          {

 

                 // go to next direction if current direction is available;

 

                 // go back to last room if no direction is available in this room.

 

                 robot[2] <<= 1;

 

                 while(robot[2] > NORTH)

 

                 {

 

                        // clean current room's solution by robot.

 

                        rooms[robot[0]][robot[1]] &= ~0xF0000;

 

                        roomsNeedDraw.push(new int[]{robot[0], robot[1]});

 

                        // get last room location of robot and clean the solution with current room.

 

                        robot = (short[])robotMemory.pop();

 

                        if((robot[2] & EAST) == EAST)

 

                        {

 

                               rooms[robot[0]][robot[1]] &= ~0x10000;

 

                        }else if((robot[2] & SOUTH) == SOUTH)

 

                        {

 

                               rooms[robot[0]][robot[1]] &= ~0x20000;

 

                        }else if((robot[2] & WEST) == WEST)

 

                        {

 

                               rooms[robot[0]][robot[1]] &= ~0x40000;

 

                        }else if((robot[2] & NORTH) == NORTH)

 

                        {

 

                               rooms[robot[0]][robot[1]] &= ~0x80000;

 

                        }

 

                        robot[2] <<= 1;

 

                 }

 

          }

 

   }

 

以上代码中需要提到的是 robot 这个一维数组的内容,该数组有四个元素,分别表示:

 

1) 机器人当前所处房间的 x 坐标;

 

2) 机器人当前所处房间的 y 坐标;

 

3) 机器人将从房间的哪个方向离开;

 

4) 机器人从哪个方向进入该坐标的房间。

 

有了以上的算法基础,我们可以得到以下的效果图:

 

3 :不同级别的迷宫效果图

 

 

在完成了 MazeRunAction 的设计及实施之后,需要进行初始阶段以及设置阶段的设计及实施,以该迷宫游戏中,这两个阶段的内容都非常简单,也很具有代表性,限于篇幅有限,在此简要说明一下即可。

 

 

初始阶段即需要完成 MazeInitialAction ,该 Action 简单的载入一张 PNG 图片,显示三秒钟后,自动结束,并通知 CoreCanvas 将游戏状态切换为 START ,进入设置阶段。在初始阶段来说很重要的一点,或者说对于整个游戏来说很重要的一点,就是美工。包装不是万能的,但没有包装是万万不能的,下面分别是我在为这个游戏进行设计时先后绘制的两张封面,选用哪一张会使游戏更有吸引力是不言而喻的。

 

4 :游戏封面设计

 

 

对于设置阶段则需要完成 MazeStartAction ,该 Action 的功能是向用户提供一个游戏菜单,让用户可以进行游戏的相关设置,目前我们主要提供了以下菜单选项:

 

1)Human :单人游戏,机器人不会被激活;

 

2)Human vs Robot :与机器人对战游戏;

 

3)Config :对游戏的级别进行控制, Easy Normal 以及 Impossible 对应图 3 的三个级别;

 

4)Help :关于游戏操作及设计者的信息;

 

5)Exit :退出游戏。

 

以下是一个游戏启动,设置的效果图。

 

5 :游戏启动设置效果图

 

 

以上,我们介绍了基于 MIDP1.0 进行游戏开发的框架设计,以及基于该框架实现一个迷宫游戏中的主要内容,由于本文并不是一篇 MIDP 1.0 API 入门文档,所针对的是有一定 Java 实践并且对 MIDP 1.0 API 有初步认识的开发人员,因此略掉了很多在运用该 API 进行开发的过程中需要注意的问题,以及一些常用的技巧,如图形绘制的双缓冲技术。但作为补偿,我将在文章的最末列出一些有用的资源,供大家参考。同时,本文中开发的迷宫游戏名为 SavorMaze ,已经在诺基亚,西门子,索爱等品牌的多种型号上运行通过,由 Savorjava 在业余时间开发完成,作为一份开源代码, Savorjava 已经向 SourceForge 申请了开源项目,因此,所有的源代码以及可运行的手机发布版本能够在 SourceForge 上或文章发布的网站获取,代码中应该有一些 J2ME 开发人员可以借鉴的部分,我们不可能在这里列出所有的源代码,请大家理解,详细情况请查看最后的参考信息。

 

 

参考信息

 

1.          本文中的全部源代码在 SourceForge 上的项目 SavorMaze 中,可运行 Jar 文件以及 Jad 文件如下:(请见附件: savormaze.jar savormaze.jad );

 

2.          http://forum.nokia.com.cn 可以找到很多关于 J2ME 开发的相关信息;

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值