游戲開發,好大的一個命題哦。無論是游戲的劇情設計,還是游戲的美工制作,那都不是一兩下子能夠完成的事,而編程,只是其中的一小部分。但是,就算是這一小部分,我也不可能把它掌握得很透徹。這篇隨筆,大部分都是些抄書的東西,主要是為了我的博客的完整性而存在,但是我依然會盡力把它寫好,要讓那些精通Java其它領域但是卻沒有機會做J2ME開發的高手們看看圖片解解饞,讓那些想做手機游戲開發的同仁們看看J2ME的Game API究竟為我們提供了哪些支持,讓我們知道寫游戲需要了解哪些概念。
先來了解一下MIDP 2.0的游戲開發包,不用怕,這組API很簡潔,只有區區5個類,它們都位于javax.microedition.lcdui.game包中。它們分別為GameCanvas類、Layer類、LayerManager類、Sprite類和TiledLayer類。下面分別來介紹一下各個類的用途。
1> GameCanvas類 GameCanvas類是Canvas的子類,它代表了游戲的基本界面,簡單一點說,就是所有的游戲畫面都是在GameCanvas上進行繪制的。那么GameCanvas和Canvas相比,究竟提供了些什么更高級的功能呢?主要有兩點:1、實現了雙緩沖功能;2、提供了輪詢鍵盤輸入事件的方法。
雙緩沖大家肯定很熟悉,就是我們繪圖的時候先把圖象繪制到一個緩沖區中,等圖象繪制完成后,再一次性顯示到屏幕上,這樣,就可以有效消除閃爍和畫面撕裂等現象。在GameCanvas中,我們可以通過getGraphics()方法取得緩沖區的Graphics對象實例,可以通過flushGraphics()方法來將緩沖區的內容顯示到屏幕上。
輪詢鍵盤輸入事件有什么用呢?要回答這個問題,我們首先要了解一些基本的游戲框架,在大部分游戲中,都存在一個主循環,這個主循環決定了我們的游戲以每秒鐘多少步的頻率運行,而在每一步中,先查詢有無鍵盤輸入事件,再運行游戲的邏輯運算,最后更新畫面,然后再進入下一步。在Canvas中,鍵盤的輸入事件都是通過回調的方式進行的,也就是說當有按鍵按下的時候,調用keyPressed()方法,這樣的功能肯定不能夠滿足我們這樣的每一步都要查詢鍵盤輸入的要求。通過調用getKeyStates()方法即可輪詢鍵盤。
2> Layer類 這是一個抽象類,我們并不直接使用它,而是使用它的兩個子類——Sprite類和TiledLayer類
3> Sprite類
Sprite是精靈的意思,這是一個游戲開發的專有名詞,在我們游戲中的每一個對象,我們都可以稱為一個精靈。Sprite類提供了畫面的翻轉、旋轉及簡單的碰撞檢測等。要在GameCanvas上繪制一個精靈對象也很簡單,只需要調用Sprite的paint方法,該方法需要一個Graphics類型的參數,我們把緩沖區的Graphics對象實例傳遞給它即可。
4> TiledLayer類
Tile也是一個二維游戲開發的經典詞匯,是磚塊的意思。這個類有什么用呢?主要是用來構建地圖。這個道理一想也很容易明白,我們玩的游戲中,地圖往往比屏幕窗口大很多,難道我們需要做這么大的圖片嗎?當然不是,我們只需要做幾個小圖片,它這些圖片按照一定的順序平鋪,就可以得到一個相當大的地圖。TiledLayer類就為我們提供了這樣的功能。
5> LayerManager類
這個類主要是用來管理Layer,它可以在畫布上分層次的繪制精靈和地圖,這樣,就可以比較方便的解決誰在前、誰在后、誰遮擋誰等問題。
下面來看實例。剛才已經說過,游戲設計是一個很復雜的過程,沒有專業的隊伍是很難搞的。當然,我們也不是沒有另類的搞法,那就是翻版。想一想我們從小時候到現在玩過哪些經典游戲?俄羅斯方塊、超級瑪麗、合金彈頭、雷電、街霸、拳皇等等,還有前兩年非常流行的“是男人系列”,這些游戲各有各的運行平臺,后來又大部分移植到PC平臺,現在,我們何不試試將它們移植到手機上呢?通過前面的介紹,不難看出,這些經典的2D游戲都可以使用上面的幾個類來概括:飛機、子彈、敵人都是Sprite,大海、天空、森林、沙漠都是TileLayer,我們所面臨的難題,就是搜集和制作圖片素材而已。
這里的實例是“是男人就下一百層”的手機翻版,下載地址:
http://www.j2medev.com/Soft/src/game/200610/802.html,作者不詳,反正不是我。下面是運行效果圖:


首先,我們看一看程序的起點,也就是我們的MIDlet類,在這個程序的LRunner類的構在函數中,創建了一個gameCanv對象,如下:

public?LRunner()?
{
???dp=Display.getDisplay(this);
???gc=new?gameCanv();
???dp.setCurrent(gc);
} 然后,在commandAction()函數中,這樣啟動游戲:
public?void?commandAction(Command?c,Displayable?d)

????
{
????????if(c==cmd_exit)

????????
{
????????????try

????????????
{
????????????????destroyApp(false);

????????????}catch(Exception?e)
{}
????????????notifyDestroyed();
????????}
????????if((c==cmd_start)&(!gc.getIsOnGame()))

????????
{
????????????gc.reStart();
????????}
} 于是,游戲的主要邏輯全部交給gameCanv類了,這個類,就是我們前面講到的GameCanvas類的子類,在這個類中,它實現了Runnable接口,并創建一個新的線程,以實現游戲的主循環,如下:
public?void?run()?//主線程

????
{
????????isOnGame=true;
????????while(isOnGame)

????????
{
????????????gameCount++;
????????????gameLevelUp(gameCount);
????????????try

????????????
{
????????????????Thread.sleep(gameSpeed);

????????????}catch(Exception?e1)
{}

????????????addGameObj();
????????????for(int?j=0;j<objList.size();j++)?//游戲人物和臺階的處理

????????????
{
????????????????gfo=(gameFlatObj)objList.elementAt(j);
????????????????gfo.up();
????????????????if(gco==null)

????????????????
{
????????????????????gco=new?gameCharObj(gfo.getX(),gfo.getY());
????????????????}
????????????????if(gfo.collidesWith(gco,false))

????????????????
{
????????????????????isOnFlat=true;
????????????????????gfo.doOnChar(gco);
????????????????}
????????????????else

????????????????
{
????????????????????gfo.resetTime();
????????????????}
????????????????if(gfo.getProperties()>1)

????????????????
{
????????????????????gfo.nextFrame();
????????????????}
????????????????gfo.paint(g);?
????????????}
????????????
????????????if(gfyo!=null)

????????????
{
????????????????if(gfyo.collidesWith(gco,false))

????????????????
{
????????????????????gfyo.doOnChar(gco);
????????????????????gfyo=null;
????????????????}
????????????????else

????????????????
{
????????????????????gfyo.go();
????????????????????gfyo.nextFrame();
????????????????????gfyo.paint(g);
????????????????????if(gfyo.getIsBottom())

????????????????????
{
????????????????????????gfyo=null;
????????????????????}
????????????????}
????????????}
????????????
????????????keyPressed();
????????????if(!isOnFlat)

????????????
{
????????????????gco.go();?
????????????}
????????????isOnFlat=false;
????????????
????????????//***************
????????????if(gameBgCount<0)

????????????
{
????????????????gameBgCount++;
????????????}
????????????else

????????????
{
????????????????gameBgCount=-20;
????????????}????????????
????????????//***************
????????????
????????????gco.nextFrame();?
????????????gco.paint(g);
????????????
????????????//***************
????????????drawMenu(g);
????????????//***************
????????????
????????????g.drawImage(gameTeeth,0,20,0);?
????????????
????????????this.flushGraphics();
????????????chkObjIsTop();
????????????if(gco.isDead())

????????????
{
????????????????setOnGame(false);
????????????}
????????}
??
????????this.flushGraphics();
} 從以上的代碼中可以看出,這是一個典型的主循環,它通過調用addGameObj()來隨機創建物體,而這些物體中,橫板、翻板、傳送帶、釘板等物體作者將之稱為gfo,對應的類為gameFlatObj,而從天而降的圓球稱為gfyo,對應的類為gameFlyObj,而這幾個類,當然是Sprite類的子類了。在這個主循環中,不難看到gfo.paint(g)和this.flushGraphics()這樣的代碼,正好和我們前面所說的GameCanvas類實現了雙緩沖是對應的。
怎樣輪詢鍵盤輸入呢?從上面的代碼可以看出,作者將輪詢鍵盤事件的代碼放到了keyPressed()函數中,我們再來看看這個函數:
private?void?keyPressed()

????
{
????????int?keyState=this.getKeyStates();
????????if((keyState&GameCanvas.LEFT_PRESSED)!=0)

????????
{
????????????if(gco!=null)

????????????
{
????????????????gco.setCharAct(0,isOnFlat);
????????????????gco.left(isOnFlat);
????????????}
????????????return;
????????}
????????if((keyState&GameCanvas.RIGHT_PRESSED)!=0)

????????
{
????????????if(gco!=null)

????????????
{
????????????????gco.setCharAct(1,isOnFlat);
????????????????gco.right(isOnFlat);
????????????}
????????????return;
????????}
????????if(gco!=null)

????????
{
????????????gco.setCharAct(2,isOnFlat);
????????}
} 和我們前面的介紹也是剛好一一對應。
下面我們再來看看Sprite類怎么使用。這里,gfo、gfyo等等精靈我就不講了,只看看gco,這個精靈是我們游戲的主角,也就是那個跑來跑去跳上跳下的那個小人兒。該精靈對應的素材圖片如下:

這個圖片的文件名為char.png,不難看出,第一幀圖象是站立不動時的效果,第2-5幀為向左跑動的效果,第6-9幀為向右跑動的效果,10-13幀為上下跳動的效果。Sprite類有一個構造函數Sprite(Image img,int x, int y),其中第一個參數就是該素材圖片,后面的兩個參數為每一幀圖象的寬和高,至于總共有多少幀圖象,Sprite類會自己計算。
gco精靈所對應的類為gameCharObj,從它的構造函數中,我們可以看出它正是以這個圖片作為參數的,如下:
public?class?gameCharObj?extends?Sprite?


{
???static

????
{
????????try

????????
{
????????????img=Image.createImage("/char.png");
????????}

????????catch(Exception?e)
{}
????}

????public?gameCharObj(int?X?,int?Y)

????
{
????????super(img,16,17);
????????this.defineCollisionRectangle(1,16,15,1);
????????this.x=X;
????????this.y=Y-17;
????????this.setFrameSequence(stand);
????????this.setPosition(this.x,this.y);
????}
} 在Sprite中,我們通過指定不同的幀序列,就可以實現動畫,比如下面定義的四個數組,分別代表了站立時、向左跑時、向右跑時和跳起時的動畫序列:

private?int[]?stand=
{0};

private?int[]?w_left=
{1,1,2,2,3,3,4,4};

private?int[]?w_right=
{5,5,6,6,7,7,8,8};

private?int[]?jump=
{9,9,10,10,11,11,12,12}; 當我們需要改變精靈的動畫序列的時候,只需要調用Sprite.setFrameSequence()函數即可,如下面這段代碼所示:
public?void?setCharAct(int?i,boolean?b)?//改變人物繪圖

????
{
????????switch(i)

????????
{
????????case?0:
????????????if(state!=i)

????????????
{
????????????????this.setFrameSequence(w_left);
????????????????state=i;
????????????}????????????
????????????break;
????????case?1:
????????????if(state!=i)

????????????
{
????????????????this.setFrameSequence(w_right);
????????????????state=i;
????????????}????????????
????????????break;
????????case?2:
????????????if((state!=i)||(face!=b))

????????????
{
????????????????this.setFrameSequence(b?stand:jump);
????????????????state=i;
????????????????face=b;
????????????}????????????
????????????break;
????????}
????} 設置完動畫序列后,只有調用Sprite.nextFrame(),精靈才會真正的動起來,這一個調用是在主線程中完成的,大家回到開頭就不難看到gco.nextFrame()這樣的代碼了。
遺憾的是,這個游戲中沒有使用到TiledLayer和LayerManager這兩個類,所以沒有辦法讓大家看到實例了。當然,要想全面了解游戲開發的各個細節,最好還是找一些專業的書來讀。最后,給大家介紹一個非常好的J2ME開發網站:
www.j2medev.com,這這里,大家可以找到很多文章和資源。