Android遊戲開發之構建遊戲框架View與SurFaceView的區別(五)

Android遊戲開發之構建遊戲框架View與SurFaceView的區別(五)
 

發佈於 2011-10-20

http://www.uml.org.cn/mobiledev/201110205.asp

1.view

view在api中的結構

java.lang.Object

android.view.View

直接子類:

AnalogClock, ImageView, KeyboardView, ProgressBar, SurfaceView, TextVie, ViewGroup, ViewStub

間接子類:

AbsListView, AbsSeekBar, AbsSpinner, AbsoluteLayout, AdapterView<T extends Adapter>, AppWidgetHostView, AutoCompleteTextView, Button, CheckBox, CheckedTextView, Chronometer, CompoundButton, DatePicker, DialerFilter, DigitalClock,EditView, ExpandableListView, ExtractEditText, FrameLayout, GLSurfaceView, Gallery, GestureOverlayView, GridView, HorizontalScrollView, ImageButton, ImageSwitcher, LinearLayout, ListView, MediaController, MultiAutoCompleteTextView, QuickContactBadge, RadioButton, RadioGroup, RatingBar, RelativeLayout, ScrollView, SeekBar, SlidingDrawer, Spinner, TabHost, TabWidget, TableLayout, TableRow, TextSwitcher, TimePicker, ToggleButton, TwoLineListItem, VideoView, ViewAnimator, ViewFlipper, ViewSwitcher, WebView, ZoomButton, ZoomControls

由此可見View類屬於Android開發繪製中的顯示老大,任何與繪製有關係的控件都是它的子類。在這篇文章中我主要講View 與SurFaceView 使用線程刷新屏幕繪製方面的知識。開發中如何去選擇使用View還是SurFaceView。我相信讀過我前幾篇博客的朋友應該知道我在刷新屏幕的時候使用invalidate()方法來重繪,下面我詳細的說明一下Andooid刷新屏幕的幾種方法。

第一種: 在onDraw方法最後調用invalidate()方法,它會通知UI線程重繪 這樣 View會重新調用onDraw方法,實現刷新屏幕。 這樣寫看起來代碼非常簡潔漂亮,但是它也同時存在一個很大的問題,它和遊戲主線程是分開的 它違背了單線程模式,這樣操作繪製的話是很不安全的,舉個例子 比如程序先進在Activity1中 使用invalidate()方法來重繪, 然後我跳到了Activity2這時候Activity1已經finash()掉 可是Activity1中 的invalidate() 的線程還在程序中,Android的虛擬機不可能主動殺死正在運行中的線程所以這樣操作是非常危險的。因爲它是在UI線程中被動掉用的所以很不安全。

invalidate() 更新整個屏幕區域

invalidate(Rect rect) 更新Rect區域

invalidate(l, t, r, b) 更新指定矩形區域

public void onDraw(Canvas canvas){    

        DosomeThing();    

        invalidate();    

第二種:使用postInvalidate();方法來刷新屏幕 ,調用後它會用handler通知UI線程重繪屏幕,我們可以 new Thread(this).start(); 開啓一個遊戲的主線程 然後在主線程中通過調用postInvalidate();方法來刷新屏幕。postInvalidate();方法 調用後 系統會幫我們調用onDraw方法 ,它是在我們自己的線程中調用 通過調用它可以通知UI線程刷新屏幕 。由此可見它是主動調用UI線程的。所以建議使用postInvalidate()方法通知UI線程來刷新整個屏幕。

postInvalidate(left, top, right, bottom) 方法 通過UI線程來刷新規定矩形區域。

@Override

public void run() {  

    while (mIsRunning) {  

    try {  

        Thread.sleep(100);  

                   postInvalidate();  

    } catch (InterruptedException e) {  

        // TODO Auto-generated catch block  

        e.printStackTrace();  

    }  

    }  

}  

View中用到的雙緩衝技術

重繪的原理是 程序根據時間來刷新屏幕 如果有一幀圖形還沒有完全繪製結束 程序就開始刷新屏幕這樣就會造成瞬間屏幕閃爍 畫面很不美觀,所以雙緩衝的技術就誕生了。它存在的目的就是解決屏幕閃爍的問題,下面我說說在自定義View中如何實現雙緩衝。

首先我們需要創建一張屏幕大小的緩衝圖片,我說一下第三個參數 ARGB 分別代表的是 透明度 紅色 綠色 藍色

Bitmap.Config ARGB_4444 ARGB 分別佔四位

Bitmap.Config ARGB_8888 ARGB 分別佔八位

Bitmap.Config RGB_565 沒有透明度(A) R佔5位 G 佔6位 B佔5位

一般情況下我們使用ARGB_8888 因爲它的效果是最好了 當然它也是最佔內存的。

mBufferBitmap = Bitmap.createBitmap(mScreenWidth,mScreenHeight,Config.ARGB_8888);

創建一個緩衝的畫布,將內容繪製在緩衝區mBufferBitmap中

Canvas mCanvas = new Canvas();

mCanvas.setBitmap(mBufferBitmap);

最後一次性的把緩衝區mBufferBitmap繪製在屏幕上,怎麼樣 簡單吧 呵呵。

@Override  

protected void onDraw(Canvas canvas) {  

    /**這裏先把所有須要繪製的資源繪製到mBufferBitmap上**/  

    /**繪製地圖**/  

    DrawMap(mCanvas,mPaint,mBitmap);  

    /**繪製動畫**/  

    RenderAnimation(mCanvas);  

    /**更新動畫**/  

    UpdateAnimation();  

    if(isBorderCollision) {  

    DrawCollision(mCanvas,"與邊界發生碰撞");  

    }  

    if(isAcotrCollision) {  

    DrawCollision(mCanvas,"與實體層發生碰撞");  

    }  

    if(isPersonCollision) {  

    DrawCollision(mCanvas,"與NPC發生碰撞");  

    }  

    /**最後通過canvas一次性的把mBufferBitmap繪製到屏幕上**/  

    canvas.drawBitmap(mBufferBitmap, 0,0, mPaint);  

    super.onDraw(canvas);  

}  

由此可見view屬於被動刷新, 因爲我們做的任何刷新的操作實際上都是通知UI線程去刷新。所以在做一些只有通過玩家操作以後纔會刷新屏幕的遊戲 並非自動刷新的遊戲 可以使用view來操作。

2.SurfaceView

從API中可以看出SurfaceView屬於View的子類 它是專門爲製作遊戲而產生的,它的功能非常強大,最重要的是它支持OpenGL ES庫,2D和3D的效果都可以實現。創建SurfaceView的時候需要實現SurfaceHolder.Callback接口,它可以用來監聽SurfaceView的狀態,SurfaceView的改變 SurfaceView的創建 SurfaceView 銷燬 我們可以在相應的方法中做一些比如初始化的操作 或者 清空的操作等等。

使用SurfaceView構建遊戲框架它的繪製原理是繪製前先鎖定畫布 然後等都繪製結束以後 在對畫布進行解鎖 最後在把畫布內容顯示到屏幕上。

代碼中是如何實現SurfaceView

首先需要實現 Callback 接口 與Runnable接口

public class AnimView extends SurfaceView implements Callback,Runnable

獲取當前mSurfaceHolder 並且把它加到CallBack回調函數中

SurfaceHolder mSurfaceHolder = getHolder();

mSurfaceHolder.addCallback(this);

通過callBack接口監聽SurfaceView的狀態, 在它被創建的時候開啓遊戲的主線程,結束的時候銷燬。這裏說一下在View的構造函數中是拿不到view有關的任何信息的,因爲它還沒有構建好。 所以通過這個監聽我們可以在surfaceCreated()中拿到當前view的屬性 比如view的寬高 等等,所以callBack接口還是非常有用處的。

@Override

public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2,  

    int arg3) {  

    // surfaceView的大小發生改變的時候  

}  

@Override

public void surfaceCreated(SurfaceHolder arg0) {  

    /**啓動遊戲主線程**/  

    mIsRunning = true;  

    mThread = new Thread(this);  

    mThread.start();  

}  

@Override

public void surfaceDestroyed(SurfaceHolder arg0) {  

 // surfaceView銷燬的時候  

    mIsRunning = false;  

在遊戲主線程循環中在繪製開始 先拿到畫布canvas 並使用mSurfaceHolder.lockCanvas()鎖定畫布,等繪製結束以後 使用mSurfaceHolder.unlockCanvasAndPost(mCanvas)解鎖畫布, 解鎖畫布以後畫布上的內容纔會顯示到屏幕上。

@Override

public void run() {  

    while (mIsRunning) {  

    try {  

        Thread.sleep(100);  

    } catch (InterruptedException e) {  

        // TODO Auto-generated catch block  

        e.printStackTrace();  

    }  

    //在這裏加上線程安全鎖  

    synchronized (mSurfaceHolder) {  

        /**拿到當前畫布 然後鎖定**/  

        mCanvas =mSurfaceHolder.lockCanvas();    

        Draw();  

        /**繪製結束後解鎖顯示在屏幕上**/  

        mSurfaceHolder.unlockCanvasAndPost(mCanvas);  

    }  

    }  

}  

由此可見SurfaceView 屬於主動刷新 ,重繪過程完全是在我們自己的線程中完成 , 由於遊戲中肯定會執行各種絢麗的動畫效果如果使用被動刷新的View就有可能就會阻塞UI線程,所以SurfaceView 更適合做遊戲。

效果圖

最近有朋友反映說運行起來有點卡 我解釋一下, 卡的主要原因是我的地圖文件太大了,當然還有模擬器不給力的原因。我每繪製一塊地圖就須要使用裁剪原圖,頻繁的切割如此大的圖片肯定會造成卡頓的情況。同學們在製作的時候將沒用的地圖塊去掉,保留只需要的地圖塊這樣會流暢很多喔 。

優化遊戲主線程循環

同學們先看看這段代碼,Draw()方法繪製結束讓線程等待100毫秒在進入下一次循環。其實這樣更新遊戲循環是很不科學的,原因是Draw()方法每一次更新所耗費的時間是不確定的。舉個例子 比如第一次循環Draw() 耗費了1000毫秒 加上線程等待100毫秒 整個循環耗時1100毫秒,第二次循環Draw() 耗時2000毫秒 加上線程等待時間100毫秒 整個循環時間就是2100毫秒。很明顯這樣就會造成遊戲運行刷新時間時快時慢,所以說它是很不科學的。

public void run() {  

    while (mIsRunning) {  

    //在這裏加上線程安全鎖  

    synchronized (mSurfaceHolder) {  

        /**拿到當前畫布 然後鎖定**/  

        mCanvas =mSurfaceHolder.lockCanvas();    

        Draw();  

        /**繪製結束後解鎖顯示在屏幕上**/  

        mSurfaceHolder.unlockCanvasAndPost(mCanvas);  

    }  

    try {  

        Thread.sleep(100);  

    } catch (InterruptedException e) {  

        e.printStackTrace();  

    }  

    }  

}  

在貼一段科學的控遊戲制循環代碼,每次循環遊戲主線程 在Draw()方法前後計算出Draw()方法所消耗的時間,然後在判斷是否達到我們規定的刷新屏幕時間,下例是以30幀刷新一次屏幕,如果滿足則繼續下次循環如果不滿足使用Thread.yield(); 讓遊戲主線程去等待 並計算當前等待時間直到等待時間滿足30幀爲止在繼續下一次循環刷新遊戲屏幕。

這裏說一下Thread.yield(): 與Thread.sleep(long millis):的區別,Thread.yield(): 是暫停當前正在執行的線程對象 ,並去執行其他線程。Thread.sleep(long millis):則是使當前線程暫停參數中所指定的毫秒數然後在繼續執行線程。

 /**每30幀刷新一次屏幕**/  

       public static final int TIME_IN_FRAME = 30;  

@Override

public void run() {  

    while (mIsRunning) {  

    /**取得更新遊戲之前的時間**/  

    long startTime = System.currentTimeMillis();  

    /**在這裏加上線程安全鎖**/  

    synchronized (mSurfaceHolder) {  

        /**拿到當前畫布 然後鎖定**/  

        mCanvas =mSurfaceHolder.lockCanvas();    

        Draw();  

        /**繪製結束後解鎖顯示在屏幕上**/  

        mSurfaceHolder.unlockCanvasAndPost(mCanvas);  

    }  

    /**取得更新遊戲結束的時間**/  

    long endTime = System.currentTimeMillis();  

    /**計算出遊戲一次更新的毫秒數**/  

    int diffTime  = (int)(endTime - startTime);  

    /**確保每次更新時間爲30幀**/  

    while(diffTime <=TIME_IN_FRAME) {  

        diffTime = (int)(System.currentTimeMillis() - startTime);  

        /**線程等待**/  

        Thread.yield();  

    }  

    }  

}  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章