Cocos掃雷游戲核心算法思想
一、掃雷游戲?qū)崿F(xiàn)核心思路解析
數(shù)據(jù)和視圖盡量分離。采用面向?qū)ο蟮膶?shí)現(xiàn)設(shè)計(jì)數(shù)據(jù)模塊。格子作為一類(lèi)對(duì)象,雷場(chǎng)作為一類(lèi)對(duì)象,雷場(chǎng)由格子構(gòu)成。
二、掃雷游戲核心數(shù)據(jù)模塊
1. Cell.js單元格類(lèi)
// 單個(gè)單元格,用于保存數(shù)據(jù) // x,y,坐標(biāo),或者行和列, // info顯示文本,空表示什么都沒(méi)有,周邊也沒(méi)有地雷 // *表示此處是一顆地雷,數(shù)字表示其周邊有幾顆地雷 function Cell(x, y, info){ ? ?this.x = x; ? ?this.y = y; ? ?this.info = info; }; module.exports = Cell;
2. MineField.js雷場(chǎng)類(lèi)
// 雷場(chǎng)(行數(shù),列數(shù),地雷數(shù)) function MineField(rowNum, colNum, mineNum){ ? ?this.rowNum = rowNum; ? ?this.colNum = colNum; ? ?this.mineNum = mineNum; ? ?// 調(diào)用【1】畫(huà)格子 ? ?this.init(); ? ?// 調(diào)用【2】藏地雷 ? ?this.hideMine(); ? ?// 調(diào)用【3】留暗號(hào) ? ?this.markNumber(); };
module.exports = MineField;
【1】畫(huà)格子
雷場(chǎng)由Cell的對(duì)象構(gòu)成的數(shù)組組成,實(shí)質(zhì)就是給雷場(chǎng)的cells數(shù)組賦值。
// 【1】利用原型擴(kuò)展方法:初始化雷場(chǎng)的所有格子數(shù)據(jù) MineField.prototype.init = function(){ ? ?let cellsNum = this.rowNum * this.colNum; ? ?this.cells = new Array(cellsNum); ? ?for(let i=0; i<this.rowNum; i++){ ? ? ? ?for(let j=0; j<this.colNum; j++){ ? ? ? ? ? ?let index = this.getIndexByXY(i,j);//this.colNum * i + j; ? ? ? ? ? ?this.cells[index] = new Cell(i, j, ""); ? ? ? ?} ? ?} };
補(bǔ)充提煉通過(guò)坐標(biāo)獲取格子索引的方法,以供后續(xù)其它地方用:
// 【1-1】通過(guò)坐標(biāo)獲取單元格的所處格子的索引 MineField.prototype.getIndexByXY = function(x, y){ ? ?return x * this.colNum + y; } // 通過(guò)單元格對(duì)象獲取單元格所處格子的索引 MineField.prototype.getIndexByCell = function(cell){ ? ?return this.getIndexByXY(cell.x, cell.y); }
在此文件頭部引入Cell類(lèi):
var Cell = require("Cell");
【2】藏地雷
實(shí)質(zhì)就是修改雷場(chǎng)的cells數(shù)組中的隨機(jī)一些索引的cell的info屬性值。
// 【2】藏地雷:將地雷數(shù)據(jù)設(shè)置到this.cells中的Cell的info中去 MineField.prototype.hideMine = function(){ ? ?// 隨機(jī)無(wú)重復(fù)元素的數(shù)組,且范圍限定在[0,this.rowNum*this.colNum); ? ?let end = this.colNum * this.rowNum; ? ?// 記錄所有地雷所在的cells的索引 ? ?this.mineIndexs = ArrayUtils.randChoiseFromTo(0, end, this.mineNum); ? ?console.log("地雷位置序號(hào):",this.mineIndexs); ? ?// 找到相應(yīng)格子的位置,設(shè)置其cell對(duì)象的info屬性為*,表示地雷 ? ?this.mineCells = new Array(this.mineNum); ? ?for(let i=0,len=this.mineIndexs.length; i<len; i++){ ? ? ? ?let index = this.mineIndexs[i]; ? ? ? ?let cell = this.cells[index]; ? ? ? ?cell.info = "*"; ? ? ? ?// 保存所有地雷所在的單元格 ? ? ? ?this.mineCells[i] = cell; ? ?} };
方法randChoiseFromTo參考四、ArrayUtils.js數(shù)組工具類(lèi)。
在此文件頭部引入數(shù)組工具類(lèi):
var ArrayUtils = require("ArrayUtils");
【3】留暗號(hào)
實(shí)質(zhì)就是在地雷周邊8個(gè)格子中標(biāo)上數(shù)字,數(shù)值為此單元格周邊8個(gè)單元格中雷的數(shù)量。如下圖所示:

// 【3】留暗號(hào):標(biāo)記地雷周?chē)袉卧竦臄?shù)字,也就是設(shè)置其info屬性 MineField.prototype.markNumber = function(){ ? ?// 遍歷所有地雷單元格,每次找到其周邊非雷格子,給其數(shù)字加1 ? ?console.log("this.mineCells:",this.mineCells) ? ?for(let i=0,len=this.mineCells.length; i<len; i++){ ? ? ? ?// 【3-1】拿到地雷單元格 周?chē)乃械姆抢讍卧?應(yīng)該是數(shù)字的單元格) ? ? ? ?let numberCells = this.getNumberCellsAround(this.mineCells[i]); ? ? ? ?// 【3-2】更新地雷周?chē)蟹抢祝〝?shù)字)單元格的數(shù)字標(biāo)記 ? ? ? ?this.updateNumberMarks(numberCells); ? ?} };
【3-1】獲取某顆地雷周?chē)袉卧?/p>
如下圖所示,假如要獲?。?,0)周?chē)?個(gè)單元格,則偏移量就是其周邊8個(gè)單元格的坐標(biāo):

同時(shí),偏移后,我們還要判斷這個(gè)格子是否超出雷場(chǎng)。即便宜后x、y值不能小于0,且不能大于行或列的最大值。
偏移量offset和判斷是否超出雷場(chǎng)區(qū)域的方法如下:
// 【3-1-1】周?chē)?個(gè)坐標(biāo)相對(duì)于中心坐標(biāo)(0,0)的偏移量 var offset = [{x:-1,y:-1},{x:0,y:-1},{x:1,y:-1}, ? ?{x:-1,y:0},{x:1,y:0}, ? ?{x:-1,y:1},{x:0,y:1},{x:1,y:1}, ]; // 【3-1-2】判斷坐標(biāo)為x,y的單元格cell是否超出了區(qū)域 MineField.prototype.outOfFiled = function(x, y){ ? ?return x<0 || x>=this.rowNum || y<0 || y>=this.colNum; };
如果地雷周?chē)膯卧袷菙?shù)字,則我們不需要計(jì)算數(shù)值,應(yīng)排除。
// 【3-1】拿到cell周?chē)乃械姆抢讍卧?應(yīng)該是數(shù)字的單元格) MineField.prototype.getNumberCellsAround = function(cell){ ? ?let result = []; ? ?for(let i=0,len=offset.length; i<len; i++){ ? ? ? ?// 【3-1-1】得到相對(duì)于cell偏移后的x、y坐標(biāo) ? ? ? ?let x = cell.x + offset[i].x; ? ? ? ?let y = cell.y + offset[i].y ? ? ? ?// 【3-1-2】判斷坐標(biāo)為x,y的單元格cell是否超出了區(qū)域 ? ? ? ?if(this.outOfFiled(x, y)){ ? ? ? ? ? ?continue; ? ? ? ?} ? ? ? ?// 如果是地雷繼續(xù)下一次循環(huán) ? ? ? ?let index = this.getIndexByXY(x,y); //x*this.colNum + y; ? ? ? ?let cellSide = this.cells[index]; ? ? ? ?if (cellSide.info === "*"){ ? ? ? ? ? ?continue; ? ? ? ?} ? ? ? ?// 如果沒(méi)有超出雷場(chǎng)區(qū)域,且為非雷單元格,則添加到數(shù)組中 ? ? ? ?result.push(new Cell(x, y, "")); ? ?} ? ?return result; };
【3-2】更新所有地雷周?chē)蟹抢祝〝?shù)字)單元格的數(shù)字標(biāo)記
// 【3-2】更新地雷周?chē)蟹抢祝〝?shù)字)單元格的數(shù)字標(biāo)記 MineField.prototype.updateNumberMarks = function(numberCells){ ? ?/** ? ? * 設(shè)置邏輯:①如果原來(lái)info屬性為*,不需要設(shè)置,【已經(jīng)排除了】 ? ? * ? ? ?②如果原來(lái)info屬性為"",證明是第一次標(biāo)記,標(biāo)記info為1 ? ? * ? ? ?③如果原來(lái)屬性不為*,也不為空,則在原有值基礎(chǔ)上加1 ? ? */ ? ?for(let i=0,len=numberCells.length; i<len; i++){ ? ? ? ?let index = this.getIndexByCell(numberCells[i]); ? ? ? ?if(this.cells[index].info === ""){ ? ? ? ? ? ?this.cells[index].info = 1; ? ? ? ?}else{ ? ? ? ? ? ?let num = parseInt(this.cells[index].info); ? ? ? ? ? ?this.cells[index].info = ++num; ? ? ? ?} ? ?} };
三、ArrayUtils.js數(shù)組工具類(lèi)(直接使用)
// 數(shù)組工具類(lèi) var ArrayUtils = function(){}; // 【1】初始化得到有序元素?cái)?shù)組:從[start,end)的自然數(shù)序列 ArrayUtils.initOrderArray = function(start, end){ ? ?let sortArray = []; ? ?for(let i= start; i<end; i++){ ? ? ? ?sortArray.push(i); ? ?} ? ?return sortArray; }; // 【2】從數(shù)組arr中隨機(jī)抽取count個(gè)元素,返回?cái)?shù)組 ArrayUtils.randChoiseFromArr = function(arr, count){ ? ?let result = arr; ? ?// 隨機(jī)排序,打亂順序 ? ?result.sort(function(){ ? ? ? ?return 0.5 - Math.random(); ? ?}); ? ?// 返回打亂順序后的數(shù)組中的前count個(gè)元素 ? ?return result.slice(0,count); }; // 【3】從從start到end中的連續(xù)整數(shù)中隨機(jī)抽取count個(gè)數(shù)字 ArrayUtils.randChoiseFromTo = function(start, end, count){ ? ?let arr = this.initOrderArray(start,end); ? ?return this.randChoiseFromArr(arr, count); }; // 【2】-【方式二】從數(shù)組arr中隨機(jī)抽取count個(gè)元素,返回?cái)?shù)組 ArrayUtils.getRandomArrayElements = function(arr, count) { ? ?// 從0位置取到結(jié)束位置存入shffled數(shù)組 ? ?let shuffled = arr.slice(0); ? ?let i = arr.length; ? ?let min = i - count; ? ?let temp = 0; ? ?let index = 0; ? ?// 隨機(jī)一個(gè)位置的元素和最后一個(gè)元素交換 ? ?// 隨機(jī)一個(gè)位置元素和倒數(shù)第二個(gè)元素交換 ? ?// 假設(shè)i=8,count=3,則min=5, ? ?// 循環(huán)體中[i]=7,6,5,也就是說(shuō)最后三個(gè)元素要從數(shù)組中隨機(jī)取 ? ?// 循環(huán)結(jié)束后,從min=5的位置取到結(jié)束,即取3個(gè)元素。 ? ?while(i-- > min) { ? ? ? ?index = Math.floor((i + 1) * Math.random()); ? ? ? ?temp = shuffled[index]; ? ? ? ?shuffled[index] = shuffled[i]; ? ? ? ?shuffled[i] = temp; ? ?} ? ?return shuffled.slice(min); }; module.exports = ArrayUtils;
四、數(shù)據(jù)校驗(yàn)測(cè)試
1. Game_mgr.js掛載到Canvas節(jié)點(diǎn)上
var MineField = require("MineField");cc.Class({ ? ?extends: cc.Component, ? ?properties: { ? ? ? ?row : 9, ? ? ? ?col : 9, ? ? ? ?mineNum : 10, ? ?}, ? ?onLoad () { ? ? ? ?// 橫豎9個(gè)單元格,共10顆雷 ? ? ? ?this.mineField = new MineField(this.row, this.col, this.mineNum); ? ? ? ?console.log(this.mineField); ? ?}, });
掛載到Canvas節(jié)點(diǎn)上,運(yùn)行測(cè)試結(jié)果如下:



2.優(yōu)化測(cè)試-驗(yàn)證數(shù)據(jù)正確與否
發(fā)現(xiàn)顯示結(jié)果不便于核實(shí)數(shù)據(jù)是否正確,我們優(yōu)化下,在MineField中添加printResult方法:
// 【4】提供打印測(cè)試的方法,便于觀察數(shù)據(jù)是否正確 MineField.prototype.printResult = function(){ ? ?for(let i=0; i<this.rowNum; i++){ ? ? ? ?let line = "| "; ? ? ? ?for(let j=0; j<this.colNum; j++){ ? ? ? ? ? ?let cell = this.cells[i*this.colNum + j]; ? ? ? ? ? ?line = line.concat(cell.info + " | "); ? ? ? ?} ? ? ? ?console.log(line); ? ?} };
為了打印時(shí)能夠上下對(duì)齊,我們將MineField.js代碼中原有""(空字符串)替換成" "(空格)。
然后,將Game_mgr.js中的代碼做如下調(diào)整:
//console.log(this.mineField); this.mineField.printResult();
運(yùn)行,瀏覽器console窗口如下:正確!


五、數(shù)據(jù)與視圖綁定
新建一個(gè)空節(jié)點(diǎn)MineField作為雷場(chǎng),將res中的block拖到MineField內(nèi),作為地磚,在block節(jié)點(diǎn)內(nèi)新建空節(jié)點(diǎn)around_bombs,在此節(jié)點(diǎn)上添加Label組件,用于顯示此地磚的信息info。之后將block做成預(yù)制體,便于動(dòng)態(tài)生成雷場(chǎng)所有地磚。
動(dòng)態(tài)生成的過(guò)程中,將每個(gè)地磚跟MineField的cells數(shù)組中的元素綁定。
在Game_mgr.js的properties中添加屬性,同時(shí)通過(guò)編輯器綁定屬性值:
// 地磚預(yù)制體、和根節(jié)點(diǎn) block_prefab : {type:cc.Prefab, default:null,}, block_root : {type:cc.Node, default:null,},
在Game_mgr.js的onLoad方法中添加如下代碼:
// 初始化游戲界面 this.showMineField();
在Game_mgr.js中增加showMineField實(shí)現(xiàn):
// 顯示雷場(chǎng)格子 showMineField(){ ? ?// 獲取地磚預(yù)制體的寬度 ? ?var block_width = this.block_prefab.data.width; ? ?// 計(jì)算第一個(gè)格子相對(duì)于中心錨點(diǎn)的偏移量 ? ?var x_offset = - block_width * this.col/2; ? ?var y_offset = block_width * this.row/2; ? ?// block的錨點(diǎn)也在中心,而不是左下角,故初始偏移量要往右上角移動(dòng) ? ?x_offset += block_width/2; ? ?y_offset += block_width*2; ? ? ?// 稍微往上移點(diǎn) ? ?for(var i=0; i<this.row; i++){ ? ? ? ?for(var j=0; j<this.col; j++){ ? ? ? ? ? ?var block = cc.instantiate(this.block_prefab); ? ? ? ? ? ?// 【*】將每個(gè)地磚跟MineField的cells數(shù)組中的元素綁定 ? ? ? ? ? ?var index = this.mineField.getIndexByXY(i,j); ? ? ? ? ? ?block.cell = this.mineField.cells[index]; ? ? ? ? ? ?this.block_root.addChild(block); ? ? ? ? ? ?// 注意:i是行,j是列,當(dāng)然行列數(shù)相等是不會(huì)有影響, ? ? ? ? ? ?// 【*】行列不等時(shí)會(huì)影響后續(xù)邊界判斷邏輯 ? ? ? ? ? ?block.setPosition(j*block_width+x_offset, y_offset-i*block_width); ? ? ? ? ? ?console.log("block[",i,j,"]=",block.cell.toString()); ? ? ? ?} ? ?} },
在Cell.js中增加toString方法顯示對(duì)象信息:
Cell.prototype.toString = function () { ? ?return "{ x : " + this.x + ", y : " + this.y + ", info : " + this.info + " }"; }
編譯運(yùn)行,結(jié)果如下:

將信息顯示到地磚上:
block.cell = this.mineField.cells[index]; // 顯示地磚內(nèi)部信息 this.showBlockInnerInfo(block);
信息顯示到地磚上的實(shí)現(xiàn)方法(便于后續(xù)觸摸調(diào)用):
// 顯示地磚內(nèi)部信息 showBlockInnerInfo(block){ ? ?block.getChildByName("around_bombs").getComponent(cc.Label).string = block.cell.info; },
編譯運(yùn)行結(jié)果如下:

仔細(xì)思考,發(fā)現(xiàn)剛才Game_mgr.js其實(shí)就是控制MineField這個(gè)節(jié)點(diǎn)的,故我們將其修改為MineField_Ctrl.js。將Canvas上的用戶自定義組件remove,在MineField節(jié)點(diǎn)上添加MineField_Ctrl組件,將其中block_root屬性去掉,將代碼中this.block_root替換為this.node。
關(guān)于掃雷的分享就到這里,更多課程和咨詢請(qǐng)點(diǎn)擊下方鏈接或加入主頁(yè)群聊:
https://bycwedu.vipwan.cn/promotion_channels/630597732