Android實例剖析筆記(八)

作者: Phinecos(洞庭散人)  來源: 博客園  發布時間: 2010-10-24 22:39  閱讀: 1577 次  推薦: 0   原文鏈接   [收藏]  
摘要:分析Android自帶的另一個小游戲LunarLander,它與前者的“定時器+系統調用onDraw”架構相比,由于采用了“多線程+強制自行繪制”的架構思路,因而更為實用。

   和Snake的比較

      就界面Layout來說,這個程序其實和Snake沒有什么不同,同樣是采用了FrameLayout,而且游戲的主界面由一個自定義的View來實現,這里是LunarView。讀過上一篇文章的朋友也許會發現,Snake的架構是“定時器+系統調用onDraw”來實現的,這里有一個最大的缺陷就是onDraw是由Android系統來調用的,我們只能依賴它,卻無法自行控制。這就好比一個黑盒,當然,總是能把我們要的東西給做出來,可卻無法控制其做事的細節,這對于游戲這樣高效率的東西可是不利的,因此最好的解決之道當然是把繪制這部分工作自己”承包“過來,告別吃大鍋飯的,進入”聯產承包制”時代。

      此外,由于游戲的本質就是連續兩幀圖片之間發生些許差異,那么要不斷催生這種差異的發生,只要有某種連續不斷發生的事件在進行就可以,例如Snake中使用的定時器,就是在不斷地產生這種“差異源”,與此類似,一個線程也是不斷在運行中,通過它也是可以不斷產生這種“差異源”的。

  SurfaceView初探

      如果說Snake中使用的Layout加自定義View是一把小型武器的話,那在SurfaceView對于android中游戲的開發來說就算是重型武器了。我們使用前者時總是容易把游戲中某個對象(比如上文的每一個方格)當做一個小組件來處理,而后者則根本沒有這種劃分的概念,在它眼中,所有東西都是在Canvas(畫布)中自行繪制出來的(背景,人物等)。

  SurfaceView提供直接訪問一個可畫圖的界面,可以控制在界面頂部的子視圖層。SurfaceView是提供給需要直接畫像素而不是使用窗體部件的應用使用的。Android圖形系統中一個重要的概念和線索是surface。View及其子類(如TextView, Button)

  要畫在surface上。每個surface創建一個Canvas對象(但屬性時常改變),用來管理view在surface上的繪圖操作,如畫點畫線。還要注意的是,使用它的時候,一般都是出現在最頂層的:The view hierarchy will take care of correctly compositing with the Surface any siblings of the SurfaceView that would normally appear on top of it. 使用的SurfaceView的時候,一般情況下還要對其進行創建,銷毀,改變時的情況進行監視,這就要用到SurfaceHolder.Callback.

class LunarView extends SurfaceView implements SurfaceHolder.Callback
{
    
public void surfaceChanged(SurfaceHolder holder,int format,int width,int height){}
//在surface的大小發生改變時激發
    public void surfaceCreated(SurfaceHolder holder){}
//在創建時激發,一般在這里調用畫圖的線程。
    public void surfaceDestroyed(SurfaceHolder holder) {}
//銷毀時激發,一般在這里將畫圖的線程停止、釋放。
}

 surfaceCreated會首先被調用,然后是surfaceChanged,當程序結束時會調用surfaceDestroyed。下面來看看LunarView最重要的成員變量,也就是負責這個View所有處理的線程

private LunarThread thread; // 實際工作線程
        thread = new LunarThread(holder, context, new Handler() {
            @Override
            
public void handleMessage(Message m) 
            {
                mStatusText.setVisibility(m.getData().getInt(
"viz"));
                mStatusText.setText(m.getData().getString(
"text"));
            }
        });

  這個線程由私有類LunarThread實現,它里面還有一個自己的消息隊列處理器,用來接收游戲狀態消息,并在屏幕上顯示當前狀態(而這個功能在Snake中是通過View自己控制其包含的TextView是否顯示來實現的,相比之下,LunarThread的消息處理機制更為高效)。由于有了LunarThread這個負責具體工作的對象,所以LunarView的大部分工作都委托給后者去執行。

    public void surfaceChanged(SurfaceHolder holder, int format, int width,int height)
    {
        thread.setSurfaceSize(width, height);
    }

public void surfaceCreated(SurfaceHolder holder)
{
//啟動工作線程結束
        thread.setRunning(true);
        thread.start();
    }
    
public void surfaceDestroyed(SurfaceHolder holder)
    {
        
boolean retry = true;
        thread.setRunning(
false);
        
while (retry) 
        {
            
try
            {//等待工作線程結束,主線程才結束
                thread.join();
                retry 
= false;
            } 
            
catch (InterruptedException e) 
            {
            }
        }
    }

  工作線程LunarThread

  由于SurfaceHolder是一個共享資源,因此在對其操作時都應該實行“互斥操作“,即需要使用synchronized進行”封鎖“機制。

      再來討論下為什么要使用消息機制來更新界面的文字信息呢?其實原因是這樣的,渲染文字的工作實際上是主線程(也就是LunarView類)的父類View的工作,而并不屬于工作線程LunarThread,因此在工作線程中式無法控制的。所以我們改為向主線程發送一個Message來代替,讓主線程通過Handler對接收到的消息進行處理,從而更新界面文字信息。再回顧上一篇SnakeView里的文字信息更新,由于是SnakeView自己(就這一個線程)對其包含的TextView做控制,當然沒有這樣的問題了。

 

  public void setState(int mode, CharSequence message) 
        {
            
synchronized (mSurfaceHolder)
            {
                mMode 
= mode;
                
if (mMode == STATE_RUNNING)
                {
//運行中,隱藏界面文字信息
                    Message msg = mHandler.obtainMessage();
                    Bundle b 
= new Bundle();
                    b.putString(
"text""");
                    b.putInt(
"viz", View.INVISIBLE);
                    msg.setData(b);
                    mHandler.sendMessage(msg);
                } 
                
else 
                {
//根據當前狀態設置文字信息
                    mRotating = 0;
                    mEngineFiring 
= false;
                    Resources res 
= mContext.getResources();
                    CharSequence str 
= "";
                    
if (mMode == STATE_READY)
                        str 
= res.getText(R.string.mode_ready);
                    
else if (mMode == STATE_PAUSE)
                        str 
= res.getText(R.string.mode_pause);
                    
else if (mMode == STATE_LOSE)
                        str 
= res.getText(R.string.mode_lose);
                    
else if (mMode == STATE_WIN)
                        str 
= res.getString(R.string.mode_win_prefix)
                                
+ mWinsInARow + " "
                                + res.getString(R.string.mode_win_suffix);
                    
if (message != null) {
                        str 
= message + "\n" + str;
                    }
                    
if (mMode == STATE_LOSE) 
                        mWinsInARow 
= 0;
                    Message msg 
= mHandler.obtainMessage();
                    Bundle b 
= new Bundle();
                    b.putString(
"text", str.toString());
                    b.putInt(
"viz", View.VISIBLE);
                    msg.setData(b);
                    mHandler.sendMessage(msg);
                }
            }
        }

  下面就是LunaThread這個工作線程的執行函數了,它一直不斷在重復做一件事情:鎖定待繪制區域(這里是整個屏幕),若游戲還在進行狀態,則更新底層的數據,然后直接強制界面重新繪制。

   public void run() 
        {
            
while (mRun) 
            {
                Canvas c 
= null;
                
try 
                {
                    
//鎖定待繪制區域
                    c = mSurfaceHolder.lockCanvas(null);
                    
synchronized (mSurfaceHolder)
                    {
                        
if (mMode == STATE_RUNNING) 
                            updatePhysics();
//更新底層數據,判斷游戲狀態
                        doDraw(c);//強制重繪制
                    }
                } 
                
finally 
                {
                    
if (c != null) {
                        mSurfaceHolder.unlockCanvasAndPost(c);
                    }
                }
            }
        }

  這里要注意的是最后要調用unlockCanvasAndPost來結束鎖定畫圖,并提交改變

  強行自繪制

   doDraw這段代碼就是在自己的Canvas上進行繪制,具體的繪制就不解釋了,主要就是用drawBitmap,drawRect,drawLine。值得注意的一段代碼是下面這個:

    canvas.save();
            canvas.rotate((
float) mHeading, (float) mX, mCanvasHeight
                    
- (float) mY);
            
if (mMode == STATE_LOSE) {
                mCrashedImage.setBounds(xLeft, yTop, xLeft 
+ mLanderWidth, yTop
                        
+ mLanderHeight);
                mCrashedImage.draw(canvas);
            } 
else if (mEngineFiring) {
                mFiringImage.setBounds(xLeft, yTop, xLeft 
+ mLanderWidth, yTop
                        
+ mLanderHeight);
                mFiringImage.draw(canvas);
            } 
else {
                mLanderImage.setBounds(xLeft, yTop, xLeft 
+ mLanderWidth, yTop
                        
+ mLanderHeight);
                mLanderImage.draw(canvas);
            }
            canvas.restore();

  在繪制火箭的前后,調用了save()和restore(),它是先保存當前矩陣,將其復制到一個私有堆棧上。然后接下來對rotate的調用還是在原有的矩陣上進行操作,但當restore調用后,以前保存的設置又重新恢復。不過,在這里還是看不出有什么用處。。。

  暫停/繼續機制

      LunarLancher的暫停其實并沒有不再強制重繪制,而是沒有對底層的數據做任何修改,依然繪制同一幀畫面,而繼續則是把mLastTime設置為當前時間+100毫秒的時間點,因為以前暫停時mLastTime就不再更新了,這樣做事為了與當前時間同步起來。

     public void pause()
        {
//暫停
            synchronized (mSurfaceHolder) 
            {
                
if (mMode == STATE_RUNNING)
                    setState(STATE_PAUSE);
            }
        }
        
public void unpause()
        {
// 繼續
            
// Move the real time clock up to now
            synchronized (mSurfaceHolder)
            {
                mLastTime 
= System.currentTimeMillis() + 100;
            }
            setState(STATE_RUNNING);
        }

  這樣做的目的是為了制造延遲的效果,都是因為updatePhysics函數里這兩句

           if (mLastTime > now) return;
        
double elapsed = (now - mLastTime) / 1000.0;

至于游戲的控制邏輯和判定部分就不介紹了,沒有多大意思。

0
0
 
 
 

文章列表

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

    IT工程師數位筆記本

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