扫雷游戏
MineSweeper是一个不错的Android开源扫雷游戏,对于初学Android开发网的网友可能有很大的帮助,对于Java游戏开发也有一定的参考意义。该游戏主要有以下技术值得学习:
1. 个性化字体,计分器使用的是LED字体,可以帮助我们如何导入外部字体在Android平台中显示。
2. 带图片的Toast,下面的You won in 36 seconds这个Toast使用了自定义的布局,可以显示图片和文字。
3. 自定义Button控件,可以看到标记是否为雷,显示附近地雷数量的按钮控件,初学者可以很容易的学习到Android开发中常用的自定义控件技术。
4. 因为游戏实时性不高,这里没有用到SurfaceView,下次Android开发网给大家提供一个将对高级些的例子。
关键代码如下:
main.xml
<?xml version="1.0" encoding="utf-8"?><TableLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:stretchColumns="0,2" android:background="@drawable/back"> <TableRow><TextViewandroid:id="@+id/Timer"android:layout_column="0"android:layout_width="fill_parent"android:layout_height="48px"android:gravity="center_horizontal"android:padding="5dip" android:textColor="#FFFFFF"android:textSize="40sp"android:text="000" /><ImageButton android:id="@+id/Smiley"android:layout_column="1"android:background="@drawable/smiley_button_states"android:scaleType="center"android:padding="5dip" android:layout_width="48px"android:layout_height="48px"/><TextViewandroid:id="@+id/MineCount"android:layout_column="2"android:layout_width="fill_parent"android:layout_height="48px"android:gravity="center_horizontal"android:padding="5dip" android:textColor="#FFFFFF"android:textSize="40sp"android:text="000" /> </TableRow> <TableRow><TextViewandroid:layout_column="0"android:layout_width="fill_parent"android:layout_height="50px"android:layout_span="3"android:padding="10dip"/></TableRow> <TableRow><TableLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/MineField" android:layout_width="260px" android:layout_height="260px" android:gravity="bottom" android:stretchColumns="*" android:layout_span="3" android:padding="5dip" ></TableLayout> </TableRow></TableLayout>
<?xml version="1.0" encoding="utf-8"?><selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_focused="true" android:state_pressed="false" android:layout_width="48px" android:layout_height="48px" android:drawable="@drawable/smile" /> <item android:state_focused="true" android:state_pressed="true" android:layout_width="48px" android:layout_height="48px" android:drawable="@drawable/surprise" /> <item android:state_focused="false" android:state_pressed="true" android:layout_width="48px" android:layout_height="48px" android:drawable="@drawable/surprise" /> <item android:layout_width="48px" android:layout_height="48px" android:drawable="@drawable/smile" /></selector>
package com.VertexVerveInc.Games;import android.content.Context;import android.graphics.Color;import android.graphics.Typeface;import android.util.AttributeSet;import android.widget.Button;public class Block extends Button{private boolean isCovered; // is block covered yetprivate boolean isMined; // does the block has a mine underneathprivate boolean isFlagged; // is block flagged as a potential mineprivate boolean isQuestionMarked; // is block question markedprivate boolean isClickable; // can block accept click eventsprivate int numberOfMinesInSurrounding; // number of mines in nearby blockspublic Block(Context context){super(context);}public Block(Context context, AttributeSet attrs){super(context, attrs);}public Block(Context context, AttributeSet attrs, int defStyle){super(context, attrs, defStyle);}// set default properties for the blockpublic void setDefaults(){isCovered = true;isMined = false;isFlagged = false;isQuestionMarked = false;isClickable = true;numberOfMinesInSurrounding = 0;this.setBackgroundResource(R.drawable.square_blue);setBoldFont();}// mark the block as disabled/opened// update the number of nearby minespublic void setNumberOfSurroundingMines(int number){this.setBackgroundResource(R.drawable.square_grey);updateNumber(number);}// set mine icon for block// set block as disabled/opened if false is passedpublic void setMineIcon(boolean enabled){this.setText("M");if (!enabled){this.setBackgroundResource(R.drawable.square_grey);this.setTextColor(Color.RED);}else{this.setTextColor(Color.BLACK);}}// set mine as flagged// set block as disabled/opened if false is passedpublic void setFlagIcon(boolean enabled){this.setText("F");if (!enabled){this.setBackgroundResource(R.drawable.square_grey);this.setTextColor(Color.RED);}else{this.setTextColor(Color.BLACK);}}// set mine as question mark// set block as disabled/opened if false is passedpublic void setQuestionMarkIcon(boolean enabled){this.setText("?");if (!enabled){this.setBackgroundResource(R.drawable.square_grey);this.setTextColor(Color.RED);}else{this.setTextColor(Color.BLACK);}}// set block as disabled/opened if false is passed// else enable/close itpublic void setBlockAsDisabled(boolean enabled){if (!enabled){this.setBackgroundResource(R.drawable.square_grey);}else{this.setBackgroundResource(R.drawable.square_blue);}}// clear all icons/textpublic void clearAllIcons(){this.setText("");}// set font as boldprivate void setBoldFont(){this.setTypeface(null, Typeface.BOLD);}// uncover this blockpublic void OpenBlock(){// cannot uncover a mine which is not coveredif (!isCovered)return;setBlockAsDisabled(false);isCovered = false;// check if it has mineif (hasMine()){setMineIcon(false);}// update with the nearby mine countelse{setNumberOfSurroundingMines(numberOfMinesInSurrounding);}}// set text as nearby mine countpublic void updateNumber(int text){if (text != 0){this.setText(Integer.toString(text));// select different color for each number// we have already skipped 0 mine countswitch (text){case 1:this.setTextColor(Color.BLUE);break;case 2:this.setTextColor(Color.rgb(0, 100, 0));break;case 3:this.setTextColor(Color.RED);break;case 4:this.setTextColor(Color.rgb(85, 26, 139));break;case 5:this.setTextColor(Color.rgb(139, 28, 98));break;case 6:this.setTextColor(Color.rgb(238, 173, 14));break;case 7:this.setTextColor(Color.rgb(47, 79, 79));break;case 8:this.setTextColor(Color.rgb(71, 71, 71));break;case 9: this.setTextColor(Color.rgb(205, 205, 0));break;}}}// set block as a mine underneathpublic void plantMine(){isMined = true;}// mine was opened// change the block icon and colorpublic void triggerMine(){setMineIcon(true);this.setTextColor(Color.RED);}// is block still coveredpublic boolean isCovered(){return isCovered;}// does the block have any mine underneathpublic boolean hasMine(){return isMined;}// set number of nearby minespublic void setNumberOfMinesInSurrounding(int number){numberOfMinesInSurrounding = number;}// get number of nearby minespublic int getNumberOfMinesInSorrounding(){return numberOfMinesInSurrounding;}// is block marked as flaggedpublic boolean isFlagged(){return isFlagged;}// mark block as flaggedpublic void setFlagged(boolean flagged){isFlagged = flagged;}// is block marked as a question markpublic boolean isQuestionMarked(){return isQuestionMarked;}// set question mark for the blockpublic void setQuestionMarked(boolean questionMarked){isQuestionMarked = questionMarked;}// can block receive click eventpublic boolean isClickable(){return isClickable;}// disable block for receive click eventspublic void setClickable(boolean clickable){isClickable = clickable;}}
package com.VertexVerveInc.Games;import java.util.Random;import android.app.Activity;import android.graphics.Typeface;import android.os.Bundle;import android.os.Handler;import android.view.Gravity;import android.view.View;import android.view.View.OnClickListener;import android.view.View.OnLongClickListener;import android.widget.ImageButton;import android.widget.ImageView;import android.widget.LinearLayout;import android.widget.TableRow.LayoutParams;import android.widget.TableLayout;import android.widget.TableRow;import android.widget.TextView;import android.widget.Toast;public class MinesweeperGame extends Activity{private TextView txtMineCount;private TextView txtTimer;private ImageButton btnSmile;private TableLayout mineField; // table layout to add mines toprivate Block blocks[][]; // blocks for mine fieldprivate int blockDimension = 24; // width of each blockprivate int blockPadding = 2; // padding between blocksprivate int numberOfRowsInMineField = 9;private int numberOfColumnsInMineField = 9;private int totalNumberOfMines = 10;// timer to keep track of time elapsedprivate Handler timer = new Handler();private int secondsPassed = 0;private boolean isTimerStarted; // check if timer already started or notprivate boolean areMinesSet; // check if mines are planted in blocksprivate boolean isGameOver;private int minesToFind; // number of mines yet to be discovered@Overridepublic void onCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.main);txtMineCount = (TextView) findViewById(R.id.MineCount);txtTimer = (TextView) findViewById(R.id.Timer);// set font style for timer and mine count to LCD styleTypeface lcdFont = Typeface.createFromAsset(getAssets(),"fonts/lcd2mono.ttf");txtMineCount.setTypeface(lcdFont);txtTimer.setTypeface(lcdFont);btnSmile = (ImageButton) findViewById(R.id.Smiley);btnSmile.setOnClickListener(new OnClickListener(){@Overridepublic void onClick(View view){endExistingGame();startNewGame();}});mineField = (TableLayout)findViewById(R.id.MineField);showDialog("Click smiley to start New Game", 2000, true, false);}private void startNewGame(){// plant mines and do rest of the calculationscreateMineField();// display all blocks in UIshowMineField();minesToFind = totalNumberOfMines;isGameOver = false;secondsPassed = 0;}private void showMineField(){// remember we will not show 0th and last Row and Columns// they are used for calculation purposes onlyfor (int row = 1; row < numberOfRowsInMineField + 1; row++){TableRow tableRow = new TableRow(this); tableRow.setLayoutParams(new LayoutParams((blockDimension + 2 * blockPadding) * numberOfColumnsInMineField, blockDimension + 2 * blockPadding));for (int column = 1; column < numberOfColumnsInMineField + 1; column++){blocks[row][column].setLayoutParams(new LayoutParams( blockDimension + 2 * blockPadding, blockDimension + 2 * blockPadding)); blocks[row][column].setPadding(blockPadding, blockPadding, blockPadding, blockPadding);tableRow.addView(blocks[row][column]);}mineField.addView(tableRow,new TableLayout.LayoutParams( (blockDimension + 2 * blockPadding) * numberOfColumnsInMineField, blockDimension + 2 * blockPadding)); }}private void endExistingGame(){stopTimer(); // stop if timer is runningtxtTimer.setText("000"); // revert all texttxtMineCount.setText("000"); // revert mines countbtnSmile.setBackgroundResource(R.drawable.smile);// remove all rows from mineField TableLayoutmineField.removeAllViews();// set all variables to support end of gameisTimerStarted = false;areMinesSet = false;isGameOver = false;minesToFind = 0;}private void createMineField(){// we take one row extra row for each side// overall two extra rows and two extra columns// first and last row/column are used for calculations purposes only// x|xxxxxxxxxxxxxx|x// ------------------// x| |x// x| |x// ------------------// x|xxxxxxxxxxxxxx|x// the row and columns marked as x are just used to keep counts of near by minesblocks = new Block[numberOfRowsInMineField + 2][numberOfColumnsInMineField + 2];for (int row = 0; row < numberOfRowsInMineField + 2; row++){for (int column = 0; column < numberOfColumnsInMineField + 2; column++){blocks[row][column] = new Block(this);blocks[row][column].setDefaults();// pass current row and column number as final int's to event listeners// this way we can ensure that each event listener is associated to // particular instance of block onlyfinal int currentRow = row;final int currentColumn = column;// add Click Listener// this is treated as Left Mouse clickblocks[row][column].setOnClickListener(new OnClickListener(){@Overridepublic void onClick(View view){// start timer on first clickif (!isTimerStarted){startTimer();isTimerStarted = true;}// set mines on first clickif (!areMinesSet){areMinesSet = true;setMines(currentRow, currentColumn);}// this is not first click// check if current block is flagged// if flagged the don't do anything// as that operation is handled by LongClick// if block is not flagged then uncover nearby blocks// till we get numbered minesif (!blocks[currentRow][currentColumn].isFlagged()){// open nearby blocks till we get numbered blocksrippleUncover(currentRow, currentColumn);// did we clicked a mineif (blocks[currentRow][currentColumn].hasMine()){// Oops, game overfinishGame(currentRow,currentColumn);}// check if we win the gameif (checkGameWin()){// mark game as winwinGame();}}}});// add Long Click listener// this is treated as right mouse click listenerblocks[row][column].setOnLongClickListener(new OnLongClickListener(){public boolean onLongClick(View view){// simulate a left-right (middle) click// if it is a long click on an opened mine then// open all surrounding blocksif (!blocks[currentRow][currentColumn].isCovered() && (blocks[currentRow][currentColumn].getNumberOfMinesInSorrounding() > 0) && !isGameOver){int nearbyFlaggedBlocks = 0;for (int previousRow = -1; previousRow < 2; previousRow++){for (int previousColumn = -1; previousColumn < 2; previousColumn++){if (blocks[currentRow + previousRow][currentColumn + previousColumn].isFlagged()){nearbyFlaggedBlocks++;}}}// if flagged block count is equal to nearby mine count// then open nearby blocksif (nearbyFlaggedBlocks == blocks[currentRow][currentColumn].getNumberOfMinesInSorrounding()){for (int previousRow = -1; previousRow < 2; previousRow++){for (int previousColumn = -1; previousColumn < 2; previousColumn++){// don't open flagged blocksif (!blocks[currentRow + previousRow][currentColumn + previousColumn].isFlagged()){// open blocks till we get numbered blockrippleUncover(currentRow + previousRow, currentColumn + previousColumn);// did we clicked a mineif (blocks[currentRow + previousRow][currentColumn + previousColumn].hasMine()){// oops game overfinishGame(currentRow + previousRow, currentColumn + previousColumn);}// did we win the gameif (checkGameWin()){// mark game as winwinGame();}}}}}// as we no longer want to judge this gesture so return// not returning from here will actually trigger other action// which can be marking as a flag or question mark or blankreturn true;}// if clicked block is enabled, clickable or flaggedif (blocks[currentRow][currentColumn].isClickable() && (blocks[currentRow][currentColumn].isEnabled() || blocks[currentRow][currentColumn].isFlagged())){// for long clicks set:// 1. empty blocks to flagged// 2. flagged to question mark// 3. question mark to blank// case 1. set blank block to flaggedif (!blocks[currentRow][currentColumn].isFlagged() && !blocks[currentRow][currentColumn].isQuestionMarked()){blocks[currentRow][currentColumn].setBlockAsDisabled(false);blocks[currentRow][currentColumn].setFlagIcon(true);blocks[currentRow][currentColumn].setFlagged(true);minesToFind--; //reduce mine countupdateMineCountDisplay();}// case 2. set flagged to question markelse if (!blocks[currentRow][currentColumn].isQuestionMarked()){blocks[currentRow][currentColumn].setBlockAsDisabled(true);blocks[currentRow][currentColumn].setQuestionMarkIcon(true);blocks[currentRow][currentColumn].setFlagged(false);blocks[currentRow][currentColumn].setQuestionMarked(true);minesToFind++; // increase mine countupdateMineCountDisplay();}// case 3. change to blank squareelse{blocks[currentRow][currentColumn].setBlockAsDisabled(true);blocks[currentRow][currentColumn].clearAllIcons();blocks[currentRow][currentColumn].setQuestionMarked(false);// if it is flagged then increment mine countif (blocks[currentRow][currentColumn].isFlagged()){minesToFind++; // increase mine countupdateMineCountDisplay();}// remove flagged statusblocks[currentRow][currentColumn].setFlagged(false);}updateMineCountDisplay(); // update mine display}return true;}});}}}private boolean checkGameWin(){for (int row = 1; row < numberOfRowsInMineField + 1; row++){for (int column = 1; column < numberOfColumnsInMineField + 1; column++){if (!blocks[row][column].hasMine() && blocks[row][column].isCovered()){return false;}}}return true;}private void updateMineCountDisplay(){if (minesToFind < 0){txtMineCount.setText(Integer.toString(minesToFind));}else if (minesToFind < 10){txtMineCount.setText("00" + Integer.toString(minesToFind));}else if (minesToFind < 100){txtMineCount.setText("0" + Integer.toString(minesToFind));}else{txtMineCount.setText(Integer.toString(minesToFind));}}private void winGame(){stopTimer();isTimerStarted = false;isGameOver = true;minesToFind = 0; //set mine count to 0//set icon to cool dudebtnSmile.setBackgroundResource(R.drawable.cool);updateMineCountDisplay(); // update mine count// disable all buttons// set flagged all un-flagged blocksfor (int row = 1; row < numberOfRowsInMineField + 1; row++){for (int column = 1; column < numberOfColumnsInMineField + 1; column++){blocks[row][column].setClickable(false);if (blocks[row][column].hasMine()){blocks[row][column].setBlockAsDisabled(false);blocks[row][column].setFlagIcon(true);}}}// show messageshowDialog("You won in " + Integer.toString(secondsPassed) + " seconds!", 1000, false, true);}private void finishGame(int currentRow, int currentColumn){isGameOver = true; // mark game as overstopTimer(); // stop timerisTimerStarted = false;btnSmile.setBackgroundResource(R.drawable.sad);// show all mines// disable all blocksfor (int row = 1; row < numberOfRowsInMineField + 1; row++){for (int column = 1; column < numberOfColumnsInMineField + 1; column++){// disable blockblocks[row][column].setBlockAsDisabled(false);// block has mine and is not flaggedif (blocks[row][column].hasMine() && !blocks[row][column].isFlagged()){// set mine iconblocks[row][column].setMineIcon(false);}// block is flagged and doesn't not have mineif (!blocks[row][column].hasMine() && blocks[row][column].isFlagged()){// set flag iconblocks[row][column].setFlagIcon(false);}// block is flaggedif (blocks[row][column].isFlagged()){// disable the blockblocks[row][column].setClickable(false);}}}// trigger mineblocks[currentRow][currentColumn].triggerMine();// show messageshowDialog("You tried for " + Integer.toString(secondsPassed) + " seconds!", 1000, false, false);}private void setMines(int currentRow, int currentColumn){// set mines excluding the location where user clickedRandom rand = new Random();int mineRow, mineColumn;for (int row = 0; row < totalNumberOfMines; row++){mineRow = rand.nextInt(numberOfColumnsInMineField);mineColumn = rand.nextInt(numberOfRowsInMineField);if ((mineRow + 1 != currentColumn) || (mineColumn + 1 != currentRow)){if (blocks[mineColumn + 1][mineRow + 1].hasMine()){row--; // mine is already there, don't repeat for same block}// plant mine at this locationblocks[mineColumn + 1][mineRow + 1].plantMine();}// exclude the user clicked locationelse{row--;}}int nearByMineCount;// count number of mines in surrounding blocks for (int row = 0; row < numberOfRowsInMineField + 2; row++){for (int column = 0; column < numberOfColumnsInMineField + 2; column++){// for each block find nearby mine countnearByMineCount = 0;if ((row != 0) && (row != (numberOfRowsInMineField + 1)) && (column != 0) && (column != (numberOfColumnsInMineField + 1))){// check in all nearby blocksfor (int previousRow = -1; previousRow < 2; previousRow++){for (int previousColumn = -1; previousColumn < 2; previousColumn++){if (blocks[row + previousRow][column + previousColumn].hasMine()){// a mine was found so increment the counternearByMineCount++;}}}blocks[row][column].setNumberOfMinesInSurrounding(nearByMineCount);}// for side rows (0th and last row/column)// set count as 9 and mark it as openedelse{blocks[row][column].setNumberOfMinesInSurrounding(9);blocks[row][column].OpenBlock();}}}}private void rippleUncover(int rowClicked, int columnClicked){// don't open flagged or mined rowsif (blocks[rowClicked][columnClicked].hasMine() || blocks[rowClicked][columnClicked].isFlagged()){return;}// open clicked blockblocks[rowClicked][columnClicked].OpenBlock();// if clicked block have nearby mines then don't open furtherif (blocks[rowClicked][columnClicked].getNumberOfMinesInSorrounding() != 0 ){return;}// open next 3 rows and 3 columns recursivelyfor (int row = 0; row < 3; row++){for (int column = 0; column < 3; column++){// check all the above checked conditions// if met then open subsequent blocksif (blocks[rowClicked + row - 1][columnClicked + column - 1].isCovered()&& (rowClicked + row - 1 > 0) && (columnClicked + column - 1 > 0)&& (rowClicked + row - 1 < numberOfRowsInMineField + 1) && (columnClicked + column - 1 < numberOfColumnsInMineField + 1)){rippleUncover(rowClicked + row - 1, columnClicked + column - 1 );} }}return;}public void startTimer(){if (secondsPassed == 0) {timer.removeCallbacks(updateTimeElasped);// tell timer to run call back after 1 secondtimer.postDelayed(updateTimeElasped, 1000);}}public void stopTimer(){// disable call backstimer.removeCallbacks(updateTimeElasped);}// timer call back when timer is tickedprivate Runnable updateTimeElasped = new Runnable(){public void run(){long currentMilliseconds = System.currentTimeMillis();++secondsPassed;if (secondsPassed < 10){txtTimer.setText("00" + Integer.toString(secondsPassed));}else if (secondsPassed < 100){txtTimer.setText("0" + Integer.toString(secondsPassed));}else{txtTimer.setText(Integer.toString(secondsPassed));} // add notificationtimer.postAtTime(this, currentMilliseconds);// notify to call back after 1 seconds// basically to remain in the timer looptimer.postDelayed(updateTimeElasped, 1000);}};private void showDialog(String message, int milliseconds, boolean useSmileImage, boolean useCoolImage){// show messageToast dialog = Toast.makeText(getApplicationContext(),message,Toast.LENGTH_LONG);dialog.setGravity(Gravity.CENTER, 0, 0);LinearLayout dialogView = (LinearLayout) dialog.getView();ImageView coolImage = new ImageView(getApplicationContext());if (useSmileImage){coolImage.setImageResource(R.drawable.smile);}else if (useCoolImage){coolImage.setImageResource(R.drawable.cool);}else{coolImage.setImageResource(R.drawable.sad);}dialogView.addView(coolImage, 0);dialog.setDuration(milliseconds);dialog.show();}}