Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發實例 >> Android示例程序剖析之Snake貪吃蛇(三:界面UI、游戲邏輯和Handler)

Android示例程序剖析之Snake貪吃蛇(三:界面UI、游戲邏輯和Handler)

編輯:Android開發實例

       往往我們在程序設計的時候喜歡將界面與處理分開,這樣降低耦合性,易於維護擴展。在貪吃蛇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(7, 7));   
  8.         mSnakeTrail.add(new Coordinate(6, 7));   
  9.         mSnakeTrail.add(new Coordinate(5, 7));   
  10.         mSnakeTrail.add(new Coordinate(4, 7));   
  11.         mSnakeTrail.add(new Coordinate(3, 7));   
  12.         mSnakeTrail.add(new Coordinate(2, 7));   
  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機制,再使用這個時間差看上去並無必要。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved