雨松MOMO帶你走進遊戲開發的世界之View與SurFaceView的區別
雨松MOMO原創文章如轉載,請註明:轉載至我的獨立網域名稱部落格雨松MOMO程式研究院,原文地址:http://www.xuanyusong.com/archives/266
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線程來重新整理規定矩形地區。
@Overridepublic 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繪製在螢幕上,怎麼樣 簡單吧 呵呵。
@Overrideprotected 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介面還是非常有用處的。
@Overridepublic void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2,int arg3) { // surfaceView的大小發生改變的時候 }@Overridepublic void surfaceCreated(SurfaceHolder arg0) { /**啟動遊戲主線程**/ mIsRunning = true; mThread = new Thread(this); mThread.start();}@Overridepublic void surfaceDestroyed(SurfaceHolder arg0) { // surfaceView銷毀的時候 mIsRunning = false;}
在遊戲主線程迴圈中在繪製開始 先拿到畫布canvas 並使用mSurfaceHolder.lockCanvas()鎖定畫布,等繪製結束以後 使用mSurfaceHolder.unlockCanvasAndPost(mCanvas)解鎖畫布, 解鎖畫布以後畫布上的內容才會顯示到螢幕上。
@Overridepublic 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;@Overridepublic 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();} }}
最後由於代碼較多我就不貼在部落格中了 , 下面給出Demo源碼的歡迎大家下載閱讀互相學習,互相研究,互相討論 雨松MOMO希望可以和大家一起進步。
:http://www.xuanyusong.com/archives/266