MIDP 2.0裡面包括一個用來簡化編寫二維游戲的API函數。這個API函數是非常簡湊的,只包括javax.microedition.lcdui.game包裡的五個類。這五個類主要提供了兩個重要的功能:
新的GameCanvas類使得在一個游戲循環體內畫一個screen和響應鍵盤輸入成為可能,而不需要調用系統的paint和input線程。
功能強大而復雜的圖層(layer)API函數可以輕松高效地建立復雜的場景。
muTank Example
利用GameCanvas類創建一個游戲循環(game loop)
GameCanvas類是附加了功能的Canvas類,它提供了立即重畫和檢查設備按鍵狀態的方法。這些新的方法把一個游戲的所有函數(功能)封裝在一個循環體內,並由一個單線程進行控制。為什麼這樣做就非常吸引人阿?先讓我們考慮一下你是如何執行一個使用了Canvas類的典型游戲的:
public void MicroTankCanvas
extends Canvas
implements Runnable {
public void run() {
while (true) {
// Update the game state.
repaint();
// Delay one time step.
}
}
public void paint(Graphics g) {
// Painting code goes here.
}
protected void keyPressed(int keyCode) {
// Respond to key presses here.
}
}
這不是一個美麗的畫面 。運行在應用程序線程中的run()方法,每一個時間段都會刷新游戲。典型的任務是刷新小球或飛行物的位置,繪制人物或飛行器動畫。每一次通過循環體,repaint()方法被用來刷新屏幕。系統把按鍵事件傳送給KeyPressed(),它能適當地刷新游戲狀態。
問題是,每樣東西都在不同的線程裡,游戲代碼在以上三種不同方法裡傳遞很容易混淆。當run()方法裡的主動畫循環體調用repaint()方法時,將沒有辦法確切知道系統什麼時候調用paint()方法。當系統調用KeyPressed()時,也沒有辦法知道程序的另一部分正在進行什麼。如果你KeyPressed()中的代碼將要刷新游戲的狀態,而同一時刻paint()方法將表現屏幕,這時屏幕將會持續非常奇怪的狀態。如果表現屏幕所用時間超過一個單時間段,動畫會看起來顛簸不定或是很奇怪。
GameCanvas類允許你避開常用繪畫(painting)和按鍵消息(key-event)機制,所以所有的游戲邏輯都可以被包括在一個單循環中。首先,GameCanvas類允許你用getGraphics()方法直接訪問Graphics對象。對於所返回的Graphics對象的任何表現(rendering)都可以通過屏幕外緩沖區(offscreen buffer)來實現。你可以用flushGraphics()復制緩沖區到屏幕上,直到屏幕被刷新才會返回。這種方式給你提供比調用repaint()方法更完善的控制。Repaint()方法會立即返回值,以至於你的應用程序不能確定系統什麼時候會調用paint()來刷新屏幕。
GameCanvas類也包含一個用來獲得設備按鍵當前狀態的方法,即所謂得polling技術。你可以通過調用GameCanvas類的getKeyStates()方法,馬上確定哪一個按鍵被按下,從而取代了等待系統調用KeyPressed()方法。
下面是一個使用GameCanvas類的典型的游戲循環體:
public void MicroTankCanvas
extends GameCanvas
implements Runnable {
public void run() {
Graphics g = getGraphics();
while (true) {
// Update the game state.
int keyState = getKeyStates();
// Respond to key presses here.
// Painting code goes here.
flushGraphics();
// Delay one time step.
}
}
}
接下來的例子描述了一個基本的游戲循環體。它向你展現了一個旋轉的“X”,你可以用方向鍵在屏幕上移動它。這裡的Run()方法特別的瘦小,這要多虧了GameCanvas。
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
public class SimpleGameCanvas
extends GameCanvas
implements Runnable {
private boolean mTrucking;
private long mFrameDelay;
private int mX, mY;
private int mState;
public SimpleGameCanvas() {
super(true);
mX = getWidth() / 2;
mY = getHeight() / 2;
mState = 0;
mFrameDelay = 20;
}
public void start() {
mTrucking = true;
Thread t = new Thread(this);
t.start();
}
public void stop() { mTrucking = false; }
public void run() {
Graphics g = getGraphics();
while (mTrucking == true) {
tick();
input();
render(g);
try { Thread.sleep(mFrameDelay); }
catch (InterruptedException ie) {}
}
}
private void tick() {
mState = (mState + 1) % 20;
}
private void input() {
int keyStates = getKeyStates();
if ((keyStates & LEFT_PRESSED) != 0)
mX = Math.max(0, mX - 1);
if ((keyStates & RIGHT_PRESSED) != 0)
mX = Math.min(getWidth(), mX + 1);
if ((keyStates & UP_PRESSED) != 0)
mY = Math.max(0, mY - 1);
if ((keyStates & DOWN_PRESSED) != 0)
mY = Math.min(getHeight(), mY + 1);
}
private void render(Graphics g) {
g.setColor(0xffffff);
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(0x0000ff);
g.drawLine(mX, mY, mX - 10 + mState, mY - 10);
g.drawLine(mX, mY, mX + 10, mY - 10 + mState);
g.drawLine(mX, mY, mX + 10 - mState, mY + 10);
g.drawLine(mX, mY, mX - 10, mY + 10 - mState);
flushGraphics();
}
}
本文所舉示例的代碼包括一個使用了這個canvas的MIDlet。你可以嘗試著運行SimpleGameMIDlet這個小程序,看看它是怎樣工作的。你將會看到一個像正在做健身操的海星的東西(或許它正在尋找自己失掉的腿)。
SimpleGameMIDlet Screen Shot
游戲場景就像是洋蔥(有層次)
典型的二維動作游戲常包含一個背景和若干動畫人物。盡管你可以自己來描繪出這種場景,不過Game API函數使你能夠用圖層來建立場景。你可以做一個城市的背景圖層,另外再做一個含有一輛小汽車的圖層。將小汽車圖層放在背景上,你就創造出了一個完整的場景。把小汽車放在一個單獨的圖層中,可以很容易的熟練操控它,而不受背景和其他圖層的影響。
Game API函數使用以下四個類為圖層提供靈活的支持
Layer類是所有圖層類對象的抽象基類。它定義了一個圖層的基本屬性,包括位置,尺寸,和此圖層是否可見。Layer類的每個子類必須定義一個paint()方法,用來把這個圖層表現在一個圖象上,這個圖象將會被描畫到屏幕表面上。兩個確切的子類TiledLayer和Sprite應該能滿足你的二維游戲的需要了。
TiledLayer類用來建立背景圖像。你可以用一個小的源圖像貼的集合來高效的制作大的圖像。
Sprite類是一個動畫層。你提供源幀就可以對整個動畫進行完全的控制。Sprite類也提供鏡像,並可對源幀作90度旋轉。
LayerManager類是一個非常有用的類,用來保存你的場景中的所有圖層的動作軌跡。LayerManager類 paint()方法的一個簡單調用就足以控制所包含的所有圖層。
使用TiledLayer類
盡管包含一些不是顯而易見的微妙不同,TiledLayer類還是很容易理解。這個類的基本思想就是,用一個源圖像提供一組圖像貼片,這些貼片可以組合成一幅大的場景。例如,下面的圖像是64*48像素的。
Source Image
這個圖像被分成了12塊16*16的圖像貼片。TiledLayer類分配給每個圖像貼片編號,左上角的圖片規定為1,以此類推。上面源圖像的各個貼片如下編號:
Tile Numbering
用代碼創建一個TiledLayer類是非常簡單的。你需要確定行數和列數,源圖像以及這個源圖像裡每個貼片的像素大小。下面的代碼片斷告訴你如何裝載圖像和創建TiledLayer類。
Image image = Image.createImage("/board.png");
TiledLayer tiledLayer = new TiledLayer(10, 10, image, 16, 16);
在例子中,新的TiledLayer類有10行,10列。這些來自image的圖像貼片大小是16*16像素。
有趣的部分還是用這些圖像貼片來創建一幕場景。利用setCell()方法可以把一個圖像貼片分配到一個數組元胞裡。你需要提供這個數組元胞所在行列數以及圖像貼片的編號。例如,你可以通過調用setCelll(2,1,5)方法把編號為5的圖像貼片分配到第2行中的第3個數組元胞裡。如果你覺得這些參數看起來不對,請注意,圖像貼片編號是從1開始計數,而行和列的編號是從0開始的。參數缺省情況下,新的TiledLayer類對象中的所有數組元胞的圖像貼片標號為0,這就意味著它們是空的。
下面的代碼片斷向你說明一種使用整數數組來填充TiledLayer類對象。在實際圖像中,TiledLayer類可以從資源文件裡定義,這就使得定義背景時可以有更多的靈活性,並能提供新的背景和級別來增強游戲的可玩性。
private TiledLayer createBoard() {
Image image = null;
try { image = Image.createImage("/board.png"); }
catch (IOException ioe) { return null; }
TiledLayer tiledLayer = new TiledLayer(10, 10, image, 16, 16);
int[] map = {
1, 1, 1, 1, 11, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 9, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 0, 0, 0, 0, 0,
0, 0, 0, 7, 1, 0, 0, 0, 0, 0,
1, 1, 1, 1, 6, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 7, 11, 0,
0, 0, 0, 0, 0, 0, 7, 6, 0, 0,
0, 0, 0, 0, 0, 7, 6, 0, 0, 0
};
for (int i = 0; i < map.length; i++) {
int column = i % 10;
int row = (i - column) / 10;
tiledLayer.setCell(column, row, map[i]);
}
return tiledLayer;
}
為了把這個TiledLayer類對象顯示在屏幕上,你需要調用一個Graphics對象的paint()方法。
TiledLayer類還支持動畫圖像帖子,這樣就使得通過一系列貼片來移動元胞集合很容易了。若想得到更詳細的說明,參看TiledLayer類相關的API文檔。
使用Sprite類實現人物動畫
Game API函數裡提供的另一個具體的Layer類是Sprite類。一方面,Sprite類是TileLayer類的概念化的逆轉.TiledLayer類使用源圖像貼片的調色板來創建一幅大場景,而Sprite類則使用一系列源圖像幀來產生動畫。
你創建一個Sprite類所需要的只是源圖像和每個幀的尺寸。在TiledLayer類裡,源圖像被分為相同大小的圖像貼片;在Sprite類裡,子圖像被稱為幀。在下面的例子裡,源圖像tank.png用來創建幀大小為32*32像素的Sprite類對象。
private MicroTankSprite createTank() {
Image image = null;
try { image = Image.createImage("/tank.png"); }
catch (IOException ioe) { return null; }
return new MicroTankSprite(image, 32, 32);
}
源圖像裡面的每一幀都有一個編號,從0開始,以此累加。(在這裡不要糊塗,記住圖像貼片的編號才是從1開始的)Sprite類有一個幀序列,它決定了幀顯示的順序。一個新Sprite類對象的缺省幀序列簡單地依照可用幀,從0開始累加。
使用Sprite類的nextFrame()方法和prevFrame()方法,可以把幀在幀序列中向前或向後移動。這些方法把幀序列的頭尾連接起來了。例如,如果Sprite類對象已經把位於幀序列末尾的幀顯示出來了,若在調用nextFrame()方法將會顯示幀序列的頭幀。
調用setFrameSequence()方法,可以通過整型數組所指定的序列來確定不同於缺省時的幀序列。
你還可以調用setFrame()方法跳至當前幀序列中的某一幀。你不能跳至特定的幀編號處,只能跳至幀序列的特定點。
利用從Layer類繼承下來的paint()方法時,只有在Sprite類在下一個時間段內被表現的時候,幀的變化才真正實現。
Sprite類還可以變換源幀。可以把幀旋轉90度,或做鏡像變換,或兩者皆有。在Sprite類裡的常數枚舉了這些可能性。Sprite類的當前變換方式可以通過向setTransform()方法傳遞這些常數之一進行設定。下面的例子是當前幀繞垂直中心做鏡像變換,並旋轉90度:
// Sprite sprite = ...
sprite.setTransform(Sprite.TRANS_MIRROR_ROT90);
應用了變換方式,從而使得Sprite類的參考像素並沒有移動。缺省下,Sprite類的參考像素位於Sprite類坐標系裡的(0,0)點處,即左上角。當應用了變換方式,參考像素的位置也變換了。Sprite類的位置被調整了,從而參考像素仍然在原位置上。
你可以通過調用defineReferencePixel()方法來改變參考像素點的位置。對於大多數類型的動畫,你可以把參考像素點定義在sprite的中心上。
最後,Sprite類提供幾個collidesWith()方法來檢測與其他Sprites,ItledLayers,或Images類對象的碰撞。你可以使用檢測矩形(快但粗糙)或者像素級別(慢但精確)來檢測碰撞。這些方法的微妙不同是難以描述的;若詳細資料,可參看API 文檔。
MuTank例子
MuTank例子向你說明TiledLayer,Sprite和LayerManager類的用法。
最重要的類是包含大部分代碼的MicroTankCanvas類和封裝了坦克行為的MicroTankSprite類。
MicroTankSprite類制作了大量的變換方式。它使用了一個只含3幀的源圖像來顯示指向16種方向的坦克。Turn()和forward()兩個公用方法使得坦克很容易控制。
MicroTankCanvas類是GameCanvas類的子類,它在run()方法裡包含一個你應該很熟悉的動畫循環體.Tick()方法用來檢測坦克是否碰到隔板上了。如果碰到了,就調用MicroTankCanvas類的undo()方法使它最近一次的運動倒退。Input()方法簡單地檢測按鍵是否被按下,並同時調整坦克的方向或位置。Render()方法使用一個LayerManager類對象來對繪畫進行處理。LayerManager類包含兩個圖層,坦克圖層和隔板圖層。
從游戲循環體中調用的Debug()方法,用來比較通過游戲循環體所用時間和期望的循環時間(80毫秒),並在屏幕上顯示時間的百分比。它僅僅是用作調試診斷目的的,在游戲被發送給用戶之前將會被刪除。
游戲循環體的計時比前面的SimpleGameCanvas類更加復雜。為了更精確的每80毫秒就重復一次游戲循環體,MicroTankCanvas類對tick(),input()和render()方法所花費時間進行測量。然後停下來花費完80毫秒中的剩余時間,以使得通過每次循環所用的總共時間盡可能的接近於80毫秒。
總結
MIDP 2.0的Game API函數提供了一個簡化二維動作游戲開發的構架。首先,GameCanvas提供了使得游戲循環體緊湊的繪畫和輸入方法。其次,圖層的架構使得創建復雜的場景成為可能。TiledLayer從源圖像簇的調色盤中組合了一個大背景或場景。Sprite適合於動畫人物,並能檢測到在游戲中與其他對象的碰撞。LayerManager把所有的圖層粘合在一起。MuTank例子提供了一組基本的工作代碼,用來說明Game API函數的使用。
譯者附言:翻譯的目的在於開闊視野,培養無線應用程序開發人員的興趣和愛好,從而有利於國內無線互聯網的發展。譯者希望這篇文章會給廣大愛好者與開發者的學習和研發提供幫助。由於譯者專業技術水平和英語水平有限,難免有不當之處,請各位朋友多多指教。歡迎大家加入我們的團體--溝通無限(GotoWireless)!