Android實例剖析筆記(七)

作者: Phinecos(洞庭散人)  來源: 博客園  發布時間: 2010-10-24 22:56  閱讀: 2250 次  推薦: 0   原文鏈接   [收藏]  
摘要:關注游戲主界面這個View是如何實現的

Snake這個項目把主界面剖成界面UI和游戲邏輯兩層,最基礎的界面UI部分用父類TileView來表示,子類SnakeView是在TileView的UI基礎上,加入相應的游戲控制邏輯,從而實現了兩者的分離,這對于游戲的修改非常有用。

UI實現部分

首先來看界面UI部分,基本思想大家都非常清楚:把整個屏幕看做一個二維數組,每一個元素可以視為一個方塊,因此每個方格在游戲進行過程中可以處于不同的狀態,比如空閑,墻,蘋果,貪食蛇(蛇身或蛇頭)。我們在操作游戲的過程,其實就是不斷修改相應方格的狀態,然后再讓整個View去重繪制自身(當然,還需要加入一些游戲當前所處狀態(失敗或成功)的判定機制)。TileView的數據成員如下:

 

//方格的大小
protected static int mTileSize;    
//方格的行數和列數
protected static int mXTileCount;
protected static int mYTileCount;
//xy坐標系的偏移量
private static int mXOffset;
private static int mYOffset;
//存儲三種方格的圖標文件
private Bitmap[] mTileArray; 
//二維方格地圖
private int[][] mTileGrid; 

那么在游戲還未正式開始前,首先要做一些初始化工作,在View第一次加載時會首先調用onSizeChanged,這里就是做這些事的最好時機。

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) 
{
        
//計算屏幕中可放置的方格的行數和列數
        mXTileCount = (int) Math.floor(w / mTileSize);
        mYTileCount 
= (int) Math.floor(h / mTileSize);
        mXOffset 
= ((w - (mTileSize * mXTileCount)) / 2);
        mYOffset 
= ((h - (mTileSize * mYTileCount)) / 2);
        mTileGrid 
= new int[mXTileCount][mYTileCount];
        clearTiles();
}

  注意模擬器屏幕默認的像素是320×400,而代碼中默認的方格大小為12,因此屏幕上放置的方格數為26×40,把屏幕剖分成這么大后,再設置一個相應的二維int型數組來記錄每一個方格的狀態,根據方格的狀態,可以從mTileArray保存的圖標文件中讀取對應的狀態圖標。

  第一次調用完onSizeChanged后,會緊跟著第一次來調用onDraw來繪制View自身,當然,此時由于所有方格的狀態都是0,所以它在屏幕上等于什么也不會去繪制。

    public void onDraw(Canvas canvas) 
    {
        
super.onDraw(canvas);
        
for (int x = 0; x < mXTileCount; x += 1)
        {
            
for (int y = 0; y < mYTileCount; y += 1)
            {
                
if (mTileGrid[x][y] > 0)
                {
                    canvas.drawBitmap(mTileArray[mTileGrid[x][y]], 
                            mXOffset 
+ x * mTileSize,
                            mYOffset 
+ y * mTileSize,
                            mPaint);
                }
            }
        }
  }

  onDraw要做的工作非常簡單,就是掃描每一個方格,根據方格當前狀態,從圖標文件中選擇對應的圖標繪制到這個方格上。當然這個onDraw在游戲進行過程中,會不斷地被調用,從而界面不斷被更新。

  游戲邏輯部分

  再來看子類SnakeView是如何在父類TileView的基礎上,加入特定的游戲邏輯,從而完成Snake這個程序的。

    private ArrayList<Coordinate> mSnakeTrail = new ArrayList<Coordinate>();//組成貪食蛇的方格列表
    private ArrayList<Coordinate> mAppleList = new ArrayList<Coordinate>();//蘋果方格列表

  由于SnakeViewTileView繼承而來,則可以說它已經擁有這個二維方格地圖了(只是此時地圖里的所有方格狀態都是0)。那么它有了這么一個二維方格地圖,如何去初始化這個地圖呢?這在initNewGame函數中實現。

    private void initNewGame()
    {
        
//清空蛇和蘋果占據的方格
        mSnakeTrail.clear();
        mAppleList.clear();
        
//目前組成蛇的方格式固定的,而且方向也固定朝北
        mSnakeTrail.add(new Coordinate(77));
        mSnakeTrail.add(
new Coordinate(67));
        mSnakeTrail.add(
new Coordinate(57));
        mSnakeTrail.add(
new Coordinate(47));
        mSnakeTrail.add(
new Coordinate(37));
        mSnakeTrail.add(
new Coordinate(27));
        mNextDirection 
= NORTH;

        
//隨即加入蘋果
        for (int i = 0; i < nApples; ++i)
        {
            addRandomApple();
        }
        
//初始化運動速率和玩家成績
        mMoveDelay = 600;
        mScore 
= 0;
}

  想象下對整個游戲屏幕拍張照,然后對其下一個狀態再拍張照,那么兩張照片之間的區別是怎么產生的呢?對于系統來說,它只知道不斷調用onDraw,后者負責對整個屏幕進行繪制,那要產生兩個屏幕之間的差異,肯定要通過一些手段對某些數據結構(比如這里的二維方格地圖)進行調整(比如用戶的控制指令,定時器等),然后等到下一次onDraw時就會把這些更改在界面上反映出來。
   這里要著重說明下private long mMoveDelay = 600;這個成員變量,雖然很不起眼,但仔細考慮它的作用就會發現很有趣,那么改變它的大小到底是如何讓我們感覺到游戲變快或變慢呢?

可以打個簡單的比方,在時刻0游戲啟動,首先把蛇和蘋果的位置都在方格地圖上作好了標記,然后我們在update函數中修改蛇身讓蛇向北前進一步,而這個改變此時還只是停留在內部的核心數據結構上(即二維方格地圖),還沒有在界面上顯示出來。當然,我們馬上想到要想讓這更改顯示出來,讓系統調用onDraw去繪制不就完了嗎?可是問題是我們不知道系統是隔多長時間去調用onDraw函數,于是mMoveDelay此時就發揮作用了,通過它就可以設置休眠的時間,等時間一到,馬上就會通知SnakeView去重繪制。你可以試試把mMoveDelay數值調大,就會看出我上面提到的“拍照“的效果。

  Handler的使用

  寫過JavaScript或者ActionScript的開發者,對于setInterval的用法會非常了解。那么在Android中如何實現setInterval的方法呢?其中有兩種方法可以實現類似的功能,其中一個是在線程中調用Handler方法,另外一個是應用Timer。Snake中使用了前者

 class RefreshHandler extends Handler 
    {
        @Override
        
public void handleMessage(Message msg) 
        {
//“蘇醒”后的處理
           SnakeView.this.update();
           SnakeView.
this.invalidate();
        }
        
public void sleep(long delayMillis) 
        {
//休眠delayMillis毫秒
            this.removeMessages(0);
            sendMessageDelayed(obtainMessage(
0), delayMillis);
        }
};

而實際調用的處理函數update就可以說是整個游戲的引擎,正是由于它的工作(修改蛇和蘋果的狀態到一個新的狀態,然后休眠自己,然后等到蘇醒后在Handler中就會讓系統區繪制上次修改過的二維方塊地圖,然后再次調用update,如此循環反復,生生不息),才使得游戲不斷被推進,因此,比做“引擎“不為過。

   public void update()
    {
        
if (mMode == RUNNING)
        {
            
long now = System.currentTimeMillis();
            
if (now - mLastMove > mMoveDelay) 
            {
                clearTiles();
                updateWalls();
                updateSnake();
                updateApples();
                mLastMove 
= now;
            }
            mRedrawHandler.sleep(mMoveDelay);
        }
    }

  既然update是游戲的動力,要讓游戲停止下來只要不再調用update就可以了(因為此時其實是畫面靜止了),因此游戲進入暫停(這個狀態還可以轉為“運行“,其實就是繼續可以修改,再繪制),若進入失敗(其實此時二維方塊地圖還停留在最后一個畫面處,這也是為什么在開始時要首先清理掉整個地圖)【這一點,可以在游戲失敗后,再次開始新游戲,此時通過設置的斷點即可觀察到上次游戲運行時的底層數據】。

  一點困惑
  可是個人認為Snake下面這段代碼讀起來有點怪,有點像一個“先有雞,還是先有蛋?“的問題,導致我的思維邏輯上出現一個“怪圈“。

    public void handleMessage(Message msg) 
    {
            SnakeView.
this.update();
            SnakeView.
this.invalidate();
     }

  按照這段代碼的意思來看,當休眠的時間已經到了,首先去調用update,即為下一次繪制做準備工作,再讓自己休眠起來,最后通知系統重繪制自己。

  哎,這讓我難以理解,還是回到時刻0的例子來說,在時刻0時讓蛇身向北前進了一步(指的是底層的二維方格地圖的修改,不是界面),然后讓自己休眠0.6毫秒,當時間到了,首先去調用update方法,那么就又會讓蛇身做出修改,也就是把上一次還沒繪制的覆蓋掉了(那么上一次的修改豈不是白費,還沒畫上去呢),更何況在update中又會讓自己去休眠(還沒調用invalidate,怎么又去休眠了?),又怎么還能去通知系統調用我的onDraw方法呢?也就是說invalidate根本沒有執行???

  按我的理解,應該把順序顛倒一下,先通知系統去調用onDraw方法重繪,使得上一次對底層二維方格地圖的修改顯示出來,然后再去為下一次修改做準備工作,最后讓自己進入休眠,等待蘇醒過來,如此循環反復。實驗證明,顛倒過來也是正確的,不過關于這一個迷惑我的地方,希望有朋友能指點我一下!

記得在javascript里使用setInterval時,也是先寫處理邏輯,然后在末尾處寫上一句setInterval(這也是我習慣的思維方式了),難道google上面這種寫法有何深意?

   此外,感覺每次繪制時都重新繪制墻壁,有點浪費時間,因為墻壁根本沒有任何變化的。還有就是mLastMove這個變量設置的初衷是保證當前時間點距上一次變化已經過去了mMoveDelay毫秒,可是既然已經用了sleep機制,再使用這個時間差看上去并無必要。

0
0
 
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()