J2ME-MIDP1.0游戏完整实现-双人扫雷1.0(二)

80酷酷网    80kuku.com

  作者:yinowl
2005年2月

帮助界面
帮助界面很简单,把需要的文字排好宽度放在一个String数组里,然后绘制在屏幕上,如果一屏放不下就增加按键响应来翻屏,其实只是重新画出数组前面或后面的几个值,源代码如下:

在MiningCanvas.java中添加如下代码
final String[] strGamehelp =new String[10];
public MiningCanvas(MiningMIDlet miningMIDlet){
...
strGamehelp[0]="此游戏为双人对战游戏";
strGamehelp[1]=",和经典的扫雷不同,这";
strGamehelp[2]="个游戏中,我们要挖出";
strGamehelp[3]="雷,挖错雷则交换玩家.";
strGamehelp[4]="一共有52颗雷,256个格";
strGamehelp[5]="子,最先挖到27颗雷者";
strGamehelp[6]="获胜.1.上,下,左,右分";
strGamehelp[7]="别为数字键2,8,4,6,挖";
strGamehelp[8]="雷为5;2.屏幕外框的颜";
strGamehelp[9]="色为当前下玩家;";
}
private void paintHelpScreen(Graphics g){
g.setColor(0x00FFFFFF);
g.fillRect(0,0,canvasW,canvasH);
g.setFont(lowFont);
g.setColor(0x00000000);
for(int i=0;i<strGamehelp.length;i++){
g.drawString(strGamehelp[i],5,5+(lowFont.getHeight()+3)*i,Graphics.TOP|Graphics.LEFT);
}
}
在keyPressed方法中的switch结构中添加
case GAMESTATE_HELP://如果游戏现在的状态为帮助状态,那个不管玩家按哪个键都会跳转到主菜单状态
gamestate=GAMESTATE_MENU;
break;


游戏主界面
主界面的绘制分成几个部分,一个是雷区棋盘的绘制,根据雷区二位数组中的每一个Bomb对象中的变量值绘出整个棋盘,如果hasFound值为false,画出;如果hasFound值为true且isBomb值为false,那么会画出这个雷位周围的雷数、、、、、、;如果hasFound值为true且isBomb值为true,那么会画出这个雷位是哪一个玩家所挖出的,分别是和。第二个部分是玩家的选择框。第三个部分是游戏的信息框,也就是两个玩家目前的分值,还剩几颗雷,信息框的图片为。最后一个部分是提示当前轮到哪一个玩家扫雷,在雷区的外框用玩家的颜色提示,和在信息框中画出玩家的旗帜和。
这里有几个注意点:
1.雷区不能全部在屏幕中画下,所以会通过卷轴滚动。整个雷区是16x16,我们的屏幕预备画出10x12,用两个变量(paintX和paintY)来控制当前要画出的雷区左上角的雷位坐标,例如这两个变量的值分别为3和4,那么会画出横轴3-10,纵轴4-13的雷位,选择框的坐标(selectedX和selectedY)不用担心,当他超出了屏幕,只需要增加或减少paintX和paintY的值即可,只要保证选择框的坐标和paintX/Y坐标值得距离不超过9(10-1)和11(12-1)即可。这里要十分小心的是,我们这里所有的坐标值和雷区的二位数组bombs中的坐标正好相反,也就是选择框(5,6)是类位bombs[6][5]。
2.在创建雷区二维Bomb数组时,二位数组的大小是18x18(new Bomb[miningMapGrid+2][miningMapGrid+2]),目的是在大小16x16的真正雷区外加一圈雷位,以避免bombOut()方法和其他需要周边搜索雷位时不会出现超越边界异常。
3.因为bombs数组中的索引1-16才是真正代表雷区中坐标(1,1)至(16,16)的雷位,所以在整个程序中,所有有关坐标和数组索引值的地方都要十分小心,不要出现位置错误
4.消息框中需要绘制的几个数值的坐标变量(Player1X/Y,Player2X/Y,bombnowX/Y)的值都是相对于整个消息框的左上角的相对坐标,我们也可以为消息框的坐标单独定义一对变量,这样就更清晰了。
源代码如下:

在MiningCanvas.java中添加如下代码
int empty;
static final int miningMapGrid=16;//雷区为16格x16格
static final int bombNum=52;//共有52颗雷
int miningMapLength,miningGridLength;//整个雷区的边长,每一个雷位格子的边长
int miningMapX,miningMapY;//雷区的左上角坐标
int selectedX,selectedY;//选择框的坐标,是在雷区里的坐标(1-16)
int player1Found,player2Found;//两个玩家的分值,即已经找到的雷数
int paintX,paintY;//要画出的区域的左上角坐标,是在雷区里的坐标(1-16)
int bombLeft;//剩下未扫出的雷数
static final int Player1X=10,Player1Y=30;//信息框中画出玩家一分数的坐标位置,相对于信息框左上角
static final int Player2X=10,Player2Y=90;//信息框中画出玩家二分数的坐标位置,相对于信息框左上角
static final int bombnowX=8,bombnowY=52;//信息框中画出剩余雷数的坐标位置,相对于信息框左上角
static final int bombMapW=10,bombMapH=12;//屏幕上能绘出的雷位的数量
Bomb[][] bombs;
boolean isPlayer1;//当前是否轮到玩家一扫雷
Alert winAlert;//显示输赢信息的Alert对象
String winString;//比出输赢后要输出的信息
boolean sbWon;//是否得出输赢
static final Font font = Font.getFont(Font.FACE_MONOSPACE,Font.STYLE_PLAIN,Font.SIZE_SMALL);
//消息框中使用的字体
Image infoImg,splashImage;
Image unFoundGroundImg;
Image foundGroundImg;
Image player1BombImg,player2BombImg,player1TurnImg,player2TurnImg;
Image bomb1Img,bomb2Img,bomb3Img,bomb4Img,bomb5Img,bomb6Img,bomb7Img,bomb8Img;
//所有要用到的图片
public MiningCanvas(MiningMIDlet miningMIDlet){
...
try{//实例化游戏中需要的Image对象
unFoundGroundImg=Image.createImage("/unfoundGroundbig.png");
foundGroundImg=Image.createImage("/foundGroundbig.png");
player1BombImg=Image.createImage("/player1bombbig.png");
player2BombImg=Image.createImage("/player2bombbig.png");
bomb1Img=Image.createImage("/bomb1big.png");
bomb2Img=Image.createImage("/bomb2big.png");
bomb3Img=Image.createImage("/bomb3big.png");
bomb4Img=Image.createImage("/bomb4big.png");
bomb5Img=Image.createImage("/bomb5big.png");
bomb6Img=Image.createImage("/bomb6big.png");
infoImg=Image.createImage("/info.png");
splashImage=Image.createImage("/occo.png");
player1TurnImg=Image.createImage("/player1turn.png");
player2TurnImg=Image.createImage("/player2turn.png");
}catch(IOException e){}
isPlayer1=true;//初始化玩家先后顺序
miningGridLength=14;//初始化每一个雷位的边长
miningMapLength=14*12;//整个扫雷棋盘的边长
miningMapX=(canvasW-miningMapLength)/2;//屏幕上棋盘左上角的X坐标
miningMapY=(canvasH-miningMapLength)/2;//屏幕上棋盘左上角的Y坐标
selectedX=selectedY=miningMapGrid/2;//初始化选择框的坐标为
player1Found=player2Found=0;//初始化两个玩家的得分
paintX=paintY=3;//初始化整个雷区一开始在屏幕上显示的范围
sbWon=false;//初始化没有玩家获胜
bombLeft=bombNum;//初始化剩余雷数为总雷数
bombs=new Bomb[miningMapGrid+2][miningMapGrid+2];
bombInit();//初始化雷区
}
private void paintGameScreen(Graphics g){
paintPlayer(g,isPlayer1);
paintMiningMap(g);
paintInfo(g);
paintSelected(g);
}
protected void paintInfo(Graphics g){
g.drawImage(infoImg,miningMapX+bombMapW*miningGridLength+1,
miningMapY,Graphics.TOP|Graphics.LEFT);
g.setFont(font);
g.setColor(0x00FFFFFF);
g.drawString(String.valueOf(bombLeft),miningMapX+bombMapW*miningGridLength+bombnowX,
miningMapY+bombnowY,Graphics.TOP|Graphics.LEFT);
g.drawString(String.valueOf(player1Found),miningMapX+bombMapW*miningGridLength+Player1X,
miningMapY+Player1Y,Graphics.TOP|Graphics.LEFT);
g.drawString(String.valueOf(player2Found),miningMapX+bombMapW*miningGridLength+Player2X,
miningMapY+Player2Y,Graphics.TOP|Graphics.LEFT);
//这个方法中接下来的代码是用来在信息框中画出小地图,也就是屏幕上显示的地雷区域在整个雷区
中的位置
g.setColor(0x00777777);
g.fillRect(miningMapX+bombMapW*miningGridLength+1,miningMapY+8*miningGridLength+1,
2*miningGridLength-2,2*miningGridLength-1);
g.setColor(0x00000000);
g.drawRect(miningMapX+bombMapW*miningGridLength+1,miningMapY+8*miningGridLength+1,
2*miningGridLength-2,2*miningGridLength-1);
g.setColor(0x00BBBBBB);
g.fillRect(miningMapX+bombMapW*miningGridLength+4+2*(paintX-1),
miningMapY+8*miningGridLength+4+2*(paintY-1),12,17);
g.setColor(0x00FFFFFF);
g.drawRect(miningMapX+bombMapW*miningGridLength+4+2*(paintX-1),
miningMapY+8*miningGridLength+4+2*(paintY-1),12,17);
}
protected void paintPlayer(Graphics g,boolean isPlayer1){
if(isPlayer1)//在棋盘外围画出玩家颜色的外框
g.setColor(0x000000FF);
else
g.setColor(0x00FF0000);
for(int i=1;i<=5;i++){
g.drawRect(miningMapX-i,miningMapY-i,miningMapLength+2*i,miningMapLength+2*i);
}
if(isPlayer1)//在信息框中画出代表玩家的旗帜
g.drawImage(player1TurnImg,miningMapX+(bombMapW+1)*miningGridLength+1,
miningMapY+11*miningGridLength,Graphics.HCENTER|Graphics.VCENTER);
else
g.drawImage(player2TurnImg,miningMapX+(bombMapW+1)*miningGridLength+1,
miningMapY+11*miningGridLength,Graphics.HCENTER|Graphics.VCENTER);
}
public void paintMiningMap(Graphics g){
for(int i=0;i<bombMapH;i++){
for(int j=0;j<bombMapW;j++){//根据每个Bomb对象中的变量值画出不同的图片
if(!bombs[i+paintY+1][j+paintX+1].hasFound){
g.drawImage(unFoundGroundImg,miningMapX+j*miningGridLength,
miningMapY+i*miningGridLength,Graphics.TOP|Graphics.LEFT);
}
else {
if(!bombs[i+paintY+1][j+paintX+1].isBomb){
switch(bombs[i+paintY+1][j+paintX+1].bombaround){
case 0:
g.drawImage(foundGroundImg,miningMapX+j*miningGridLength,
miningMapY+i*miningGridLength,Graphics.TOP|Graphics.LEFT);
break;
case 1:
g.drawImage(bomb1Img,miningMapX+j*miningGridLength,
miningMapY+i*miningGridLength,Graphics.TOP|Graphics.LEFT);
break;
case 2:
g.drawImage(bomb2Img,miningMapX+j*miningGridLength,
miningMapY+i*miningGridLength,Graphics.TOP|Graphics.LEFT);
break;
case 3:
g.drawImage(bomb3Img,miningMapX+j*miningGridLength,
miningMapY+i*miningGridLength,Graphics.TOP|Graphics.LEFT);
break;
case 4:
g.drawImage(bomb4Img,miningMapX+j*miningGridLength,
miningMapY+i*miningGridLength,Graphics.TOP|Graphics.LEFT);
break;
case 5:
g.drawImage(bomb5Img,miningMapX+j*miningGridLength,
miningMapY+i*miningGridLength,Graphics.TOP|Graphics.LEFT);
break;
case 6:
g.drawImage(bomb6Img,miningMapX+j*miningGridLength,
miningMapY+i*miningGridLength,Graphics.TOP|Graphics.LEFT);
break;
default:
g.drawImage(foundGroundImg,miningMapX+j*miningGridLength,
miningMapY+i*miningGridLength,Graphics.TOP|Graphics.LEFT);
break;
}
}
else {
if(bombs[i+paintY+1][j+paintX+1].isPlayer1){
g.drawImage(player1BombImg,miningMapX+j*miningGridLength,
miningMapY+i*miningGridLength,Graphics.TOP|Graphics.LEFT);
}
else{
g.drawImage(player2BombImg,miningMapX+j*miningGridLength,
miningMapY+i*miningGridLength,Graphics.TOP|Graphics.LEFT);
}
}
}
}
}
}
public void paintSelected(Graphics g){
g.setColor(0x00FF0000);//画出选择框,注意其与总雷区和能在屏幕上显示出的区域间的关系
g.drawRoundRect(miningMapX+(selectedX-paintX)*miningGridLength-1,
miningMapY+(selectedY-paintY)*miningGridLength-1,
miningGridLength+1,miningGridLength+1,2,2);
}
在keyPressed方法中的switch结构中添加
case GAMESTATE_GAMEING:
{
if(keyCode==FullCanvas.KEY_SOFTKEY1){
gamestate=GAMESTATE_GAMEMENU;
}
else if (action == FullCanvas.LEFT ) {
selectedX=(--selectedX+miningMapGrid)%(miningMapGrid);
}
else if (action == FullCanvas.RIGHT) {
selectedX=(++selectedX)%(miningMapGrid);
}
else if (action == FullCanvas.UP) {
selectedY=(--selectedY+miningMapGrid)%(miningMapGrid);
}
else if (action == FullCanvas.DOWN) {
selectedY=(++selectedY)%(miningMapGrid);
}
else if (action == FullCanvas.FIRE) {
if(!bombs[selectedY+1][selectedX+1].hasFound){
if(bombs[selectedY+1][selectedX+1].isBomb){
bombs[selectedY+1][selectedX+1].hasFound=true;
bombs[selectedY+1][selectedX+1].isPlayer1=this.isPlayer1;
if(isPlayer1)
player1Found++;
else
player2Found++;
bombLeft--;
checkWin();//每次有玩家挖到了雷就监测其是否胜出
}
else{
bombOut(selectedY+1,selectedX+1);
//如果此雷位及周围都无雷,打开所有与其相连的相同情况的雷位及此区域周边一圈雷位
isPlayer1=!isPlayer1;
}
}
}
//以下几行代码是调整显示在屏幕上的区域的坐标,以免选择框跑出屏幕范围
if((selectedX-paintX)<0)
paintX=selectedX;
else if((selectedX-paintX)>=bombMapW)
paintX=selectedX-bombMapW+1;
if((selectedY-paintY)<0)
paintY=selectedY;
else if((selectedY-paintY)>=bombMapH)
paintY=selectedY-bombMapH+1;
break;
}


这里会使用到几个方法
checkWin()方法:我们把这个方法的调用放在玩家挖到雷的时候,每当有玩家挖到雷的时候,会改变player1Found和player2Found的值,这时候检测游戏的输赢,当有玩家的分数大于半数的总雷数的时候,胜负就得出了,这时候用Alert对象显示出比分和胜负信息,同时要为下一局比赛作准备---游戏初始化(gameInit())。
gameInit()方法:这个方法用来初始化游戏,程序中在两种情况需要调用初始化游戏,一个是重新开始游戏时,另一个是在一局结束后准备另一局的开始。每一次需要初始化游戏时,需要将所有的变量初始化
bombInit()方法:初始化表示整个雷区的Bomb型的二维表,病随机的安排52颗雷的位置
bombOut()方法:经典扫雷中,如果我们点到一雷位,本身不是雷,周围也没有雷,电脑会自己打开周围所有相连的类似的雷和这块区域最外圈的一圈雷位,这个方法就是这个作用。我在这里用了递归,当然我们只需要监测fasFound值为false的雷位,要不然就会都搜一遍

在MiningCanvas.java中添加如下代码
public void gameInit(){
sbWon=false;
bombInit();
player1Found=0;player2Found=0;
selectedX=miningMapGrid/4;selectedY=miningMapGrid/4;
bombLeft=bombNum;
System.gc();//手动进行垃圾回收
}
public void bombInit(){
int bombindex=0;
int x=0,y=0;
Random random=new Random();
for(int i=0;i<miningMapGrid+2;i++){
for(int j=0;j<miningMapGrid+2;j++){
bombs[i][j]=new Bomb();
}
}
while(bombindex<bombNum){
x=Math.abs(random.nextInt()%16+1);
y=Math.abs(random.nextInt()%16+1);
if(!bombs[x][y].isBomb && x<=16 && x>=1 && y<=16 && y>=1){
try{
bombs[x][y].isBomb=true;
bombindex++;
bombs[x-1][y].bombaround++;
bombs[x-1][y-1].bombaround++;
bombs[x][y-1].bombaround++;
bombs[x+1][y-1].bombaround++;
bombs[x+1][y].bombaround++;
bombs[x+1][y+1].bombaround++;
bombs[x][y+1].bombaround++;
bombs[x-1][y+1].bombaround++;
}catch(ArrayIndexOutOfBoundsException e){}
}
}
}
public void bombOut(int x,int y){
if(bombs[x][y].hasFound==false && x>=1 && y>=1 &&
x<=MiningCanvas.miningMapGrid && y<=MiningCanvas.miningMapGrid){
bombs[x][y].hasFound=true;
if(bombs[x][y].bombaround==0){
this.bombOut(x-1,y);
this.bombOut(x-1,y-1);
this.bombOut(x,y-1);
this.bombOut(x+1,y-1);
this.bombOut(x+1,y);
this.bombOut(x+1,y+1);
this.bombOut(x,y+1);
this.bombOut(x-1,y+1);
}
}
}
public void checkWin(){
if(player1Found>26){
winString="Player1 has won!";
sbWon=true;
}
if(player2Found>26){
winString="Player2 has won!";
sbWon=true;
}
if(sbWon){
winAlert=new Alert("",winString+"\nPlayer1 "+player1Found
+" : "+player2Found+" player2",null,AlertType.INFO);
gameInit();
winAlert.setTimeout(Alert.FOREVER);
MiningMIDlet.display.setCurrent(winAlert);
}
}



游戏时菜单
在游戏进行时,我们需要有几个功能:重新开始游戏,打开帮助,返回主菜单,退出游戏,当然,进入了菜单就还要能返回正在进行的游戏。这个游戏时菜单和主菜单非常相似,我就不详细解释了,只是这里有一个缺陷,游戏时菜单中选择帮助后,在帮助界面中选择返回,会回到主菜单,而不会回到这个游戏时菜单,当然,加一个变量就可以解决这个问题,原代码如下:

在MiningCanvas.java中添加如下代码
static final int GAME_RETURN = 0;
static final int GAME_RESTART = 1;
static final int GAME_HELP = 2;
static final int GAME_MENU = 3;
static final int GAME_EXIT = 4;
static final int GAME_MENU_ITEM_COUNT = 5;
static String[] gameMenu = new String[GAME_MENU_ITEM_COUNT];
static int gamemenuIdx;
public MiningCanvas(MiningMIDlet miningMIDlet){
...
gamemenuIdx=0;
gameMenu[0] = "RETURN";
gameMenu[1] = "RESTART";
gameMenu[2] = "HELP";
gameMenu[3] = "MENU";
gameMenu[4] = "EXIT";
}
private void paintGameMenuScreen(Graphics g){
for(int i=0;i<gameMenu.length;i++){
if(i==gamemenuIdx){
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(gameMenu[i],(canvasW-highFont.stringWidth(gameMenu[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(gameMenu[i],(canvasW-lowFont.stringWidth(gameMenu[i]))/2,
startHeight+i*(lowFont.getHeight()+spacing),Graphics.TOP|Graphics.LEFT);
}
}
}
在keyPressed方法中的switch结构中添加
case GAMESTATE_GAMEMENU:
{
if (getGameAction(keyCode) == FullCanvas.UP && gamemenuIdx - 1 >= 0) {
gamemenuIdx--;
}
else if (getGameAction(keyCode) == FullCanvas.DOWN && gamemenuIdx + 1 < gameMenu.length) {
gamemenuIdx++;
}
else if (getGameAction(keyCode) == FullCanvas.FIRE) {
switch(gamemenuIdx) {
case GAME_RETURN:
gamestate=GAMESTATE_GAMEING;
break;
case GAME_RESTART:
gameInit();
gamestate=GAMESTATE_GAMEING;
break;
case GAME_HELP:
gamestate=GAMESTATE_HELP;
break;
case GAME_MENU:
gamestate=GAMESTATE_MENU;
break;
case GAME_EXIT:
miningMIDlet.destroyApp(false);
break;
}
}
break;
}


最终代码补全
到这里,按照游戏的功能部件,主要的代码基本上都给出了,在稍微补充一些代码就是整个游戏的完整程序了,需要补充如下:

修改keyPressed方法中的switch结构
switch(gamestate){

...

default:
gamestate=GAMESTATE_MENU;
}
repaint();


总结
整个游戏已经全部完成,大家一定会觉得很简单吧,但已经是一个完整的游戏了,希望对一些朋友有所帮助。当然我们完全可以进行一些扩展,比如加上声音,加上蓝牙对战功能,这样游戏就慢慢的完善,并且具有商业价值。
文章慢慢写到这里也差不多该结束,突然间发现整篇文章完全偏离了我原先预计的方式和效果,我原先打算从游戏的设想,到设计,到编码,到测试除错整个过程写成文章,最重要的是脑子里的想法,包括刚开始错误的、不完善的和后来的修改,这样才能把我做游戏真正的整个过程和经验告诉大家。没想到写到最后还是没有哪个效果,但愿这只是写文章的经验问题,下次一定尽量改善,请大家多谅解。
文章写完了不免会有一些错误,而且每次一写完,总是迫不及待的想贴上来请大家指点,尽管每次都克制自己告诉自己在检查一下看看有没有错误,偶尔也会小偷懒一下,呵呵。有由于《J2ME-MIDP1.0游戏完整实现-双人扫雷(一)》几天前就贴上来了,看一个游戏的代码总不能一半一半看吧,就赶着把这一半写出来了,原本打算补充一个界面和变量的标示图和一个游戏思考流程,就再写一篇后记吧,顺便把原代码整理一下打包一起贴上来。



分享到
  • 微信分享
  • 新浪微博
  • QQ好友
  • QQ空间
点击: