一、概述
這一講我將帶著大家來實現文字和圖片的繪制,然後試著讓文字和圖片在屏幕裡動起來。雖然,離真正的游戲還有一段距離,但是,這些都是游戲的基礎,所以,大家都是需要掌握的。好的,不多說了,一起進入正題吧!
完成這一講的任務,我們需要掌握如下一些概念,然後我會分別進行講解。
層的概念
文字層
貼圖層
View對象:自定義顯示控件
onDraw()方法:執行一系列繪制
Canvas對象:畫布,呈現數據
Paint:畫筆對象
drawText:繪制文字
drawBitmap:繪制貼圖
SurfaceView對象
SurfaceHolder.Callback
Thread:線程讓畫面動起來
二、層的概念
學習photoshop的朋友肯定都知道層的概念,用通俗的話來說,層就是一個透明的玻璃紙。在android游戲中層的概念跟photoshop中也很相似,它可以用來呈現文字、圖片等元素。游戲中一般都會有很多層組成,每個層中會有不同的元素,而且每個層中的元素是獨立可控的。比如:在打飛機游戲中,背景是一層,玩家飛機是一層,敵機也是一層。大家要注意:層是有層次關系的,上面層會覆蓋下面的層。那麼,在打飛機游戲中,背景肯定是最裡面一層,其他任何游戲元素都呈現在背景的上面。
另外有一點要跟大家特別講一下,就是關於圖片素材問題,我們都知道圖片都是正規的矩形,而且有背景,所以在場景中肯定會有顏色塊,看起來很不逼真。但是PNG格式的圖片是可以做成透明背景,這樣就解決這個問題,這也就是為什麼android的圖片素材基本上都是PNG格式的原因。
文字層:顯示文字內容的層
貼圖層:顯示圖片元素的層
但是,常常文字層和貼圖層分的不是很清楚,文字層也可以繪制貼圖,貼圖層也可以繪制文字。
三、View對象
在普通的應用開發中似乎很難直接接觸到View類,但實際上幾乎所有的Android顯示組件都是繼承View類,TextView, EidtView, ImageView等等都是繼承View類。開發中我們常常在XML文件中使用這些組件,但是如果要讓組件具有更多獨特的功能就需要自定義View類來擴展我們的需求了。
在Android游戲當中充當主要的除了控制類外就是顯示類,在J2ME中我們用Display和Canvas來實現這些,而在Android中涉及到顯示的為View類,Android游戲開發中比較重要和復雜的就是顯示和游戲邏輯的處理。那麼,我們首先研究顯示的問題。
首先創建一個游戲主戰場:GameView 類,並繼承View類,結構如下:
[java]
package cn.zkyc.android.game;
import android.content.Context;
import android.view.View;
public class GameView extends View {
public GameView(Context context) {
super(context);
}
}
接下來我們要將上面創建的GameView類顯示到手機屏幕上。需要在入口Activity中進行調用。項目創建的時候我就已經設定了一個主Activity,名稱為:GameStartActivity,代碼結構如下:
[java]
package cn.zkyc.android.game;
import android.app.Activity;
import android.os.Bundle;
public class GameStartActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 默認是加載XML配置文件,顯示的也是XML文件中的視圖組件
//setContentView(R.layout.main);
//顯示自定義的View,只需要將XML文件換成自定義的View對象就即可,如下:
GameView gameView = new GameView(this);
setContentView(gameView);
}
}
運行Application,效果如下:
很遺憾,頁面中除了title什麼也看不到。實際上,我只是測試自定義View是否能夠正確顯示,只要程序沒有bug,就算是成功。(請看代碼中的注釋)
好的,接下來我們就在View裡面展現一些內容,這個時候就要用到View對象中的onDraw方法,在自定義的GameView對象中必須覆蓋父類View中的onDraw方法。接下來,你想展現任何內容都可以在此方法中進行了。假如,我想在屏幕的(100,100)處繪制藍色文字:“飛機大戰”,在屏幕的(100,200)處繪制一個半徑10像素的紅色圓。
[java]
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 畫筆對象,可以控制顏色和文字大小
Paint paint = new Paint();
// 給畫筆設置系統內置的顏色:藍色
paint.setColor(Color.BLUE);
// 文字左上角的坐標為(100,100)
canvas.drawText("飛機大戰",100, 100, paint);
//將畫筆顏色調成紅色
paint.setColor(Color.RED);
// 圓心點坐標為(100,200)
canvas.drawCircle(100, 200, 10, paint);
}
運行效果如下圖:
到目前為止,你已經可以在自定義的GameView中繪制文字和各種圖形了,但游戲中都是大量的圖片素材,對於圖片如何繪制呢?也很簡單,Canvas類也提供了相應的drawBitmap方法。現在,我來繪制屏幕的(100,300)處繪制一個飛機圖片。只需要在ondraw方法中添加如下代碼即可:
[java]
//加載資源圖片圖片
Bitmap heroBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.hero1);
// 在(100,300)處繪制圖片
canvas.drawBitmap(heroBitmap, 100, 300, paint);
運行效果如下圖:
對於代碼中還有Canvas和Paint兩個類沒有細講,我們可以這樣打個比方吧,假如Canvas是一個畫家,那麼Paint就是畫家手中的筆。畫家能夠畫出各種景象(文字,形狀,貼圖等等),就要用到不同的畫筆和不同的顏色。通過畫筆類Paint就可以調整顏色,字體樣式,字體大小等等。
我們發現繪制貼圖canvas.drawBitmap(heroBitmap, 100, 300, paint);也會用到paint對象,但實際上paint起到的作用不大,我們完全可以忽略。
這種寫法也是對的:canvas.drawBitmap(heroBitmap, 100, 300, null);
具體的用法代碼裡面已經有了,我就不再多說了,大家可以親自查詢下Android SDK API。
四、Thread:讓畫面動起來
上面我們已經實現了自定義的View中繪制了文字、形狀和貼圖,但是一切都是靜止的,跟游戲還差的很遠,意義不是很大。那麼,接下來我就帶著大家一起來讓畫面動起來。
實現這個目標,我們需要用到一個在游戲開發中非常重要的機制,就是多線程機制。具體多線程實現方式,不是我們現在討論的問題,如果還不是很明白就需要自己補補線程方面的知識了。
在這裡我們采用GameView類直接實現Runnable接口的方式:
[java]
public class GameView extends View implements Runnable { }
默認必須實現run方法:
[java]
public void run() {
while(true){
// 不斷的調用View中的postInvalidate方法,讓界面重新繪制
this.postInvalidate();
try {
// 暫停0.5秒繼續
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
postInvalidate() :此方法是View類中的方法,功能是觸發調用onDraw方法實現界面重繪。
只要在每次重繪之前對層中對象的位置、形狀、顏色或者透明度進行修改, 而且在一秒鐘之內完成幾十次的重繪,人的眼睛根本無法分辨,所以流暢的動畫效果就產生了。動畫片和電影也是這個原理。
現在我想讓上面場景中的小球每隔0.5秒鐘改變一次透明度和顏色,飛機垂直向上飛行10dp,效果如下:
完整的代碼如下:
[java]
package cn.zkyc.android.game;
import java.util.Random;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.View;
public class GameView extends View implements Runnable{
Random rand = new Random();
private int dx = 0 ; // x軸移動像素
private int dy = 0 ; // y軸移動像素
/**
* 必須要覆蓋View中一個構造方法
* @param context
*/
public GameView(Context context) {
super(context);
//啟動線程
new Thread(this).start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 畫筆對象,可以控制顏色和文字大小
Paint paint = new Paint();
// 給畫筆設置系統內置的顏色:藍色
paint.setColor(Color.BLUE);
// 文字左上角的坐標為(100,100)
canvas.drawText("飛機大戰",100, 100, paint);
//將畫筆顏色調成紅色
//paint.setColor(Color.RED);
//ARGB : A:透明度 、R:紅色、G:綠色、B:藍色。取值范圍都在:0 ~ 255
paint.setARGB(rand.nextInt(255), rand.nextInt(255), rand.nextInt(255), rand.nextInt(255));
// 圓心點坐標為(100,200)
canvas.drawCircle(100, 200, 10, paint);
//加載資源圖片圖片
Bitmap heroBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.hero1);
// 在(100,300)處繪制圖片,dy值控制在y軸上的位置
canvas.drawBitmap(heroBitmap, 100, 300+dy, null);
}
@Override
public void run() {
while(true){
//修改下dy的坐標,方向向上,所以dy不斷減,每次上移10dp
dy -= 10;
// 不斷的調用View中的postInvalidate方法,讓界面重新繪制
this.postInvalidate();
//this.invalidate(); 此方法要求在UI主線程調用
try {
// 暫停0.5秒繼續
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
到此,實際上我們已經完成了我們的任務,但是並沒有考慮到系統運行效率等問題。下面我將帶著大家學習一下高效且更適合做游戲開發的SurfaceView類。
五、SurfaceView對象
Surfaceview類是View類的一個子類,我們來看看API的層級關系:
1、SurfaceView的特點
可以在主線程之外的線程中向屏幕繪圖上。這樣可以避免畫圖任務繁重的時候造成主線程阻塞,從而提高了程序的反應速度
2、實現方式
定義一個游戲場景類繼承SurefaceView ,同事實現SurfaceHolder.Callback接口。因為使用SurfaceView有一個原則,所有的繪圖工作必須在Surface 被創建之後才能開始(Surface這個概念在 圖形編程中常常被提到,基本上我們可以把它當作顯存的一個映射,寫入到Surface 的內容可以被直接復制到顯存從而顯示出來,這使得顯示速度會非常快),而在Surface 被銷毀之前必須結束。所以Callback 中的surfaceCreated 和surfaceDestroyed 就成了繪圖處理代碼的邊界。
3、需要重寫的幾個方法:
//在surface的大小發生改變時激發
1) public void surfaceChanged(SurfaceHolder holder,int format,int width,int height){}
//在創建時激發,一般在這裡調用畫圖的線程。
2) public void surfaceCreated(SurfaceHolder holder){}
//銷毀時激發,一般在這裡將畫圖的線程停止、釋放。
3) public void surfaceDestroyed(SurfaceHolder holder) {}
4、整個代碼過程邏輯:
-->繼承SurfaceView並實現SurfaceHolder.Callback接口
--> SurfaceView.getHolder()獲得SurfaceHolder對象
-->SurfaceHolder.addCallback(callback) 添加回調函數
-->SurfaceHolder.lockCanvas()獲得Canvas對象並鎖定畫布
--> Canvas繪畫
-->SurfaceHolder.unlockCanvasAndPost(Canvas canvas)結束鎖定,並提交改變,將圖形顯示。
5、SurfaceHolder
這裡用到了一個類SurfaceHolder,可以把它當成surface的控制器,用來操縱surface。處理Canvas上的效果和動畫,控制表面,大小,像素等。
幾個需要注意的方法:
// 給SurfaceView當前的持有者一個回調對象。
1) abstract void addCallback(SurfaceHolder.Callback callback);
// 鎖定畫布,一般在鎖定後就可以通過其返回的畫布對象Canvas,在其上面畫圖等操作了。
2) abstract Canvas lockCanvas();
// 鎖定畫布的某個區域進行畫圖等,因為畫完圖後,會調用下面的unlockCanvasAndPost來改變顯示內容。
// 相對部分內存要求比較高的游戲來說,可以不用重畫dirty外的其它區域的像素,可以提高速度。
3) abstract Canvas lockCanvas(Rect dirty);
// 結束鎖定畫圖,並提交改變。
4) abstract void unlockCanvasAndPost(Canvas canvas);
6、我們把上面View的實現功能改為SurfaceView來重新實現
[java]
package cn.zkyc.android.game;
import java.util.Random;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;
import android.view.SurfaceView;
public class GameSFView extends SurfaceView implements Callback, Runnable {
private SurfaceHolder surfaceHolder;
private Random rand = new Random();
private int dx = 0, dy = 0;
public GameSFView(Context context) {
super(context);
surfaceHolder = this.getHolder(); // 獲取SurfaceHolder對象
surfaceHolder.addCallback(this); // 添加回調
}
@Override
public void run() {
while(true){
dy -= 10; //修改下dy的坐標,方向向上,所以dy不斷減,每次上移10dp
draw(); //調用重繪
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
/**
* 自定義繪制方法
*/
public void draw() {
synchronized(surfaceHolder){
// 獲取Canvas對象
Canvas canvas = surfaceHolder.lockCanvas(); // 鎖住Canvas
//清理背景,游戲中將換成具體的背景貼圖
canvas.drawColor(Color.BLACK);
// 畫筆對象,可以控制顏色和文字大小
Paint paint = new Paint();
// 給畫筆設置系統內置的顏色:藍色
paint.setColor(Color.BLUE);
// 文字左上角的坐標為(100,100)
canvas.drawText("飛機大戰", 100, 100, paint);
// 將畫筆顏色調成紅色
// paint.setColor(Color.RED);
// ARGB : A:透明度 、R:紅色、G:綠色、B:藍色。取值范圍都在:0 ~ 255
paint.setARGB(rand.nextInt(255), rand.nextInt(255), rand.nextInt(255),rand.nextInt(255));
// 圓心點坐標為(100,200)
canvas.drawCircle(100, 200, 10, paint);
// 加載資源圖片圖片
Bitmap heroBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.hero1);
// 在(100,300)處繪制圖片,dy值控制在y軸上的位置
canvas.drawBitmap(heroBitmap, 100, 300 + dy, null);
surfaceHolder.unlockCanvasAndPost(canvas); // 解鎖Canvas,更新
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int arg1, int arg2,int arg3) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
// Surface創建成功啟動線程
new Thread(this).start();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
}
大家發現,飛機移動到頂部之後就不見了,請大家思考,如何讓飛機飛過頂部之後還能從底部出來呢?