【讀書筆記《Android游戲編程之從零開始》】11.游戲開發基礎(SurfaceView 游戲框架、View 和 SurfaceView 的區別)


1. SurfaceView 游戲框架實例

實例效果:就是屏幕上的文本跟着點擊的地方移動,效果圖如下:

 

步驟:

新建項目“GameSurfaceView”,首先自定義一個類"MySurfaceView",此類繼承SurfaceView,並實現android.view.SurfaceHolder.Callback 接口,代碼如下

package com.example.ex4_5;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;
import android.view.SurfaceView;

//Callback接口用於SurfaceHolder 對SurfaceView 的狀態進行監聽
public class MySurfaceView extends SurfaceView implements Callback{
    //用於控制SurfaceView 的大小、格式等,並且主要用於監聽SurfaceView 的狀態
    private SurfaceHolder sfh;
    private Paint paint;
    private int textX=30,textY=30;
    public MySurfaceView(Context context) {
        super(context);
        //實例SurfaceView
        sfh = this.getHolder();
        //為SurfaceView添加狀態監聽
        sfh.addCallback(this);
        //實例一個畫筆
        paint = new Paint();
        //設置字體大小
        paint.setTextSize(30);
        //設置畫筆的顏色
        paint.setColor(Color.GREEN);
    }

    @Override
    //當SurfaceView 被創建完成后響應
    public void surfaceCreated(SurfaceHolder holder) {
        myDraw();
    }

    @Override
    //當SurfaceView 狀態發生改變時響應
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {
        
    }

    @Override
    //當SurfaceView 狀態被摧毀時響應
    public void surfaceDestroyed(SurfaceHolder holder) {
        
    }

    //SurfaceView 是通過SurfaceHolder 來修改其數據,所以即時重寫View 的onDraw(Canvas canvas)函數,在SurfaceView 啟動時也不會執行到,因此這里自定義繪圖函數
    public void myDraw()
    {
        //獲取SurfaceView 的Canvas 對象,
        //同時對獲取的Canvas 畫布進行加鎖,防止SurfaceView 在繪制過程中被修改、摧毀等發生的狀態改變
        //另外一個lockCanvas(Rect rect)函數,其中傳入一個Rect矩形類的實例,用於得到一個自定義大小的畫布
        Canvas canvas = sfh.lockCanvas();
        //填充背景色,即刷屏,每次在畫布繪圖前都對畫布進行一次整體的覆蓋
        canvas.drawColor(Color.BLACK);
        //繪制內容
        canvas.drawText("This is a Text !", textX, textY, paint);
        //解鎖畫布和提交
        sfh.unlockCanvasAndPost(canvas);
    }

    //重寫觸屏監聽事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        textX = (int)event.getX();
        textY = (int)event.getY();
        myDraw();
        return super.onTouchEvent(event);
    }
    
}
MySurfaceView

修改MainActivity 類,讓其顯示自定義的SurfaceView 視圖

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //顯示自定義的SurfaceView 視圖
    setContentView(new MySurfaceView(this));
}
}

配置文件中設置應用程序為全屏

  android:theme="@android:style/Theme.NoTitleBar.Fullscreen" 

具體說明可查看代碼。

 

2.SurfaceView 視圖添加線程

在游戲中,基本上不會等到用戶每次觸發了按鍵事件、觸屏事件才去重繪畫布,而是會固定一個時間去刷新畫布:比如游戲中的倒計時、動態的花草、流水等等,這些游戲元素並不會跟玩家交互,但是這些元素都是動態的。所以游戲開發中會有一個線程不停的去重繪畫布,實時的更新游戲元素的狀態。
當然游戲中除了畫布給玩家最直接的動態展現外,也會有很多邏輯需要不斷的去更新,比如怪物的AI(人工智能)、游戲中錢幣的更新等等。

下面給上面實例中的SurfaceView 視圖添加線程,用於不停的重繪畫布以及不停地執行游戲邏輯。

實例效果如下:

修改后,MySurfaceView 類代碼如下:

package com.example.ex4_5;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;
import android.view.SurfaceView;

//Callback接口用於SurfaceHolder 對SurfaceView 的狀態進行監聽
public class MySurfaceView extends SurfaceView implements Callback, Runnable {
    // 用於控制SurfaceView 的大小、格式等,並且主要用於監聽SurfaceView 的狀態
    private SurfaceHolder sfh;
    // 聲明一個畫筆
    private Paint paint;
    // 文本坐標
    private int textX = 30, textY = 30;
    // 聲明一個線程
    private Thread th;
    // 線程消亡的標識符
    private boolean flag;
    // 聲明一個畫布
    private Canvas canvas;
    // 聲明屏幕的寬高
    private int screenW, screenH;

    /**
     * SurfaceView 初始化函數
     * 
     * @param context
     */
    public MySurfaceView(Context context) {
        super(context);
        // 實例SurfaceView
        sfh = this.getHolder();
        // 為SurfaceView添加狀態監聽
        sfh.addCallback(this);
        // 實例一個畫筆
        paint = new Paint();
        // 設置字體大小
        paint.setTextSize(20);
        // 設置畫筆的顏色
        paint.setColor(Color.WHITE);
        // 設置焦點
        setFocusable(true);
    }

    /**
     * SurfaceView 視圖創建,響應此函數
     */
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        screenW = this.getWidth();
        screenH = this.getHeight();
        flag = true;
        // 實例線程
        th = new Thread(this);
        // 啟動線程
        th.start();
    }

    /**
     * SurfaceView 視圖狀態發生改變時,響應此函數
     */
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {

    }

    /**
     * SurfaceView 視圖消亡時,響應此函數
     */
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        flag = false;
    }

    /**
     * 游戲繪圖
     */
    public void myDraw() {
        try {
            canvas = sfh.lockCanvas();
            if (canvas != null) {
                // ————利用繪制矩形的方式刷屏
                // canvas.drawRect(0, 0, this.getWidth(), this.getHeight(),
                // paint);
                // ————利用填充畫布,刷屏
                // canvas.drawColor(Color.BLACK);
                // ————利用填充畫布指定的顏色分量,刷屏
                canvas.drawRGB(0, 0, 0);
                canvas.drawText("啦啦啦,德瑪西亞!", textX, textY, paint);
            }
        } catch (Exception e) {
            // TODO: handle exception
        } finally {
            if (canvas != null) {
                sfh.unlockCanvasAndPost(canvas);
            }
        }
    }

    /**
     * 觸屏事件監聽
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        textX = (int) event.getX();
        textY = (int) event.getY();
        return true;
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        return super.onKeyDown(keyCode, event);
    }

    /**
     * 游戲邏輯
     */
    private void logic() {
    }

    @Override
    public void run() {
        while (flag) {
            long start = System.currentTimeMillis();
            myDraw();
            logic();
            long end = System.currentTimeMillis();
            try {
                if (end - start < 50) {
                    Thread.sleep(50 - (end - start));
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}
MySurfaceView

代碼說明:

(1)線程標識位

在代碼中“boolean flag;”語句聲明一個布爾值,它主要用於以下兩點:

①便於消亡線程

一個線程一旦啟動,就會執行run() 函數,run() 函數執行結束后,線程也伴隨着消亡。由於游戲開發中使用的線程一般都會在run() 函數中使用一個while 死循環,在這個循環中會調用繪圖和邏輯函數,使得不斷的刷新畫布和更新邏輯;那么如果游戲暫停或者游戲結束時,為了便於銷毀線程在此設置一個標識位來控制。

②防止重復創建線程及程序異常

為什么會重復創建線程,首先從Android 系統的手機說起。熟悉或者接觸過 Android 系統的人都知道,Android 手機上一般都有“Back(返回)”與“Home(小房子)”按鍵。不管當前手機運行了什么程序,只要單擊“Back”或者“Home”按鍵的時候,默認會將當前的程序切入到系統后台運行(程序中沒有截獲這兩個按鈕的前提下);也正因為如此,會造成MySurfaceView 視圖的狀態發生改變。

首先單擊“Back” 按鈕使當前程序切入后台,然后單擊項目重新回到程序中,SurfaceView 的狀態變化為:surfaceDestroyed -> 構造函數 -> surfaceCreated -> surfaceChanged 。
然后單擊“Home” 按鈕使當前程序切入后台,然后單擊項目重新回到程序中,SurfaceView 的狀態變化為:surfaceDestroyed -> surfaceCreated -> surfaceChanged 。
通過 SurfaceView 的狀態變化可以明顯看到,當點擊“Back” 按鈕並重新進入程序的過程要比點擊“Home” 按鈕多執行了一個構造函數。也就是說當點擊“Back” 返回按鍵時,SurfaceView 視圖會被重新加載 。
正因為這個原因,如果線程的初始化是在構造函數或者在構造函數之前,那么線程也要放在視圖構造函數中進行。
千萬不要把線程的初始化放在 surfaceCreated 視圖創建函數之前,而線程的啟動卻放在 surfaceCreated 視圖創建的函數中,否則程序一旦被玩家點擊“Home”按鍵后再重新回到游戲時,程序會拋出異常。

異常是因為線程已經啟動造成的,原因很簡單,因為程序被“Home” 鍵切入后台再從后台恢復時,會直接進入 surfaceCreated 視圖中創建函數,又執行了一遍線程啟動!

能夠想到的解決方法是,可以將線程的初始化和啟動都放在視圖的構造函數中,或者都放在視圖創建的函數中。但是這里又出現新的問題,如果將線程的初始化和啟動都放在視圖的構造函數中,那么當程序被“Back”鍵切入后台再從后台恢復時,線程的數量會增多,反復多次,就會反復多出對應的線程。

那么,如果將flag這個線程標識位在視圖摧毀時讓其值改為false ,從而使當前這個線程的run 方法執行完畢,以達到摧毀線程的目的,但是如果點擊“Home”鍵呢?當程序恢復的時候,程序就不執行線程了,也就是說重繪和邏輯函數都不再執行!
所以最完美的做法是,線程的初始化與線程的啟動都寫在視圖的surfaceCreated 創建函數中,並且將線程標識位在視圖摧毀時將其值改變為 false 。這樣既可避免“線程已啟動”的異常,還可以避免點擊Back 按鍵無限增加線程數的問題。
Introductions

(2)獲取視圖的寬和高

在SurfaceView 視圖中獲取視圖的寬和高的方法:
this.getWidth(); 獲取視圖寬度
this.getHeight(); 獲取視圖高度

在 SurfaceView 視圖中獲取視圖的寬高,一定要在視圖創建之后才可以獲取到,也就是在 surfaceCreated 函數之后獲取,在此函數執行之前獲取到的永遠是零,因為當前視圖還沒有創建,是沒有寬高值的。
Introductions

(3)繪圖函數 try 一下

因為當 SurfaceView 不可編輯或尚未創建時,調用 lockCanvas() 函數會返回null; Canvas 進行繪圖時也會出現不可預知的問題,所以要對繪制函數中進行 try...catch 處理;既然 lockCanvas() 函數有可能獲取為 null, 那么為了避免其他使用 canvas 實例進行繪制的函數報錯,在使用 Canvas 開始繪制時,需要對其進行判定是否為 null
Introductions

(4)提交畫布必須放在 finally 中

繪圖的時候可能會出現不可預知的 Bug, 雖然使用 try 語句包起來了,不會導致程序崩潰;但是一旦在提交畫布之前出錯,那么解鎖提交畫布函數則無法被執行到,這樣會導致下次通過 lockCanvas() 來獲取 Canvas 時程序拋出異常,原因是因為畫布上次沒有解鎖提交!所以畫布將解鎖提交的函數應放入 finally 語句塊中。
還要注意,雖然這樣保證了每次能正常提交解鎖畫布,但是提交解鎖之前要保證畫布不為空的前提,所以還需判斷 Canvas 是否為空,這樣一來就完美了。
Introductions

(5)刷幀時間盡可能保持一致

雖然在線程循環中,設置了休眠時間,但是這樣並不完善!比如在當前項目中, run 的 while 循環中除了調用繪圖函數還一直調用處理游戲邏輯的 logic() 函數,雖然在當前項目的邏輯函數中並沒有寫任何的代碼,但是假設這個邏輯函數 logic()中寫了幾千行的邏輯,那么系統在處理邏輯時,時間的開銷是否和上次的相同,這是無法預料的,但是可以盡可能地讓其時間差值趨於相同。假設游戲線程的休眠時間為X毫秒,一般線程的休眠寫法為:
Thread.sleep(X);

優化寫法步驟如下:

步驟1 首先通過系統函數獲取到一個時間戳:
long start = System.currentTimeMillis();
//在線程中的繪圖、邏輯等函數

步驟2 處理以上所有函數之后,再次通過系統函數獲取到一個時間戳:
long end = System.currentTimeMillis();

步驟3 通過這兩個時間戳的差值,就可以知道這些函數所消耗的時間:如果(end - start) > X, 那線程就完全沒有必要去休眠;如果(end - start) < X, 那線程的休眠時間應該為 X - (end - start) 。
線程休眠應更改為以下寫法:
 if ((end - start) < X) {
 Thread.sleep(X - (end - start));
 }
 一般游戲中刷新時間在50~100毫秒之間,也就是每秒10~20幀左右;當然還要視具體情況和項目而定。
Introductions

 

上面的MySurfaceView 類繼承surfaceview類,並且使用回調callback接口以及線程runnable接口。那么這里簡單的說下Callback接口和SurfaceHolder 類的作用;

callback接口:

只要繼承SurfaceView類並實現SurfaceHolder.Callback接口就可以實現一個自定義的SurfaceView了,SurfaceHolder.Callback在底層的Surface狀態發生變化的時候通知View,SurfaceHolder.Callback具有如下的接口:

surfaceCreated(SurfaceHolder holder):當Surface第一次創建后會立即調用該函數。程序可以在該函數中做些和繪制界面相關的初始化工作,一般情況下都是在另外的線程來繪制界面,所以不要在這個函數中繪制Surface。

surfaceChanged(SurfaceHolder holder, int format, int width,int height):當Surface的狀態(大小和格式)發生變化的時候會調用該函數,在surfaceCreated調用后該函數至少會被調用一次。

SurfaceHolder 類:

它是一個用於控制surface的接口,它提供了控制surface 的大小,格式,上面的像素,即監視其改變的。

SurfaceView的getHolder()函數可以獲取SurfaceHolder對象,Surface 就在SurfaceHolder對象內。雖然Surface保存了當前窗口的像素數據,但是在使用過程中是不直接和Surface打交道的,由SurfaceHolder的Canvas lockCanvas()或則Canvas lockCanvas()函數來獲取Canvas對象,通過在Canvas上繪制內容來修改Surface中的數據。如果Surface不可編輯或則尚未創建調用該函數會返回null,在 unlockCanvas() 和 lockCanvas()中Surface的內容是不緩存的,所以需要完全重繪Surface的內容,為了提高效率只重繪變化的部分則可以調用lockCanvas(Rect rect)函數來指定一個rect區域,這樣該區域外的內容會緩存起來。在調用lockCanvas函數獲取Canvas后,SurfaceView會獲取Surface的一個同步鎖直到調用unlockCanvasAndPost(Canvas canvas)函數才釋放該鎖,這里的同步機制保證在Surface繪制過程中不會被改變(被摧毀、修改)。

本例沒有在該surfaceview的初始化函數中將其 ScreenW 與 ScreenH 進行賦值,這里要特別注意,如果你在初始化調用ScreenW = this.getWidth();和ScreenH = this.getHeight();那么你將得到很失望的值 全部為0;原因是和接口Callback接口機制有關,當繼承callback接口會重寫它的surfaceChanged()、surfaceCreated()、surfaceDestroyed(),這幾個函數當surfaceCreated()被執行的時候,真正的view才被創建,也就是說之前得到的值為0 ,是因為初始化會在surfaceCreated()方法執行以前執行,view沒有的時候我們去取屏幕寬高肯定是0,所以這里要注意這一點;
 
這里把draw的代碼都try起來,主要是為了當畫的內容中一旦拋出異常了,那么也能在finally中執行該操作。這樣當代碼拋出異常的時候不會導致Surface出去不一致的狀態。  

3.View 和 SurfaceView 的區別

1.更新畫布
在 View 視圖中對於畫布的重新繪制,是通過調用 View 提供的 postInvalidate() 與 invalidate() 這兩個函數來執行的,也就是說畫布是由系統主 UI 進行更新。那么當系統主 UI 線程更新畫布時可能會引發一些問題;比如更新畫面的時間一旦過長,就會造成主 UI 線程被繪制函數阻塞,這樣一來則會引發無法響應按鍵、觸屏等消息的問題。
SurfaceView 視圖中對於畫布的重繪是由一個新的單獨線程去執行處理,所以不會出現因主 UI 線程阻塞而導致無法響應按鍵、觸屏信息等問題

2.視圖機制
Android 中的View 視圖是沒有雙緩沖機制的,而 SurfaceView 視圖卻有!也可以簡單理解為, SurfaceView 視圖就是一個由 View 拓展出來的更加適合游戲開發的視圖類。
View 與 SurfaceView 都各有其優點:
比如一款棋牌類游戲,此類型游戲畫面的更新屬於被動更新;因為畫布的重繪主要是依賴與按鍵和觸屏事件(當玩家有了操作之后畫布才需要進行更新),所以此類游戲選擇 View 視圖進行開發比較合適,而且也減少了因使用 SurfaceView 需單獨起一個新的線程來不斷更新畫布所帶來的運行開銷。
但如果是主動更新畫布的游戲類型,比如RPG、飛行射擊等類型的游戲中,很多元素都是動態的,需要不斷重繪元素狀態,這時再使用 View 顯然就不合適了。
所以到底開發游戲使用哪種視圖更加的合適,這完全取決於游戲類型、風格與需求。
總體來說, SurfaceView 更加適合游戲開發,因為它能適應更多的游戲類型。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM