這部分叫做需求分析,聽起來挺嚇人的,其實就是搞清楚我們要做什麼,做成什麼樣,那些不做。下面我引領著大家共同來完成這一步驟。首先,我們要做一個華容道的游戲,華容道的故事這裡不再贅述了,但其中的人物在這裡限定一下,如上面Images類裡的定義,我們這個版本只提供曹操(Caocao)、關羽(Guanyu)、張飛(Zhangfei)、趙雲(Zhaoyun)、黃忠(Huangzhong)、馬超(Machao)和卒(Zu)。我們這裡也限定一下游戲的操作方法:首先要通過方向鍵選擇一個要移動的區域(就是一張圖片),被選擇的區域用黑色方框框住;選好後按Fire鍵(就是確定鍵)將這塊區域選中,被選中的區域用綠色方框框住;然後選擇要移動到的區域,此時用紅色方框框住被選擇的區域;選好要移動到的區域之後按Fire鍵將要移動的區域(圖片)移到要移動到的區域,並去掉綠色和紅色的方框。這裡需要強調的概念有選擇的區域、選中的區域、要移動的區域和要移動到的區域,這四個概念請讀者注意區分,當然也應當把這一部分記入數據字典之中。
為了使文章的重點突出(介紹如何制作一個J2ME的收集游戲),我們這裡限定一些與本主題無關的內容暫不去實現:過關之後的動畫(實現時要用到TimerTask或Thread類,後續的系列文章中我會詳細介紹動畫方面的知識)、關面之間的切換(其實很簡單,當完成任務之後重新再做一邊)、暫停和保存等操作(這部分的內容介紹的資料很多,我也寫不出什麼新的東東來,難免抄襲,故此免掉)。
需求分析基本完成,離下午還有一段時間,馬上動手用ACDSee把從網上找來的BMP文件,調整其大小為271*177(我的這個圖片是兩個部分合在一起,所以比手機實際屏幕大了),另存為PNG格式。半天時間剛剛好,不但搞清楚了要做的東東,還把要用的圖片准備好了。
四、概要設計
概要設計是從需求分析過渡到詳細設計的橋梁和紐帶,這一部分中我們確定項目的實現方法和模塊的劃分。我們決定將整個項目分成五個部分,分別是前面介紹的Images、Draw,還有Map和Displayable1和MIDlet1。Images和Draw類功能簡單、結構固定,因此很多項目我們都使用這兩各類,這裡直接拿來改改就能用了,前面已經介紹過這裡不再贅述。Map類是用來從外部文件讀入地圖,然後保存在一個數組之中,這部分的內容是我們在本階段討論的重點。Displayable1是一個繼承了Canvas類的畫布,它用來處理程序的主要控制邏輯和一部分控制邏輯所需的輔助函數,主要函數應該包括用來繪圖的paint()函數、用來控制操作的keyPressed()函數、用來控制選擇區域的setRange()函數、用來控制選擇要移動到區域的setMoveRange()函數、用來移動選中區域的Move()函數和判斷是否完成任務的win()函數,更具體的分析,我們放到詳細設計中去細化。MIDlet1實際上就是一個控制整個J2ME應用的控制程序,其實也沒有什麼可特別的,它和我們前面介紹的"Hello World"程序大同小異,這裡就不展開來說了,後面會貼出它的全部代碼。
Map類主要應該有一個Grid[][]的二維數組,用來存放華容道的地圖,還應該有一個read_map()函數用來從外部文件讀取地圖內容填充Grid數據結構,再就是要有一個draw_map()函數用來把Grid數據結構中的地圖內容轉換成圖片顯示出來(當然要調用Draw類的paint方法)。說到讀取外部文件,筆者知道有兩種方法:一種是傳統的定義一個InputStream對象,然後用getClass().getResourceAsStream()方法取得輸入流,然後再從輸入流中取得外部文件的內容,例如
InputStream is = getClass().getResourceAsStream("/filename");
if (is != null) {
byte a = (byte) is.read();
}
這裡請注意文件名中的根路徑是相對於便以後的class文件放置的位置,而不是源文件(Java)。第二種方法是使用onnector.openInputStream方法,然後打開的協議是Resource,但是這種方法筆者反復嘗試都沒能調通,報告的錯誤是缺少Resource協議,估計第二種方法用到J2ME的某些擴展類包,此處不再深究。由於以前已經做過一些類似華容道這樣的地圖,這裡直接給出Map類的代碼,後面就不再詳細解釋Map類了,以便於我們可以集中精力處理Displayable1中的邏輯。
Map類的代碼如下:
package huarongroad;
import Java.io.InputStream;
import Javax.microedition.lcdui.*;
public class Map {
//處理游戲的地圖,負責從外部文件加載地圖數據,存放地圖數據,並按照地圖數據繪制地圖
public byte Grid[][];//存放地圖數據
public Map() {//構造函數,負責初始化地圖數據的存儲結構
this.Grid = new byte[Images.HEIGHT][Images.WIDTH];
//用二維數組存放地圖數據,注意第一維是豎直坐標,第二維是水平坐標
}
public int[] read_map(int i) {
<A href="file://從">file://從</A>外部文件加載地圖數據,並存放在存儲結構中,返回值是光標點的位置
//參數是加載地圖文件的等級
int[] a = new int[2];//光標點的位置,0是水平位置,1是豎直位置
try {
InputStream is = getClass().getResourceAsStream(
"/huarongroad/level".concat(String.valueOf(i)));
if (is != null) {
for (int k = 0; k < Images.HEIGHT; k++) {
for (int j = 0; j < Images.WIDTH; j++) {
this.Grid[k][j] = (byte) is.read();
if ( this.Grid[k][j] == Images.CURSOR ) {
//判斷出光標所在位置
a[0] = j;//光標水平位置
a[1] = k;//光標豎直位置
this.Grid[k][j] = Images.BLANK;//將光標位置設成空白背景
}
}
is.read();//讀取回車(13),忽略掉
is.read();//讀取換行(10),忽略掉
}
is.close();
}else {
//讀取文件失敗
a[0] = -1;
a[1] = -1;
}
}catch (Exception ex) {
//打開文件失敗
a[0] = -1;
a[1] = -1;
}
return a;
}
public boolean draw_map(Graphics g) {
//調用Draw類的靜態方法,繪制地圖
try {
for (int i = 0; i < Images.HEIGHT; i++) {
for (int j = 0; j < Images.WIDTH; j++) {
Draw.paint(g, this.Grid[i][j], j, i);//繪制地圖
}
}
return true;
}catch (Exception ex) {
return false;
}
}
}
對於像華容道這樣的小型地圖可以直接用手工來繪制地圖的內容,比如:
fa1c
2232
bd1e
2gg2
gihg
但是,如果遇到像坦克大戰或超級瑪莉那樣的地圖,就必須另外開發一個地圖編輯器了(我會在後續的文章中介紹用vb來開發一個地圖編輯器)。
五、詳細設計
詳細設計是程序開發過程中至關重要的一個環節,好在我們在前面的各個階段中已經搭建好了項目所需的一些工具,現在這個階段中我們只需集中精力設計好Displayable1中的邏輯。(兩天的時間當然不只干這點活,還要把其他幾個類的設計修改一下)
Displayable1這個類負責處理程序的控制邏輯。首先,它需要有表示當前關面的變量level、表示當前光標位置的變量loc、表示要移動區域的變量SelectArea、表示要移動到的區域的變量MoveArea、表示是否已有區域被選中而准備移動的變量Selected和Map類的實例MyMap。然後,我們根據用戶按不同的鍵來處理不同的消息,我們要實現keyPressed()函數,在函數中我們處理按鍵的上下左右和選中(Fire),這裡的處理需要我展開來講一講,後面我很快會把這一部分詳細展開。
接下來,是實現paint()函數,我們打算在這一部分中反復的重畫背景、地圖和選擇區域,這個函數必須處理好區域被選中之後的畫筆顏色的切換,具體講就是在沒有選中任何區域時要用黑色畫筆,當選重要移動的區域時使用綠色畫筆,當選擇要移動到的區域時改用紅色畫筆(當然附加一張流程圖是必不可少的)。
再下面要實現的setRange()函數和setMoveRange()函數,這兩個函數用來設置要移動的區域和要移動到的區域,我的思路就是利用前面在Images類中介紹過的地圖組合標記常量,當移動到地圖組合標記常量時,根據該點地圖中的值做逆向變換找到相應的地圖標記常量,然後設置相應的loc、SelectArea和MoveArea,其中setMoveRange()函數還用到了一個輔助函數isInRange(),isInRange()函數是用來判斷給定的點是否在已選中的要移動的區域之內,如果isInRange()的返回值是假並且該點處的值不是空白就表明要移動到的區域侵犯了其他以被占用的區域。有了setRange()和setMoveRange()函數,Move()函數就水到渠成了,Move()函數將要移動的區域移動到要移動到的區域,在移動過程中分為三步進行:
第一.復制要移動的區域;
第二.將復制出的要移動區域復制到要移動到的區域(這兩步分開進行的目的是防止在復制過程中覆蓋掉要移動的區域);
第三.用isInRange2()判斷給定的點是否在要移動到的區域內,將不在要移動到的區域內的點設置成空白。
下面我們詳細的分析一下keyPressed()函數的實現方法:首先,keyPressed()函數要處理按鍵的上下左右和選中(Fire),在處理時需要用Canvas類的getGameAction函數來將按鍵的鍵值轉換成游戲的方向,這樣可以提高游戲的兼容性(因為不同的J2ME實現,其方向鍵的鍵值不一定是相同的)。
接下來,分別處理四個方向和選中.當按下向上時,先判斷是否已經選定了要移動的區域(即this.selected是否為真),如果沒有選中要移動區域則讓光標向上移動一格,然後調用setRange()函數設置選擇要移動的區域,再調用repaint()函數刷新屏幕,否則如果已經選中了要移動的區域,就讓光標向上移動一格,然後調用setMoveRange()函數判斷是否能夠向上移動已選中的區域,如果能移動就調用repaint()函數刷新屏幕,如果不能移動就讓光標向下退回到原來的位置。
當按下向下時,先判斷是否已經選定了要移動的區域,如果沒有選中要移動的區域則判斷當前所處的區域是否為兩個格高,如果是兩個格高則向下移動兩格,如果是一個格高則向下移動一格,接著再調用setRange()函數設置選擇要移動的區域,而後調用repaint()函數刷新屏幕,否則如果已經選中了要移動的區域,就讓光標向下移動一格,然後調用setMoveRange()函數判斷是否能夠向下移動已選中的區域,如果能移動就調用repaint()函數刷新屏幕,如果不能移動就讓光標向上退回到原來的位置.按下向左時情況完全類似向上的情況,按下向右時情況完全類似向下的情況,因此這裡不再贅述,詳細情況請參見程序的源代碼。
當按下選中鍵時,先判斷是否已經選中了要移動的區域,如果已經選中了要移動的區域就調用Move()函數完成由要移動的區域到要移動到的區域的移動過程,接著調用repaint()函數刷新屏幕,然後將已選擇標記置成false,繼續調用win()函數判斷是否完成了任務,否則如果還沒有選定要移動的區域則再判斷當前選中區域是否為空白,如果不是空白就將選中標記置成true,然後刷新屏幕.這裡介紹一個技巧,在開發程序遇到復雜的邏輯的時候,可以構造一格打印函數來將所關心的數據結構打印出來以利調試,這裡我們就構造一個PrintGrid()函數,這個函數純粹是為了調試之用,效果這得不錯.至此我們完成了編碼前的全部工作。