第七章 精靈、幀動畫與碰撞檢測
經過前幾章的學習,大家對使用位元影像、接受使用者控制應該已經有了初步的概念,也可以運用這些知識完成簡單的小遊戲。這一章中,我們會為遊戲中最重要的部分——圖形處理建立一個基本的架構,這還不是遊戲引擎,但是其中很多方法可以為讀者以後建立自己的遊戲引擎提供借鑒。這一章的涉及的內容比較多,既有2D遊戲的基礎理論,又有複雜的代碼。尤其是代碼部分,如果詳細講解,恐怕會佔用很大的篇幅。所以我們只對關鍵的函數進行講解,以方便讀者今後靈活運用這些代碼(所有的原始碼都與本章節內容一同提供下載)。
這個架構是完全依照MIDP中javax.microedition.lcdui.game包設計的:
Classes
GameCanvas
Layer
LayerManager
Sprite
TiledLayer
game包中有5個類,其中Layer(層)是一個抽象類別,對圖形顯示作了基本的定義。以我們的目標遊戲《坦克大戰》為例,在遊戲中有這樣一些圖形元素:我方和敵方的坦克、坦克發出的子彈、地面、牆體、水域掩體等。這些元素雖然外觀不同,但是本質上卻非常相似:都是在特定位置以特定尺寸顯示一個或一組位元影像,有些位元影像位置還會變動,Layer就定義了位置,尺寸,顯示等相關的功能。之所以叫做Layer,與遊戲中分層地圖的概念有關,先讓我們瞭解一下什麼是分層地圖:還是說坦克大戰,當我們的坦克行駛在普通地面上時,坦克的映像肯定是覆蓋了地面的映像,這樣我們能看到坦克。當坦克行駛到掩體時,我們會發現,掩體的映像覆蓋了坦克的映像,:
實際上,我們在程式中,只要首先顯示地面的映像,然後顯示坦克的映像,最後顯示掩體的映像(掩體圖片是鏤空的),就能達到這種效果,這就是分層地圖。通常我們把最下面的叫做地面層,中間的叫做物件層,最上面的叫做天空層。關於地圖我們就講這麼多,這裡只介紹圖形意義上的分層,是為了協助大家理解Layer一詞的意義。關於地圖的詳細內容我們在第十章會深入講解。
首先讓我們看一下抽象類別Layer的定義:
package org.yexing.android.games.common;
import android.graphics.Canvas;
public abstract class Layer {
int x = 0; // Layer的橫座標
int y = 0; // Layer的縱座標
int width = 0; // Layer的寬度
int height = 0; // Layer的高度
boolean visible = true; // Layer是否可見
Layer(int width, int height) {
setWidthImpl(width);
setHeightImpl(height);
}
/**
* 設定Layer的顯示位置
*
* @param x
* 橫座標
* @param y
* 縱座標
*/
public void setPosition(int x, int y) {
this.x = x;
this.y = y;
}
/**
* 相對於當前的位置移動Layer
*
* @param dx
* 橫座標變化量
* @param dy
* 縱座標變化量
*/
public void move(int dx, int dy) {
x += dx;
y += dy;
}
/**
* 取得Layer的橫座標
*
* @return 橫座標值
*/
public final int getX() {
return x;
}
/**
* 取得Layer的縱座標
*
* @return 縱座標值
*/
public final int getY() {
return y;
}
/**
* 取得Layer的寬度
*
* @return 寬度值
*/
public final int getWidth() {
return width;
}
/**
* 取得Layer的高度
*
* @return 高度值
*/
public final int getHeight() {
return height;
}
/**
* 設定Layer是否可見
*
* @param visible
* true Layer可見,false Layer不可見
*/
public void setVisible(boolean visible) {
this.visible = visible;
}
/**
* 檢測Layer是否可見
*
* @return true Layer可見,false Layer不可見
*/
public final boolean isVisible() {
return visible;
}
/**
* 繪製Layer,必須被重載
*
* @param c
*/
public abstract void paint(Canvas c);
/**
* 設定Layer的寬度
*
* @param width
*/
void setWidthImpl(int width) {
if (width
throw new IllegalArgumentException();
}
this.width = width;
}
/**
* 設定Layer的高度
*
* @param height
*/
void setHeightImpl(int height) {
if (height
throw new IllegalArgumentException();
}
this.height = height;
}
}
Layer的代碼不多,根據函數名稱就可以知道它的功能,主要是Layer尺寸、位置的設定和擷取。其中最重要的方法paint是虛方法,Layer映像就是通過這個方法顯示出來的。因此繼承自Layer的所有類都要實現這個方法。
Sprite(精靈)繼承自Layer,同時又增加了幀動畫,圖形變換和碰撞檢測的功能。Sprite是我們這一章重點介紹的內容。首先讓我們瞭解一下精靈的概念。Sprite這個詞在2D遊戲中非常常見,一般指遊戲中具有獨立外觀和屬性的個體元素。如主角、NPC、寶箱、子彈等等,這些都是精靈。
下面就讓我們來建立Sprite類並使其繼承自Layer。建立完畢時,IDE會提示必須實現paint方法。但是這時候我們會發現,paint方法要顯示那些圖形呢?沒有。因此我們需要為Sprite增加一個Bitmap類型變數,用來存放paint要顯示的圖形。同時,我們要建立一個建構函式用來初始化這個Bitmap變數。
public Sprite(Bitmap image) {
super(image.getWidth(), image.getHeight());
initializeFrames(image, image.getWidth(), image.getHeight(), false);
initCollisionRectBounds();
this.setTransformImpl(TRANS_NONE);
}
雖然這個建構函式只有幾行,卻涉及到不少的知識。super不用說了,initializeFrames是做什麼的呢?這就要提到幀動畫的概念了。什麼是幀動畫呢?如,我們看到一組星星的圖片(4張16x16的位元影像)
當我們在同一個位置以一定的時間間隔連續顯示這幾幅圖片的時候就變成了這個樣子
我們看到,星星在發光,這就是幀動畫。即取得一個連續畫面中的幾個主要畫面格,在一定的時間間隔下連續的顯示這些幀從而形成動畫,initializeFrames的功能就是初始化這些主要畫面格。那麼又為什麼要初始化呢?通常情況下,我們為了節省空間的也為了便於管理,會將一組動畫的多個幀儲存在同一個圖片檔案中(如的星星)。這樣一來每次顯示的時候就不能顯示整張圖片,而只能顯示這個圖片的一部分。因此,我們要計算每一幀在整張圖片上的位置以便正確顯示。就讓我們來看看initializeFrames的定義
private void initializeFrames(Bitmap image, int fWidth, int fHeight,
boolean maintainCurFrame)
initializeFrames作了這樣的工作,首先取得一個位元影像,然後根據使用者佈建的單一幀的寬度和高度計算這個位元影像中包括多少幀。如剛剛我們看到的星星的圖片(64x16像素),當單一幀的寬度和高度與圖片相同的時候,就只有一幀。但是當一幀的寬度和高度均為16像素時,整個圖片就可以分為4幀了。這時候,函數會計算每一幀的頂點座標,如第一幀的頂點是(0,0),第二幀的頂點是(16,0),並依次類推。這個函數還可以處理更複雜的情況,如:
函數會將計算好的各個幀的頂點橫縱座標分別儲存在兩個數組中(frameCoordsX[],frameCoordsY[]),下次我們使用幀的序號訪問各個幀時(在paint中)就可以很快找到它的所對應的位元影像地區了。
在這個Sprite的建構函式中,initializeFrames使用了image.getWidth()和image.getHeight()作為一幀的高度和寬度,所以這個精靈註定只能有一幀。Sprite的建構函式還有其他樣式,如
public Sprite(Bitmap image, int frameWidth, int frameHeight)
這時候我們就可以指定幀的寬度和高度,定義具有多個幀的Sprite了。
說完了為幀動畫做初始化工作的initializeFrames,讓我們來看下一個函數initCollisionRectBounds。
private void initCollisionRectBounds() {
collisionRectX = 0;
collisionRectY = 0;
collisionRectWidth = this.width;
collisionRectHeight = this.height;
}
這個函數的代碼不多,名字翻譯過來就是“初始化碰撞矩形邊緣”。由此引出另外一個遊戲中的重要概念——碰撞檢測。在我們的目標遊戲坦克大戰中,碰撞檢測可是少不了的,我們的子彈擊中敵方坦克就是一次碰撞,只有進行了碰撞檢測才能夠觸發這次擊中事件,不然我們就沒法消滅敵人的坦克了。2D遊戲中的碰撞檢測有幾種,最簡單的是矩形碰撞檢測,複雜一些的有多邊形檢測和像素檢測等。這裡我們只介紹一下矩形檢測。
我們取坦克和子彈的矩形外框,當這兩個矩形重疊的時候就認為是碰撞了。函數initCollisionRectBounds的功能就是設定Sprite的矩形外框。同前面初始化幀的原理一樣,我們需要這個函數在多個幀組合成的圖片中確定一幀的大小。
現在我們還剩下建構函式中最後一行代碼了:
this.setTransformImpl(TRANS_NONE);
這行代碼設定了Sprite圖形的變換方式。變換一共有8種,定義如下:
public static final int TRANS_NONE = 0;
public static final int TRANS_ROT90 = 5;
public static final int TRANS_ROT180 = 3;
public static final int TRANS_ROT270 = 6;
public static final int TRANS_MIRROR = 2;
public static final int TRANS_MIRROR_ROT90 = 7;
public static final int TRANS_MIRROR_ROT180 = 1;
public static final int TRANS_MIRROR_ROT270 = 4;
所謂變換,就是對圖形進行旋轉和鏡像等操作,這就相當於增加了圖形資源。
因為要顯示向4個方向行駛的坦克,每個坦克都需要4組圖片。如果我們使用了旋轉變換功能,每個坦克只需要一組圖片就夠了,其他的圖片完全可以由旋轉獲得。需要指出的是,旋轉是圍繞參照點(reference pixel)進行的,如果沒有使用函數setRefPixelPosition設定參照點,預設情況下參照點就是(0,0)。因此如果我們使用參數TRANS_ROT90旋轉的話,圖形應該是這樣
這時候有一點必須要特別提示一下,旋轉之後,getX和getY的傳回值將發生變化。
講到這裡,這一章的理論實在是夠多了,我們必須要總結一下:
首先這一章講的是圖形顯示。我們依照j2me中的games包建立了兩個類Layer和Sprite。重點介紹了Sprite(精靈)類相關的知識,包括楨動畫、碰撞檢測和旋轉變換。下面我們來看一組Sprite的應用執行個體:
第一個例子,在螢幕上顯示一個帶有楨動畫的Sprite。
讓我們拿出前面做過的Tank的原始碼,將Layer.java和Sprite.java添加到原始碼中。
(其中的TiledLayer我們在講解地圖的時候再詳細介紹)
將圖片bore.png拷貝到圖形資來源目錄下
開啟GameView.java,在其中添加一個Sprite類型變數s,並在建構函式中初始化s
//取得系統資源
Resources res = context.getResources();
//擷取位元影像
Bitmap bmpBore = BitmapFactory.decodeResource(res, R.drawable.bore);
//建立一個Sprite對象,使用位元影像bmpBore,楨的寬度和高度都為16像素
s = new Sprite(bmpBore, 16, 16);
//顯示在150,150位置
s.setPosition(150, 150);
//顯示第0楨,第1楨和第3楨
s.setFrameSequence(new int[]{0,1,3});
在GameThread的run函數中顯示這個Sprite
synchronized (surfaceHolder) {
c = surfaceHolder.lockCanvas();
//顯示精靈
s.paint(c);
//顯示下一楨
s.nextFrame();
Thread.sleep(200);
}
運行程式,我們可以看到一個小星星在螢幕上閃爍
第二個例子,Sprite的旋轉變換。
依舊使用上一個程式,因為星星旋轉起來根本沒法分辨,所以這次我們使用系統為程式提供的表徵圖檔案icon.png。
首先我們定義一個Sprite數組
Sprite[] ss = new Sprite[8];
並在建構函式中初始化這個數組
Bitmap bmpIcon = BitmapFactory.decodeResource(res, R.drawable.icon);
//建立Sprite,並設定為不同的旋轉方式
for(int i=0; i
ss[i] = new Sprite(bmpIcon);
ss[i].setTransform(i);
}
然後在run函數中顯示出來
for(int i=0; i
ss[i].setPosition(100, i*40);
ss[i].paint(c);
}
Thread.sleep(200);
運行一下看看效果
最後讓我們來做一個碰撞檢測的例子,這個例子要稍稍複雜一些。先說一下設計思路:
這是一個小遊戲,在一個320x320的地區中,我方坦克在最中間。從上下左右4個方向有敵人坦克向中間進攻。我方坦克可以向四個方向發射子彈,使用方向鍵改變方向,使用空格鍵發射,但同時最多隻能發射兩顆子彈。敵人坦克有三種類型,其中是普通坦克,的速度是普通坦克的一倍,速度與普通坦克相同,但是需要擊中兩次才能被摧毀。
首先我們需要引入幾個圖形檔案
分別為子彈、敵方坦克、爆炸效果和我方坦克。
在GameView中聲明變數
// 背景色
Paint p = new Paint();
// 文字
Paint pntText = new Paint();
Sprite player; // 我方坦克
……
在GameView的建構函式中做一些初始化工作
// 初始化我方坦克
player = new Sprite(BitmapFactory.decodeResource(res,
R.drawable.player1), 16, 16);
player.setFrameSequence(new int[] { 0, 1 });
……
增加按鍵響應事件
public boolean onKeyDown(int keyCode, KeyEvent event)
最後就是在GameThread的run函數中完成遊戲邏輯了。具體內容請看本章的代碼,代碼中有詳細的注釋。
本章樣本程式http://u.115.com/file/f1fd539783