往往我们在程序设计的时候喜欢将界面与处理分开,这样降低耦合性,易于维护扩展。在贪吃蛇Snake这个示例程序中同样将界面UI和游戏逻辑进行了分离,它的实现方式就是,用父类TileView来实现比较基础的界面UI部分,而TileView类的子类SnakeView类完成了游戏控制逻辑部分,这样就成功的将两者进行了分离,对后面的扩展和维护奠定了良好的基础。

       界面UI

       首先来看界面UI部分,基本思想大家都非常清楚:把整个屏幕看做一个二维数组,每一个元素可以视为一个方块,因此每个方格在游戏进行过程中可以处于不同的状态,比如空闲,墙,苹果,贪食蛇(蛇身或蛇头)。我们在操作游戏的过程,其实就是不断修改相应方格的状态,然后再让整个View去重绘制自身(当然,还需要加入一些游戏当前所处状态(失败或成功)的判定机制)。TileView的数据成员如下:

Java代码
  1. //方格的大小   
  2. protected static int mTileSize;       
  3. //方格的行数和列数   
  4. protected static int mXTileCount;   
  5. protected static int mYTileCount;   
  6. //xy坐标系的偏移量   
  7. private static int mXOffset;   
  8. private static int mYOffset;   
  9. //存储三种方格的图标文件   
  10. private Bitmap[] mTileArray;    
  11. //二维方格地图   
  12. private int[][] mTileGrid;   

       那么在游戏还未正式开始前,首先要做一些初始化工作,在View第一次加载时会首先调用onSizeChanged,这里就是做这些事的最好时机。

Java代码
  1. @Override  
  2. protected void onSizeChanged(int w, int h, int oldw, int oldh)    
  3. {   
  4.         //计算屏幕中可放置的方格的行数和列数   
  5.         mXTileCount = (int) Math.floor(w / mTileSize);   
  6.         mYTileCount = (int) Math.floor(h / mTileSize);   
  7.         mXOffset = ((w - (mTileSize * mXTileCount)) / 2);   
  8.         mYOffset = ((h - (mTileSize * mYTileCount)) / 2);   
  9.         mTileGrid = new int[mXTileCount][mYTileCount];   
  10.         clearTiles();   
  11. }  

       注意模拟器屏幕默认的像素是320×400,而代码中默认的方格大小为12,因此屏幕上放置的方格数为26×40,把屏幕剖分成这么大后,再设置一个相应的二维int型数组来记录每一个方格的状态,根据方格的状态,可以从mTileArray保存的图标文件中读取对应的状态图标。

  第一次调用完onSizeChanged后,会紧跟着第一次来调用onDraw来绘制View自身,当然,此时由于所有方格的状态都是0,所以它在屏幕上等于什么也不会去绘制。

Java代码
  1. public void onDraw(Canvas canvas)    
  2. {   
  3.      super.onDraw(canvas);   
  4.      for (int x = 0; x < mXTileCount; x += 1)   
  5.      {   
  6.          for (int y = 0; y < mYTileCount; y += 1)   
  7.          {   
  8.              if (mTileGrid[x][y] > 0)   
  9.              {   
  10.                  canvas.drawBitmap(mTileArray[mTileGrid[x][y]],    
  11.                      mXOffset + x * mTileSize,   
  12.                      mYOffset + y * mTileSize,   
  13.                      mPaint);   
  14.              }   
  15.          }   
  16.      }   
  17. }  

       onDraw要做的工作非常简单,就是扫描每一个方格,根据方格当前状态,从图标文件中选择对应的图标绘制到这个方格上。当然这个onDraw在游戏进行过程中,会不断地被调用,从而界面不断被更新。

  游戏逻辑

  再来看子类SnakeView是如何在父类TileView的基础上,加入特定的游戏逻辑,从而完成Snake这个程序的。

Java代码
  1. private ArrayList<Coordinate> mSnakeTrail = new ArrayList<Coordinate>();//组成贪食蛇的方格列表   
  2. private ArrayList<Coordinate> mAppleList = new ArrayList<Coordinate>();//苹果方格列表  

       由于SnakeView从TileView继承而来,则可以说它已经拥有这个二维方格地图了(只是此时地图里的所有方格状态都是0)。那么它有了这么一个二维方格地图,如何去初始化这个地图呢?这在initNewGame函数中实现。

Java代码
  1. private void initNewGame()   
  2.     {   
  3.         //清空蛇和苹果占据的方格   
  4.         mSnakeTrail.clear();   
  5.         mAppleList.clear();   
  6.         //目前组成蛇的方格式固定的,而且方向也固定朝北   
  7.         mSnakeTrail.add(new Coordinate(77));   
  8.         mSnakeTrail.add(new Coordinate(67));   
  9.         mSnakeTrail.add(new Coordinate(57));   
  10.         mSnakeTrail.add(new Coordinate(47));   
  11.         mSnakeTrail.add(new Coordinate(37));   
  12.         mSnakeTrail.add(new Coordinate(27));   
  13.         mNextDirection = NORTH;   
  14.   
  15.         //随即加入苹果   
  16.         for (int i = 0; i < nApples; ++i)   
  17.         {   
  18.             addRandomApple();   
  19.         }   
  20.         //初始化运动速率和玩家成绩   
  21.         mMoveDelay = 600;   
  22.         mScore = 0;   
  23. }  

       想象下对整个游戏屏幕拍张照,然后对其下一个状态再拍张照,那么两张照片之间的区别是怎么产生的呢?对于系统来说,它只知道不断调用onDraw,后者负责对整个屏幕进行绘制,那要产生两个屏幕之间的差异,肯定要通过一些手段对某些数据结构(比如这里的二维方格地图)进行调整(比如用户的控制指令,定时器等),然后等到下一次onDraw时就会把这些更改在界面上反映出来。

       这里要着重说明下private long mMoveDelay = 600;这个成员变量,虽然很不起眼,但仔细考虑它的作用就会发现很有趣,那么改变它的大小到底是如何让我们感觉到游戏变快或变慢呢?

       可以打个简单的比方,在时刻0游戏启动,首先把蛇和苹果的位置都在方格地图上作好了标记,然后我们在update函数中修改蛇身让蛇向北前进一步,而这个改变此时还只是停留在内部的核心数据结构上(即二维方格地图),还没有在界面上显示出来。当然,我们马上想到要想让这更改显示出来,让系统调用onDraw去绘制不就完了吗?可是问题是我们不知道系统是隔多长时间去调用onDraw函数,于是mMoveDelay此时就发挥作用了,通过它就可以设置休眠的时间,等时间一到,马上就会通知SnakeView去重绘制。你可以试试把mMoveDelay数值调大,就会看出我上面提到的“拍照“的效果。

  Handler的使用

  写过JavaScript或者ActionScript的开发者,对于setInterval的用法会非常了解。那么在Android中如何实现setInterval的方法呢?其中有两种方法可以实现类似的功能,其中一个是在线程中调用Handler方法,另外一个是应用Timer。Snake中使用了前者。

Java代码
  1. class RefreshHandler extends Handler    
  2. {   
  3.         @Override  
  4.         public void handleMessage(Message msg)    
  5.         {//“苏醒”后的处理   
  6.            SnakeView.this.update();   
  7.            SnakeView.this.invalidate();   
  8.         }   
  9.         public void sleep(long delayMillis)    
  10.         {//休眠delayMillis毫秒   
  11.             this.removeMessages(0);   
  12.             sendMessageDelayed(obtainMessage(0), delayMillis);   
  13.         }   
  14. };  

       而实际调用的处理函数update就可以说是整个游戏的引擎,正是由于它的工作(修改蛇和苹果的状态到一个新的状态,然后休眠自己,然后等到苏醒后在Handler中就会让系统区绘制上次修改过的二维方块地图,然后再次调用update,如此循环反复,生生不息),才使得游戏不断被推进,因此,比做“引擎“不为过。

Java代码
  1. public void update()   
  2. {   
  3.     if (mMode == RUNNING)   
  4.     {   
  5.         long now = System.currentTimeMillis();   
  6.         if (now - mLastMove > mMoveDelay)    
  7.         {   
  8.             clearTiles();   
  9.             updateWalls();   
  10.             updateSnake();   
  11.             updateApples();   
  12.             mLastMove = now;   
  13.         }   
  14.         mRedrawHandler.sleep(mMoveDelay);   
  15.     }   
  16. }  

       既然update是游戏的动力,要让游戏停止下来只要不再调用update就可以了(因为此时其实是画面静止了),因此游戏进入暂停(这个状态还可以转为“运行“,其实就是继续可以修改,再绘制),若进入失败(其实此时二维方块地图还停留在最后一个画面处,这也是为什么在开始时要首先清理掉整个地图)【这一点,可以在游戏失败后,再次开始新游戏,此时通过设置的断点即可观察到上次游戏运行时的底层数据】。

  一点困惑

  可是个人认为Snake下面这段代码读起来有点怪,有点像一个“先有鸡,还是先有蛋?“的问题,导致我的思维逻辑上出现一个“怪圈“。

Java代码
  1. public void handleMessage(Message msg)    
  2. {   
  3.       SnakeView.this.update();   
  4.       SnakeView.this.invalidate();   
  5. }  

      按照这段代码的意思来看,当休眠的时间已经到了,首先去调用update,即为下一次绘制做准备工作,再让自己休眠起来,最后通知系统重绘制自己。

  哎,这让我难以理解,还是回到时刻0的例子来说,在时刻0时让蛇身向北前进了一步(指的是底层的二维方格地图的修改,不是界面),然后让自己休眠0.6毫秒,当时间到了,首先去调用update方法,那么就又会让蛇身做出修改,也就是把上一次还没绘制的覆盖掉了(那么上一次的修改岂不是白费,还没画上去呢),更何况在update中又会让自己去休眠(还没调用invalidate,怎么又去休眠了?),又怎么还能去通知系统调用我的onDraw方法呢?也就是说invalidate根本没有执行???

  按我的理解,应该把顺序颠倒一下,先通知系统去调用onDraw方法重绘,使得上一次对底层二维方格地图的修改显示出来,然后再去为下一次修改做准备工作,最后让自己进入休眠,等待苏醒过来,如此循环反复。实验证明,颠倒过来也是正确的,不过关于这一个迷惑我的地方,希望有朋友能指点我一下!

       记得在javascript里使用setInterval时,也是先写处理逻辑,然后在末尾处写上一句setInterval(这也是我习惯的思维方式了),难道google上面这种写法有何深意?

     此外,感觉每次绘制时都重新绘制墙壁,有点浪费时间,因为墙壁根本没有任何变化的。还有就是mLastMove这个变量设置的初衷是保证当前时间点距上一次变化已经过去了mMoveDelay毫秒,可是既然已经用了sleep机制,再使用这个时间差看上去并无必要。

本文发布:Android开发网
本文地址:http://www.jizhuomi.com/android/example/189.html
2012年9月5日
发布:鸡啄米 分类:Android开发实例 浏览: 评论:2