前面一講中,我們介紹了,游戲開發的前期准備與如何創建項目。
Android基於box2d開發彈弓類游戲[一]-------------前期准備&創建項目
在這一講中,我們介紹如何搭建游戲界面,在游戲界面中加入靜態如片,如何移動游戲場景。
呼呼呼!!那么,我們開始吧!
三.創建游戲界面
Android中用於顯示游戲界面的視圖,常用的有View和SurfaceView。SurfaceView是從View基類中派生出來的顯示類SurfaceView和View最本質的區別在於,surfaceView是在一個新起的單獨線程中可以重新繪制畫面而View必須在UI的主線程中更新畫面。
那么在UI的主線程中更新畫面 可能會引發問題,比如你更新畫面的時間過長,那么你的主UI線程會被你正在畫的函數阻塞。那么將無法響應按鍵,觸屏等消息。
當使用surfaceView 由於是在新的線程中更新畫面所以不會阻塞你的UI主線程。但這也帶來了另外一個問題,就是事件同步。比如你觸屏了一下,你需要surfaceView中thread處理,一般就需要有一個event queue的設計來保存touch event,這會稍稍復雜一點,因為涉及到線程同步。所以基於以上,根據游戲特點,一般分成兩類。
1 被動更新畫面的。比如棋類,這種用view就好了。因為畫面的更新是依賴於 onTouch 來更新,可以直接使用 invalidate。 因為這種情況下,這一次Touch和下一次的Touch需要的時間比較長些,不會產生影響。
2 主動更新。比如一個人在一直跑動。這就需要一個單獨的thread不停的重繪人的狀態,避免阻塞main UI thread。所以顯然view不合適,需要surfaceView來控制。
3.Android中的SurfaceView類就是雙緩沖機制。因此,開發游戲時盡量使用SurfaceView而不要使用View,這樣的話效率較高,而且SurfaceView的功能也更加完善。
考慮以上幾點,所以我們選用SurfaceView 來進行游戲開發。
下面創建 基於SurfaceView的游戲界面類MainView.java:
package com.catapultdemo; import android.content.Context; import android.view.SurfaceHolder; import android.view.SurfaceHolder.Callback; import android.view.SurfaceView; public class MainView extends SurfaceView implements Callback,Runnable { public MainView(Context context) { super(context); } @Override public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) { // TODO Auto-generated method stub } @Override public void surfaceCreated(SurfaceHolder arg0) { // TODO Auto-generated method stub } @Override public void surfaceDestroyed(SurfaceHolder arg0) { // TODO Auto-generated method stub } @Override public void run() { // TODO Auto-generated method stub } }
只要繼承SurfaceView類並實現SurfaceHolder.Callback接口和runnable接口就可以實現一個自定義的SurfaceView了。
SurfaceHolder.Callback在底層的Surface狀態發生變化的時候通知View。
Runnable用來實現多線程
游戲界面已經搭建完成,下面要做的就是讓項目啟動之后,顯示我們的游戲界面,也就是MainView。
自定義draw方法,用后之后話界面使用。為什么要實現這個方法,會在第四節進行說明。
public void draw() {}
現在可以運行一下項目,看一下運行結果。
可能與想象的有些差別。因為android項目創建完成之后,默認創建一個layout布局文件,用於初始布局。所以我們可以刪除這個布局文件。
刪除布局文件之后項目主文件MainActivity.java會報錯。因為,默認情況下,向界面輸出剛剛刪除的布局文件,然后布局文件已經被我們刪除了。
package com.catapultdemo; import android.os.Bundle; import android.app.Activity; public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }
此時將setContentView(R.layout.activity_main);改成 setContentView(new MainView(this));
此行代碼的意思是,讓程序運行,向界面輸出我們的游戲場景界面。接下來再一次運行程序。
我們刪除了布局文件后,顯示出了我們的游戲場景,中間的“hello world”也已經消失的,但是與我們的想象的結果還是有些差距。我們接下來需要去除頭部的狀態欄和應用程序的名稱欄。還要試游戲屏幕變成橫屏。
// 隱去電池等圖標和一切修飾部分(狀態欄部分) this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN); // 隱去標題欄(程序的名字) this.requestWindowFeature(Window.FEATURE_NO_TITLE); // 游戲界面橫屏 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
此時屏幕顯示界面已經全部變黑了。此時我們的游戲界面搭建完成了。
四.在游戲場景中加入靜態圖片
現在游戲界面還沒有任何的東西。接下來,我們在游戲場景中加入背景圖片,和一些靜態的物體。由於這些背景和靜態的物體不需要模擬物理場景,所以,之需要在游戲場景中畫出圖片即可。
此前已經創建了繼承自SurfaceView的MainView.java游戲界面類。接下來對方法進行完善和介紹。
實現了CallBack接口,重寫了一下方法。
public void surfaceChanged(SurfaceHolder holder,int format,int width,int height){}
//看其名知其義,在surface的大小發生改變時激發
public void surfaceCreated(SurfaceHolder holder){}
//同上,在創建時激發,一般在這里調用畫圖的線程。
public void surfaceDestroyed(SurfaceHolder holder) {}
//同上,銷毀時激發,一般在這里將畫圖的線程停止、釋放。
實現了Runnable接口,重寫了run方法。下面介紹一下為什么要實現Runnable接口並且要重寫run方法:surfaceView有onDraw方法,但是surfaceView不會自己去調用這個方法,所以我們要自己實現 draw方法,並放在run方法內。Runnable實現線程,run方法就是在開辟的線程中無限的去執行。所以我們自己完成的draw方法也可以不斷的執行。這個就是刷屏。
在類中定義一些必要的變量。
private Resources res; private SurfaceHolder sfh; private Thread th; private Canvas canvas; private Paint paint; public MainView(Context context) { super(context); res = this.getResources(); sfh = this.getHolder(); sfh.addCallback(this); paint = new Paint(); paint.setAntiAlias(true); paint.setColor(Color.RED); this.setKeepScreenOn(true);// 保持屏幕常亮 }
Resources資源變量。可以通過this.getResources()獲取項目中的資源。
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繪制過程中不會被改變(被摧毀、修改)。
Thread:定義線程
Canvas: 定義游戲展示的平台,也就是一個畫布。所有的游戲界面將會在畫布上展示。
Paint: 定義畫筆。 擁有了畫布,我們需要一個畫筆在畫布上進行圖畫。
1. 場景中加入背景
首先要在資源文件中提取圖片文件。
//背景圖片 background_top = BitmapFactory.decodeResource(res, R.drawable.bg); background_bottom = BitmapFactory.decodeResource(res, R.drawable.fg); //兩個松鼠圖片 squirrel_1 = BitmapFactory.decodeResource(res, R.drawable.squirrel_1); squirrel_2 = BitmapFactory.decodeResource(res, R.drawable.squirrel_2); //發射器底座圖片 catapult_base_1 = BitmapFactory.decodeResource(res, R.drawable.catapult_base_1); catapult_base_2=BitmapFactory.decodeResource(res,R.drawable.catapult_base_2); 加載了圖片文件之后,定義一個常量FLOOR_HEIGHT。這個是地面的高度,為了能夠更精確的擺放物體。這個高度就是手機屏幕下邊緣到游戲中模擬的地圖的高度。 private static final float FLOOR_HEIGHT =82f; 接下來還要定義屏幕的高和寬。這個高和寬指的是手機屏幕可見區域的高和寬,並不是游戲場景中的高和寬。請注意。 ScreenW = this.getWidth(); ScreenH = this.getHeight(); 接下來在canvas上畫出這些圖片。 public void surfaceCreated(SurfaceHolder arg0) { ScreenH = this.getHeight(); ScreenW = this.getWidth(); thread_flag = true; th = new Thread(this); // 創建線程 th.start(); //開啟線程 } 此時需要注意。一定要把th.start()開啟線程這段代碼放到surfaceCreated最后,否則會出現啟動自動退出的bug. private void draw() { try { canvas = sfh.lockCanvas(); // 得到一個canvas實例 if (canvas != null) { canvas.drawColor(Color.WHITE);// 刷屏 canvas.drawBitmap(background_top, 0-w/2, 0, paint); canvas.drawBitmap(catapult_base_2,260-w,ScreenH-FLOOR_HEIGHT-catapult_base_2.getHeight()-catapult_base_2.getHeight()/4,paint); canvas.drawBitmap(catapult_base_1,265-w,ScreenH-FLOOR_HEIGHT-catapult_base_1.getHeight()-catapult_base_1.getHeight()/4,paint); canvas.drawBitmap(squirrel_1, 50-w, ScreenH-FLOOR_HEIGHT-squirrel_1.getHeight(), paint); canvas.drawBitmap(squirrel_2, 350-w, ScreenH-FLOOR_HEIGHT-squirrel_2.getHeight(), paint); canvas.drawBitmap(background_bottom, 0-w, ScreenH-background_bottom.getHeight(), paint); } } catch (Exception ex) { } finally { if (canvas != null) sfh.unlockCanvasAndPost(canvas); // 將畫好的畫布提交 } }
此時運行程序。就可以看到我們的游戲場景了。但是現實的都是非物理模擬部分。
此時,是不是有些小小的興奮。。。。但是不要怪我潑涼水。現在我們只是把一些圖片拼湊在了一起。其他的什么都沒有呢。手指滑動屏幕也沒有任何反應。
接下來我們使游戲場景進行移動。
五.移動場景
現在的游戲運行之后,只能顯示一半的場景,接下來實現用手指滑動屏幕移動場景。我們要在MainView.java主類中,復寫View中的onTouchEvent方法。此方法用於檢測觸摸屏事件。
@Override public boolean onTouchEvent(MotionEvent event) { return super.onTouchEvent(event); } 然后分別在onTouchEvent方法中,實現觸屏 按下,抬起,移動事件。 public boolean onTouchEvent(MotionEvent event) { if(event.getAction() == MotionEvent.ACTION_DOWN) {} else if(event.getAction() == MotionEvent.ACTION_UP) {} else if(event.getAction() == MotionEvent.ACTION_MOVE) {} return super.onTouchEvent(event); }
還需要定義兩個變量。position_X是當按下觸摸屏時,觸摸點在當前游戲場景中的x軸位置,move_X是移動觸摸屏時,移動的偏移量。
private float position_X; private float move_X;
當按下觸摸屏時計算當前的場景中的位置。event.getX()是獲取觸摸點的x軸坐標,這個是相對於屏幕的坐標,所以我們還需要加上move_X偏移量,這樣就能獲取當前觸摸點在游戲場景中的位置。
if(event.getAction() == MotionEvent.ACTION_DOWN) { position_X =move_X+ event.getX(); } 接下來就完成當手指移動時,move_X偏移量的值。偏移量應該是按下手指時的位置與移動時當前位置的差。 else if(event.getAction() == MotionEvent.ACTION_MOVE) { move_X = position_X-event.getX(); }
得到了偏移量我之需要在draw方法中,畫游戲界面時對每張圖片的x軸位置進行改變。這樣就能屏幕移動的效果。
例如話背景頭部圖片時,x軸的位置減去偏移量的位置就是移動之后的位置。其他圖片方法一樣。不明白的可以查看第二節,Android游戲坐標系一節。
canvas.drawBitmap(background_top, 0-move_X, 0, paint);
接下來我們可以運行程序查看一下效果。
此時的運行效果可能會很失望,移動觸屏,游戲場景並沒有進行移動。原因是我們只是實現了觸屏方法,但是當前surfaceview不允許對觸屏進行點擊。所以我們需要在MainView構造方法中,添加以下代碼。
setClickable(true);
此時再次運行程序。游戲界面可以進行移動了。
但是經過測試會發現,當移動場景的時候,可能會超出整個游戲場景。如下圖。
這個bug是在有些尷尬啊!!
這是由於我們沒有為偏移量進行限制的原因。
0<Move_X<游戲場景寬度 – 屏幕寬度
游戲場景的寬度也就是背景圖片的寬度。定義一個變量 gameWidth,它的值為背景圖片的寬度。
private float gameWidth; gameWidth = background_top.getWidth(); 在觸屏移動時對move_X進行限制。 else if(event.getAction() == MotionEvent.ACTION_MOVE) { move_X = position_X-event.getX(); move_X = move_X<0?0:(move_X>gameWidth-ScreenW?gameWidth-ScreenW:move_X); }
這樣就能保證屏幕不會超出游戲場景的范圍。
再次運行程序。可以發現,運行正常。
下一章中,我們將要介紹整個游戲的核心部門---》創建游戲世界!