一、 扫雷游戏实现核心思路解析
数据和视图尽量分离。采用面向对象的实现设计数据模块。格子作为一类对象,雷场作为一类对象,雷场由格子构成。
二、 扫雷游戏核心数据模块
1. Cell.js单元格类
// 单个单元格,用于保存数据
// x,y,坐标,或者行和列,
// info显示文本,空表示什么都没有,周边也没有地雷// *表示此处是一颗地雷,数字表示其周边有几颗地雷function Cell(x, y, info){
this.x = x;
this.y = y;
this.info = info;
};
module.exports = Cell;
2. MineField.js雷场类
// 雷场(行数,列数,地雷数)
function MineField(rowNum, colNum, mineNum){
this.rowNum = rowNum;
this.colNum = colNum;
this.mineNum = mineNum;
// 调用【1】画格子
this.init();
// 调用【2】藏地雷
this.hideMine();
// 调用【3】留暗号
this.markNumber();
};
module.exports = MineField;
【1】画格子
雷场由Cell的对象构成的数组组成,实质就是给雷场的cells数组赋值。
// 【1】利用原型扩展方法:初始化雷场的所有格子数据
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, "");
}
}
};
补充提炼通过坐标获取格子索引的方法,以供后续其它地方用:
// 【1-1】通过坐标获取单元格的所处格子的索引
MineField.prototype.getIndexByXY = function(x, y){
return x * this.colNum + y;
}
// 通过单元格对象获取单元格所处格子的索引
MineField.prototype.getIndexByCell = function(cell){
return this.getIndexByXY(cell.x, cell.y);
}
在此文件头部引入Cell类:
var Cell = require("Cell");
【2】藏地雷
实质就是修改雷场的cells数组中的随机一些索引的cell的info属性值。
// 【2】藏地雷:将地雷数据设置到this.cells中的Cell的info中去
MineField.prototype.hideMine = function(){
// 随机无重复元素的数组,且范围限定在[0,this.rowNum*this.colNum);
let end = this.colNum * this.rowNum;
// 记录所有地雷所在的cells的索引
this.mineIndexs = ArrayUtils.randChoiseFromTo(0, end, this.mineNum);
console.log("地雷位置序号:",this.mineIndexs);
// 找到相应格子的位置,设置其cell对象的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数组工具类。
在此文件头部引入数组工具类:
var ArrayUtils = require("ArrayUtils");
【3】留暗号
实质就是在地雷周边8个格子中标上数字,数值为此单元格周边8个单元格中雷的数量。如下图所示:
// 【3】留暗号:标记地雷周围所有单元格的数字,也就是设置其info属性
MineField.prototype.markNumber = function(){
// 遍历所有地雷单元格,每次找到其周边非雷格子,给其数字加1
console.log("this.mineCells:",this.mineCells)
for(let i=0,len=this.mineCells.length; i<len; i++){
// 【3-1】拿到地雷单元格 周围的所有的非雷单元格(应该是数字的单元格)
let numberCells = this.getNumberCellsAround(this.mineCells[i]);
// 【3-2】更新地雷周围所有非雷(数字)单元格的数字标记
this.updateNumberMarks(numberCells);
}
};
【3-1】获取某颗地雷周围所有单元格
如下图所示,假如要获取(0,0)周围8个单元格,则偏移量就是其周边8个单元格的坐标:
同时,偏移后,我们还要判断这个格子是否超出雷场。即便宜后x、y值不能小于0,且不能大于行或列的最大值。
偏移量offset和判断是否超出雷场区域的方法如下:
// 【3-1-1】周围8个坐标相对于中心坐标(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】判断坐标为x,y的单元格cell是否超出了区域
MineField.prototype.outOfFiled = function(x, y){
return x<0 || x>=this.rowNum || y<0 || y>=this.colNum;
};
如果地雷周围的单元格是数字,则我们不需要计算数值,应排除。
// 【3-1】拿到cell周围的所有的非雷单元格(应该是数字的单元格)
MineField.prototype.getNumberCellsAround = function(cell){
let result = [];
for(let i=0,len=offset.length; i<len; i++){
// 【3-1-1】得到相对于cell偏移后的x、y坐标
let x = cell.x + offset[i].x;
let y = cell.y + offset[i].y
// 【3-1-2】判断坐标为x,y的单元格cell是否超出了区域
if(this.outOfFiled(x, y)){
continue;
}
// 如果是地雷继续下一次循环
let index = this.getIndexByXY(x,y); //x*this.colNum + y;
let cellSide = this.cells[index];
if (cellSide.info === "*"){
continue;
}
// 如果没有超出雷场区域,且为非雷单元格,则添加到数组中
result.push(new Cell(x, y, ""));
}
return result;
};
【3-2】更新所有地雷周围所有非雷(数字)单元格的数字标记
// 【3-2】更新地雷周围所有非雷(数字)单元格的数字标记
MineField.prototype.updateNumberMarks = function(numberCells){
/**
* 设置逻辑:①如果原来info属性为*,不需要设置,【已经排除了】
* ②如果原来info属性为"",证明是第一次标记,标记info为1
* ③如果原来属性不为*,也不为空,则在原有值基础上加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数组工具类(直接使用)
// 数组工具类
var ArrayUtils = function(){};
// 【1】初始化得到有序元素数组:从[start,end)的自然数序列
ArrayUtils.initOrderArray = function(start, end){
let sortArray = [];
for(let i= start; i<end; i++){
sortArray.push(i);
}
return sortArray;
};
// 【2】从数组arr中随机抽取count个元素,返回数组
ArrayUtils.randChoiseFromArr = function(arr, count){
let result = arr;
// 随机排序,打乱顺序
result.sort(function(){
return 0.5 - Math.random();
});
// 返回打乱顺序后的数组中的前count个元素
return result.slice(0,count);
};
// 【3】从从start到end中的连续整数中随机抽取count个数字
ArrayUtils.randChoiseFromTo = function(start, end, count){
let arr = this.initOrderArray(start,end);
return this.randChoiseFromArr(arr, count);
};
// 【2】-【方式二】从数组arr中随机抽取count个元素,返回数组
ArrayUtils.getRandomArrayElements = function(arr, count) {
// 从0位置取到结束位置存入shffled数组
let shuffled = arr.slice(0);
let i = arr.length;
let min = i - count;
let temp = 0;
let index = 0;
// 随机一个位置的元素和最后一个元素交换
// 随机一个位置元素和倒数第二个元素交换
// 假设i=8,count=3,则min=5,
// 循环体中[i]=7,6,5,也就是说最后三个元素要从数组中随机取
// 循环结束后,从min=5的位置取到结束,即取3个元素。
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;
四、 数据校验测试
1. Game_mgr.js挂载到Canvas节点上
var MineField = require("MineField");cc.Class({
extends: cc.Component,
properties: {
row : 9,
col : 9,
mineNum : 10,
},
onLoad () {
// 横竖9个单元格,共10颗雷
this.mineField = new MineField(this.row, this.col, this.mineNum); console.log(this.mineField); },
});
挂载到Canvas节点上,运行测试结果如下:
3. 优化测试-验证数据正确与否
发现显示结果不便于核实数据是否正确,我们优化下,在MineField中添加printResult方法:
// 【4】提供打印测试的方法,便于观察数据是否正确
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);
}
};
为了打印时能够上下对齐,我们将MineField.js代码中原有""(空字符串)替换成" "(空格)。
然后,将Game_mgr.js中的代码做如下调整:
//console.log(this.mineField);
this.mineField.printResult();
运行,浏览器console窗口如下:正确!
五、 数据与视图绑定
新建一个空节点MineField作为雷场,将res中的block拖到MineField内,作为地砖,在block节点内新建空节点around_bombs,在此节点上添加Label组件,用于显示此地砖的信息info。之后将block做成预制体,便于动态生成雷场所有地砖。
动态生成的过程中,将每个地砖跟MineField的cells数组中的元素绑定。
在Game_mgr.js的properties中添加属性,同时通过编辑器绑定属性值:
// 地砖预制体、和根节点
block_prefab : {type:cc.Prefab, default:null,},
block_root : {type:cc.Node, default:null,},
在Game_mgr.js的onLoad方法中添加如下代码:
// 初始化游戏界面
this.showMineField();
在Game_mgr.js中增加showMineField实现:
// 显示雷场格子
showMineField(){
// 获取地砖预制体的宽度
var block_width = this.block_prefab.data.width;
// 计算第一个格子相对于中心锚点的偏移量
var x_offset = - block_width * this.col/2;
var y_offset = block_width * this.row/2;
// block的锚点也在中心,而不是左下角,故初始偏移量要往右上角移动
x_offset += block_width/2;
y_offset += block_width*2; // 稍微往上移点
for(var i=0; i<this.row; i++){
for(var j=0; j<this.col; j++){
var block = cc.instantiate(this.block_prefab);
// 【*】将每个地砖跟MineField的cells数组中的元素绑定
var index = this.mineField.getIndexByXY(i,j);
block.cell = this.mineField.cells[index];
this.block_root.addChild(block);
// 注意:i是行,j是列,当然行列数相等是不会有影响,
// 【*】行列不等时会影响后续边界判断逻辑
block.setPosition(j*block_width+x_offset, y_offset-i*block_width);
console.log("block[",i,j,"]=",block.cell.toString());
}
}
},
在Cell.js中增加toString方法显示对象信息:
Cell.prototype.toString = function () {
return "{ x : " + this.x + ", y : " + this.y + ", info : " + this.info + " }";
}
编译运行,结果如下:
将信息显示到地砖上:
block.cell = this.mineField.cells[index];
// 显示地砖内部信息
this.showBlockInnerInfo(block);
信息显示到地砖上的实现方法(便于后续触摸调用):
// 显示地砖内部信息
showBlockInnerInfo(block){
block.getChildByName("around_bombs").getComponent(cc.Label).string = block.cell.info;
},
编译运行结果如下:
仔细思考,发现刚才Game_mgr.js其实就是控制MineField这个节点的,故我们将其修改为MineField_Ctrl.js。将Canvas上的用户自定义组件remove,在MineField节点上添加MineField_Ctrl组件,将其中block_root属性去掉,将代码中this.block_root替换为this.node。
给大家推荐个学习交流群 点击链接即可加入群链接