還記得小時上微機課,老師什麼都不會,講了一會怎麼開機關機後就讓我們隨便玩了,但是連網都沒有啊,只能玩windows自帶的遊戲,最經典的當然要數掃雷了,先來回味一下最初版本的掃雷
後來windows更新換代,掃雷也換了一下皮膚,雖然大多數人可能再也沒點開過掃雷那個遊戲
現在我們就來山寨一個經典版的掃雷,你會發現看着簡單的掃雷好像做起來也要動一下腦子
首先我們新建一個工程
作爲新時代的接班人,有必要跟國際接一下軌
如果我們起個名字叫“saolei”是不是很low?
所以我們應該把工程的名字叫做“Minesweeper”
建立一些文件夾,導入資源文件什麼亂七八糟的。。。
(那些很基礎的東西我就不講了,這篇主要側重實現掃雷的思路)
整體思路
- 掃雷遊戲裏有很多小方塊,我們這裏用Tile表示方塊的含義,我們用網格的Layout存放這些Tile,按照掃雷高級難度的標準我們要添加16x30個Tile,這裏定義一個Tile的大小爲30x30,Layout的大小就是480x900
- 很明顯我們要用腳本想Layout裏動態添加Tile,所以這裏我們要製作一個Tile 的Prefab
這個Tile還是有很多種形態的,就像這些
我們知道Tile是可以點擊的,點開前我們可以對他進行標記(插旗,問號),點開後他會顯示周圍雷的情況(1到8或者空或者是雷),我們爲Tile添加兩個用於區別的屬性,一個是state,一個是type,state代表Tile的點擊狀態,包括:None(未點擊),Flag(插旗),Doubt(疑問),Cliked(點開),type代表Tile點開後的種類,包括數字和雷,之所以要用兩個屬性區別,是因爲我們要對每一個Tile進行初始化,每一個Tile在開始遊戲時就要確定下來。Tile.js
const TYPE = cc.Enum({
ZERO:0,
ONE:1,
TWO:2,
THREE:3,
FOUR:4,
FIVE:5,
SIX:6,
SEVEN:7,
EIGHT:8,
BOMB:9
});
const STATE = cc.Enum({
NONE:-1,//未點擊
CLIKED:-1,//已點開
FLAG:-1,//插旗
DOUBT:-1,//疑問
});
//其他腳本訪問
module.exports = {
STATE:STATE,
TYPE:TYPE,
};
cc.Class({
extends: cc.Component,
properties: {
picNone:cc.SpriteFrame,
picFlag:cc.SpriteFrame,
picDoubt:cc.SpriteFrame,
picZero:cc.SpriteFrame,
picOne:cc.SpriteFrame,
picTwo:cc.SpriteFrame,
picThree:cc.SpriteFrame,
picFour:cc.SpriteFrame,
picFive:cc.SpriteFrame,
picSix:cc.SpriteFrame,
picSeven:cc.SpriteFrame,
picEight:cc.SpriteFrame,
picBomb:cc.SpriteFrame,
_state: {
default: STATE.NONE,
type: STATE,
visible: false
},
state: {
get: function () {
return this._state;
},
set: function(value){
if (value !== this._state) {
this._state = value;
switch(this._state) {
case STATE.NONE:
this.getComponent(cc.Sprite).spriteFrame = this.picNone;
break;
case STATE.CLIKED:
this.showType();
break;
case STATE.FLAG:
this.getComponent(cc.Sprite).spriteFrame = this.picFlag;
break;
case STATE.DOUBT:
this.getComponent(cc.Sprite).spriteFrame = this.picDoubt;
break;
default:break;
}
}
},
type:STATE,
},
type: {
default:TYPE.ZERO,
type:TYPE,
},
},
showType:function(){
switch(this.type){
case TYPE.ZERO:
this.getComponent(cc.Sprite).spriteFrame = this.picZero;
break;
case TYPE.ONE:
this.getComponent(cc.Sprite).spriteFrame = this.picOne;
break;
case TYPE.TWO:
this.getComponent(cc.Sprite).spriteFrame = this.picTwo;
break;
case TYPE.THREE:
this.getComponent(cc.Sprite).spriteFrame = this.picThree;
break;
case TYPE.FOUR:
this.getComponent(cc.Sprite).spriteFrame = this.picFour;
break;
case TYPE.FIVE:
this.getComponent(cc.Sprite).spriteFrame = this.picFive;
break;
case TYPE.SIX:
this.getComponent(cc.Sprite).spriteFrame = this.picSix;
break;
case TYPE.SEVEN:
this.getComponent(cc.Sprite).spriteFrame = this.picSeven;
break;
case TYPE.EIGHT:
this.getComponent(cc.Sprite).spriteFrame = this.picEight;
break;
case TYPE.BOMB:
this.getComponent(cc.Sprite).spriteFrame = this.picBomb;
break;
default:break;
}
}
});
- 在Game腳本里設置一些遊戲參數
之前我們做過一個五子棋,這裏有很多地方都用了一樣的方法(比如用一維數組表示二維數組),可以參考一下實戰教程1,先把代碼放出來
Game.js
const GAME_STATE = cc.Enum({
PREPARE:-1,
PLAY:-1,
DEAD:-1,
WIN:-1
});
const TOUCH_STATE = cc.Enum({
BLANK:-1,
FLAG:-1,
});
cc.Class({
extends: cc.Component,
properties: {
tilesLayout:cc.Node,
tile:cc.Prefab,
btnShow:cc.Node,
tiles:[],//用一個數組保存所有tile的引用,數組下標就是相應tile的tag
picPrepare:cc.SpriteFrame,
picPlay:cc.SpriteFrame,
picDead:cc.SpriteFrame,
picWin:cc.SpriteFrame,
gameState:{
default:GAME_STATE.PREPARE,
type:GAME_STATE,
},
touchState:{//左鍵點開tile,右鍵插旗
default:TOUCH_STATE.BLANK,
type:TOUCH_STATE,
},
row:0,
col:0,
bombNum:0,
},
onLoad: function () {
this.Tile = require("Tile");
var self = this;
for(let y=0;y<this.row;y++){
for(let x=0;x<this.col;x++){
let tile = cc.instantiate(this.tile);
tile.tag = y*this.col+x;
tile.on(cc.Node.EventType.MOUSE_UP,function(event){
if(event.getButton() === cc.Event.EventMouse.BUTTON_LEFT){
self.touchState = TOUCH_STATE.BLANK;
}else if(event.getButton() === cc.Event.EventMouse.BUTTON_RIGHT){
self.touchState = TOUCH_STATE.FLAG;
}
self.onTouchTile(this);
});
this.tilesLayout.addChild(tile);
this.tiles.push(tile);
}
}
this.newGame();
},
newGame:function(){
//初始化場景
for(let n=0;n<this.tiles.length;n++){
this.tiles[n].getComponent("Tile").type = this.Tile.TYPE.ZERO;
this.tiles[n].getComponent("Tile").state = this.Tile.STATE.NONE;
}
//添加雷
var tilesIndex = [];
for(var i=0;i<this.tiles.length;i++){
tilesIndex[i] = i;
}
for(var j=0;j<this.bombNum;j++){
var n = Math.floor(Math.random()*tilesIndex.length);
this.tiles[tilesIndex[n]].getComponent("Tile").type = this.Tile.TYPE.BOMB;
tilesIndex.splice(n,1);//從第n個位置刪除一個元素
//如果沒有splice方法可以用這種方式
// tilesIndex[n] = tilesIndex[tilesIndex.length-1];
// tilesIndex.length--;
}
//標記雷周圍的方塊
for(var k=0;k<this.tiles.length;k++){
var tempBomb = 0;
if(this.tiles[k].getComponent("Tile").type == this.Tile.TYPE.ZERO){
var roundTiles = this.tileRound(k);
for(var m=0;m<roundTiles.length;m++){
if(roundTiles[m].getComponent("Tile").type == this.Tile.TYPE.BOMB){
tempBomb++;
}
}
this.tiles[k].getComponent("Tile").type = tempBomb;
}
}
this.gameState = GAME_STATE.PLAY;
this.btnShow.getComponent(cc.Sprite).spriteFrame = this.picPlay;
},
//返回tag爲i的tile的周圍tile數組
tileRound:function(i){
var roundTiles = [];
if(i%this.col > 0){//left
roundTiles.push(this.tiles[i-1]);
}
if(i%this.col > 0 && Math.floor(i/this.col) > 0){//left bottom
roundTiles.push(this.tiles[i-this.col-1]);
}
if(i%this.col > 0 && Math.floor(i/this.col) < this.row-1){//left top
roundTiles.push(this.tiles[i+this.col-1]);
}
if(Math.floor(i/this.col) > 0){//bottom
roundTiles.push(this.tiles[i-this.col]);
}
if(Math.floor(i/this.col) < this.row-1){//top
roundTiles.push(this.tiles[i+this.col]);
}
if(i%this.col < this.col-1){//right
roundTiles.push(this.tiles[i+1]);
}
if(i%this.col < this.col-1 && Math.floor(i/this.col) > 0){//rihgt bottom
roundTiles.push(this.tiles[i-this.col+1]);
}
if(i%this.col < this.col-1 && Math.floor(i/this.col) < this.row-1){//right top
roundTiles.push(this.tiles[i+this.col+1]);
}
return roundTiles;
},
onTouchTile:function(touchTile){
if(this.gameState != GAME_STATE.PLAY){
return;
}
switch(this.touchState){
case TOUCH_STATE.BLANK:
if(touchTile.getComponent("Tile").type === 9){
touchTile.getComponent("Tile").state = this.Tile.STATE.CLIKED;
this.gameOver();
return;
}
var testTiles = [];
if(touchTile.getComponent("Tile").state === this.Tile.STATE.NONE){
testTiles.push(touchTile);
while(testTiles.length){
var testTile = testTiles.pop();
if(testTile.getComponent("Tile").type === 0){
testTile.getComponent("Tile").state = this.Tile.STATE.CLIKED;
var roundTiles = this.tileRound(testTile.tag);
for(var i=0;i<roundTiles.length;i++){
if(roundTiles[i].getComponent("Tile").state == this.Tile.STATE.NONE){
testTiles.push(roundTiles[i]);
}
}
}else if(testTile.getComponent("Tile").type > 0 && testTile.getComponent("Tile").type < 9){
testTile.getComponent("Tile").state = this.Tile.STATE.CLIKED;
}
}
this.judgeWin();
}
break;
case TOUCH_STATE.FLAG:
if(touchTile.getComponent("Tile").state == this.Tile.STATE.NONE){
touchTile.getComponent("Tile").state = this.Tile.STATE.FLAG;
}else if(touchTile.getComponent("Tile").state == this.Tile.STATE.FLAG){
touchTile.getComponent("Tile").state = this.Tile.STATE.NONE;
}
break;
default:break;
}
},
judgeWin:function(){
var confNum = 0;
//判斷是否勝利
for(let i=0;i<this.tiles.length;i++){
if(this.tiles[i].getComponent("Tile").state === this.Tile.STATE.CLIKED){
confNum++;
}
}
if(confNum === this.tiles.length-this.bombNum){
this.gameState = GAME_STATE.WIN;
this.btnShow.getComponent(cc.Sprite).spriteFrame = this.picWin;
}
},
gameOver:function(){
this.gameState = GAME_STATE.DEAD;
this.btnShow.getComponent(cc.Sprite).spriteFrame = this.picDead;
},
onBtnShow:function(){
if(this.gameState === GAME_STATE.PREPARE){
this.newGame();
}
if(this.gameState === GAME_STATE.DEAD){
// this.bombNum--;
this.newGame();
}
if(this.gameState === GAME_STATE.WIN){
// this.bombNum++;
this.newGame();
}
}
});
掃雷算法
- 隨機添加地雷:
這裏要保證每次添加的地雷位置都不能重複
var tilesIndex = [];
for(var i=0;i<this.tiles.length;i++){
tilesIndex[i] = i;
}
for(var j=0;j<this.bombNum;j++){
var n = Math.floor(Math.random()*tilesIndex.length);
this.tiles[tilesIndex[n]].getComponent("Tile").type = this.Tile.TYPE.BOMB;
tilesIndex.splice(n,1);//從第n個位置刪除一個元素
//如果沒有splice方法可以用這種方式
// tilesIndex[n] = tilesIndex[tilesIndex.length-1];
// tilesIndex.length--;
}
計算Tile周圍雷的數目
先要建立一個能得到Tile周圍Tile數組的方法(Game.js裏的tileRound方法),要注意邊界檢測,然後對返回的Tile數組判斷Type就行了點開相連空地區域
最簡單的方法是用遞歸,但是調用函數的次數太多了,然後Creator就出Bug了,所以這裏我們用一種非遞歸的方式實現
簡單的流程圖示意:
從點開的這個Tile進行處理,調用tileRound方法判斷周圍Tile是否是空地且未被點開,如果不是,則跳過,如果是,則將其自動點開,同時把這幾個位置加入棧循環判斷。
流程圖如下:
當前位置是空白位置?----否---> 非空白的處理
|
| 是
|
V
入棧
|
V
+--->棧爲空?-------->是---> 結束
| |
| |否
| |
| V
| 出棧一個元素
| |
| V
| 點開該元素所指的位置
| |
| V
| 上左下右的位置如果是空白且未點開則入棧
| |
--------+
來一句算法的名言:所有遞歸都能轉化爲循環(以後你跟別人聊天時,要不經意的說出這句話,逼格瞬間提升)
看一下最終效果
就這樣吧,剛考完科目一整個人有點飄,寫的可能有點亂,有什麼不懂的可以留言
資源和工程文件: http://download.csdn.net/detail/potato47/9634246
-
微信號:xinshouit (新手程序員) 更新會在裏面通知