【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 (新手程序员) 更新会在里面通知

这里写图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值