前言
我的上一篇也是第一篇文章《J2ME-MIDP1.0小游戲入門-五子棋》貼出以後,有好多的朋友發郵件、加QQ、加MSN和我聊有關J2ME的內容,我很開心也很感慨,開心並不是因為自己文章寫得如何如何而有很多人聯系我,而是有這麼多的朋友在學J2ME,我原來以為現在已經沒有人再會聯系文章的作者,看來我錯了;感慨是因為,我知道我的第一篇文章其實很差的,從大家的反應,我能感覺出網上的原創資料的缺乏,官方或者書籍雖然權威且,但總感覺學的時候和實際的情況有點距離,我在看這些資料的時候,也經常會想,實際游戲開發公司裡為怎麼處理、會怎麼寫這個代碼、程序的結構流程會怎麼樣等等。原創文章的最大有點就是它包含了作者的經驗,實際開發中的經驗。介於大家對我的支持並且網上原創文章有限,我非常樂意繼續寫一些自己的經驗和技術,充實我們的中文資源。所以我接著寫了這篇文檔,文檔內容即程序的開發過程都是真實的,不是為寫文章而寫文章,完全是近一段時間裡先完成的程序,現在再把它寫成教學文章。
願大家學J順利,多多交流(MSN:[email protected] QQ:47599318 E-mail:[email protected]
注意
平台:這個游戲我是在 Nokia 的平台上設計的,也就是說使用了 FullCanvas 類以及針對Nokia-60系列(7650)的屏幕設計的,稍作修改就能運行在其他型號的手機上(我已經制作了 SIEmens-C65 ,Nokia-7210 ,以及所有支持標准midp1.0/2.0、屏幕128 x 128的手機的幾個移植版本,有需要可以和我聯系)
代碼:同我的前一篇文章《J2ME-MIDP1.0小游戲入門-五子棋》一樣,代碼列出解釋的形式仿照《J2ME Game Programming》一書,按照程序功能思路給出相關代碼,一個文件的代碼會根據功能在不同的小節給出,文章結束了,代碼也就完整了。這不同於通常書中的代碼以文件為單位一次全部給出,我認為這樣更有助於讓大家了解一個程序從設計到最後完成的思路。
游戲介紹
掃雷這個游戲大家一定再熟悉不過了,但這個雙人掃雷游戲的掃雷新玩法大家可能就沒見過了,其實這就是MSN軟件中的一個網絡聯機小游戲,大家每天在使用MSN,但都很少注意或玩MSN中的游戲吧,可以說我就是把MSN上的聯機雙人掃雷移植到的手機上,一模一樣。如果你現在不方便上網或者沒有人和你聯機看一下這個游戲的界面和玩法,沒關系,我現在就來介紹一下,游戲區(雷區)中一共有 16 x 16 共256個格子,其中有52顆雷,操作除了上下左右鍵(當然電腦上是用鼠標點的)只需要一個挖雷鍵,你要做的是挖出雷,而不是用另一個鍵去標示雷。兩個玩家,一方開始挖雷,如果挖到(點到)雷,則加一分,沒挖到,就和經典的掃雷一樣,顯示這個位置周圍一圈有幾個雷,如果一個雷都沒有,和經典的掃雷一樣,會把和這一格相連的所有周圍沒有雷的格子和再外面一圈格子打開,然後另一位玩家挖雷,誰先挖到半數以上(大於26顆)的雷誰就獲勝,其實很簡單吧,游戲的界面如下:
游戲邏輯設計
數據結構:這個游戲屬於二維棋類游戲,所以我們自然而然想到設計一個 Bomb 類表示每一顆雷位,用一個Bomb類型的二維數組表示整個雷區的所有雷位,每一個雷位(即每一個Bomb實例)包含一個表示該雷位是否是雷的boolean類型的變量,一個表示該雷位是否已被挖的boolean類型的變量,一個表示是否是被玩家一挖的boolean類型的變量(否表示是被玩家二挖),一個表示該雷位周圍八個雷位共有幾顆雷的int類型變量。這樣整個雷區的狀態就完全被描述出來了,然後就根據這個二維表來繪制游戲界面
游戲流程:整個游戲只使用唯一一個 FullCanvas 類,用唯一一個表示狀態的變量來控制整個游戲的狀態,整個游戲也只使用本身的唯一線程
玩家切換:用一個boolean類型的變量來表示當前執行的玩家,用游戲的外框顏色及右下角的旗子顏色來表示出來
接下來就開始詳細介紹並列出游戲程序的各部分代碼,文章的結束,整個游戲也就完成了
應用程序類:MiningMIDlet.Java
首先就是一個MIDlet類。 MiningMIDlet 類繼承自MIDlet類,用於連接設備的應用程序管理器(Application Manager),通過方法startApp,pauseApp,destroyApp來通知游戲的開始,暫停和銷毀結束。源代碼如下:
package com.imy.yinowl.miningscroll;
import javax.microedition.lcdui.Display; import Javax.microedition.midlet.MIDlet;
public class MiningMIDlet extends MIDlet {
MiningCanvas miningCanvas; //定義游戲界面的FullCanvas類MiningCanvas的對象miningCanvas
public MiningMIDlet() { display=Display.getDisplay(this); miningCanvas=new MiningCanvas(this); //生成MiningCanvas類的對象miningCanvas }
protected void startApp(){ MiningMIDlet.display.setCurrent(miningCanvas); //在屏幕上繪出游戲見面miningCanvas }
protected void pauseApp(){ }
protected void destroyApp(boolean arg0){ notifyDestroyed(); }
}
地雷類:Bomb.Java
Bomb 類定義了二維雷區內每一個雷位的所有信息,這樣用一個Bomb類型的二維表就能完全描述整個雷區的狀態。我在Bomb類中,用了三個boolean型變量和一個int型變量來描述一個雷位:boolean型變量isBomb:描述這個位置是否是雷;boolean型變量hasFound:描述這個位置是否已被挖掘;boolean型變量isPlayer1:描述如果這個位置是雷,而且已經被挖掘,那麼是否是玩家一挖掘,如果值是false,表示是被玩家二挖掘,只有在isBomb和hasFound都為true時,這個變量的值才有意義;int型變量bombaround:描述此位置周圍八個位置共有幾顆雷,這個變量除了用來告訴玩家周圍雷數還可以用來打開成片的非雷區域。通過這四個變量配合不同的圖片就可以把整個雷區的不同狀態的圖形界面呈現在玩家面前。Bomb類的源代碼如下:
package com.imy.yinowl.miningscroll;
public class Bomb {
int bombaround; boolean isPlayer1; boolean hasFound; boolean isBomb;
public Bomb(){ bombaround=0; isBomb=false; hasFound=false; isPlayer1=true; }
}
界面邏輯類框架:MiningCanvas.Java
一般游戲會出現很多的界面,例如游戲的LOGO(也稱作閃屏或啟動界面)、開始菜單(主菜單)、設置界面、游戲界面、幫助界面、關於界面、游戲時幫助菜單等等,我的這個游戲不需要設置所以沒有設置界面,所有的這些界面我們都整合在這一繼承自Nokia-API中的FullCanvas類中,我們要盡可能的較少類的數目以減少資源的開銷,具體是如何整合的,我們會再稍候詳細介紹。FullCanvas不支持Command及CommandListener,所以我們不能再使用原先的命令方法;我們會需要通過線程控制啟動界面的停留時間,所以會用到游戲自己的主線程。
整個游戲我們通過一個int型變量gamestate配合switch結構來控制游戲中所處的界面狀態,也就是當前所顯示界面,在switch結構中根據gamestate不同的值,繪制不同的界面,在相應的時間改變gamestate的值,然後repaint,也就改變了接下來玩家所看見的界面,也同樣用這個方法在keyPressed方法中控制玩家按鍵的相應
框架源代碼如下:
package com.imy.yinowl.miningscroll; import java.io.IOException; import java.util.Random; import java.util.Vector; import javax.microedition.lcdui.Alert; import javax.microedition.lcdui.AlertType; import javax.microedition.lcdui.Font; import Javax.microedition.lcdui.Graphics;
import Javax.microedition.lcdui.Image;
import com.nokia.mid.ui.FullCanvas; public class MiningCanvas extends FullCanvas implements Runnable{ MiningMIDlet miningMIDlet; int gamestate; static final int GAMESTATE_SPLASH=0; static final int GAMESTATE_MENU=1; static final int GAMESTATE_GAMEING=2; static final int GAMESTATE_HELP=3; static final int GAMESTATE_SETTING=4; static final int GAMESTATE_ABOUT=5; static final int GAMESTATE_GAMEMENU=6; static final int GAMESTATE_COUNT=7; public MiningCanvas(MiningMIDlet miningMIDlet){ super(); this.miningMIDlet=miningMIDlet; gamestate=0;//游戲加載時默認界面為啟動界面 } protected void paint(Graphics g) { g.setColor(0x00FFFFFF); g.fillRect(0,0,canvasW,canvasH); switch(gamestate){ case GAMESTATE_SPLASH: paintSplashScreen(g);//繪制游戲啟動界面 break; case GAMESTATE_MENU: paintMenuScreen(g);//繪制游戲主菜單 break; case GAMESTATE_HELP: paintHelpScreen(g);//繪制幫助界面 break; case GAMESTATE_GAMEING: paintGameScreen(g);//繪制游戲界面 break; case GAMESTATE_GAMEMENU: paintGameMenuScreen(g);//繪制游戲時菜單界面 break; default: paintMenuScreen(g);//繪制游戲主菜單 break; } public void run() { } protected synchronized void keyPressed(int keyCode) { int action = getGameAction(keyCode); switch(gamestate){ } } }
接下來就開始設計繪制游戲的每一個界面了
啟動畫面
一般啟動畫面是一個靜態的或動態的圖片(靜態居多),顯示了游戲的LOGO和游戲的發行或制作商的名稱,停留時間為三秒,而且不能通過按鍵跳過,如果游戲需要進行一些費時的初始化,可以在這三秒中同時進行。這停留的三秒通過控制本線程來實現。圖片:源代碼如下:
在MiningCanvas.Java中添加如下代碼 Thread thread; Image splashImage; int splashDelayTime; public MiningCanvas(MiningMIDlet miningMIDlet){ ... splashDelayTime=3000; try{ splashImage=Image.createImage("/occo.png"); }catch(IOException e){} thread=new Thread(this); thread.start(); } private void paintSplashScreen(Graphics g){ g.setColor(0x00000000); g.drawImage(splashImage,getWidth()/2,getHeight()/2-5,Graphics.HCENTER|Graphics.VCENTER); } public void run() { try{ Thread.sleep(splashDelayTime); }catch(InterruptedException e){} gamestate=GAMESTATE_MENU;//在啟動動畫停留3秒後,改變游戲狀態變量值,跳轉到主菜單狀態 repaint(); }
游戲主菜單界面
主菜單通過不同大小的兩種字體來顯示選中和未選中的菜單項,用一個int型變量表示當前選中的菜單項,用一個String型的一維數組儲存菜單項的內容,在keyPresseed方法中根據菜單選項變量menuIdx來相應的改變游戲的狀態,從而使游戲跳轉到相應的狀態。需添加的源代碼如下:
在MiningCanvas.Java中添加如下代碼 static final Font lowFont = Font.getFont (Font.FACE_MONOSPACE, Font.STYLE_PLAIN, Font.SIZE_SMALL); static final Font highFont = Font.getFont (Font.FACE_MONOSPACE, Font.STYLE_BOLD, Font.SIZE_MEDIUM); //兩種字體,分別是未選中和選中狀態的字體 static final int lowColor = 0x000000FF;//未選中狀態的顏色
static final int highColor = 0x00FF0000;//選中狀態的顏色
static final int highBGColor = 0x00CCCCCC;//選中狀態的背景顏色 static final int MAIN_NEW_GAME = 0;
static final int MAIN_SETTINGS = 1;
static final int MAIN_HELP = 2;
static final int MAIN_ABOUT = 3;
static final int MAIN_EXIT = 4;
static final int MAIN_MENU_ITEM_COUNT = 5;
static String[] mainMenu = new String[MAIN_MENU_ITEM_COUNT]; static int canvasW;//屏幕寬
static int canvasH;//屏幕高
static int startHeight;//菜單列表的起始高度
static int spacing;//菜單項間寬度
static int menuIdx;//當前選中的菜單項 public MiningCanvas(MiningMIDlet miningMIDlet){ ... menuIdx = 0; canvasW=getWidth();canvasH=getHeight(); spacing = highFont.getHeight()/2; startHeight = (canvasH-(lowFont.getHeight()*mainMenu.length)-(mainMenu.length-1)*spacing)/2; mainMenu[0] = "New Game"; mainMenu[1] = "Settings"; mainMenu[2] = "Help"; mainMenu[3] = "About"; mainMenu[4] = "Exit"; } private void paintMenuScreen(Graphics g){ for(int i=0;i < mainMenu.length;i++){ if(i==menuIdx){ g.setColor(highBGColor); g.fillRect(0,startHeight+i*(lowFont.getHeight()+spacing)-(highFont.getHeight() -lowFont.getHeight())/2,canvasW,highFont.getHeight()); g.setFont(highFont); g.setColor(highColor); g.drawString(mainMenu[i],(canvasW-highFont.stringWidth(mainMenu[i]))/2, startHeight+i*(lowFont.getHeight()+spacing)-(highFont.getHeight()-lowFont.getHeight())/2, Graphics.TOP|Graphics.LEFT); } else { g.setFont(lowFont); g.setColor(lowColor); g.drawString(mainMenu[i],(canvasW - lowFont.stringWidth(mainMenu[i])) / 2, startHeight + i*(lowFont.getHeight() + spacing),Graphics.TOP|Graphics.LEFT); } } } 在keyPressed方法中的switch結構中添加 case GAMESTATE_MENU: { if (getGameAction(keyCode) == FullCanvas.UP && menuIdx - 1 >= 0) { menuIdx--; } else if (getGameAction(keyCode) == FullCanvas.DOWN && menuIdx + 1 < mainMenu.length) { menuIdx++; } else if (getGameAction(keyCode) == FullCanvas.FIRE) { switch(menuIdx) {//選中後,按照選項值改變游戲狀態值,跳轉到相應的狀態 case MAIN_NEW_GAME: gamestate=GAMESTATE_GAMEING; break; case MAIN_SETTINGS: gamestate=GAMESTATE_SETTING; break; case MAIN_HELP: gamestate=GAMESTATE_HELP; break; case MAIN_about: gamestate=GAMESTATE_ABOUT; break; case MAIN_EXIT: miningMIDlet.destroyApp(false); break; } } break; } }