【Cocos Creator實戰教程(5)】——掃雷

還記得小時上微機課,老師什麼都不會,講了一會怎麼開機關機後就讓我們隨便玩了,但是連網都沒有啊,只能玩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 (新手程序員) 更新會在裏面通知

這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章