吕蒙曰:士隔三月【1】,当刮目相看。所以,在下在这三月中发奋图强,花了约莫8节信息课的时间研究扫雷。呜呼,由于在下才能尚且不足,所以也就只能勉强打过中级难度的吧。不过,一边玩的同时,我还一边对扫雷这个游戏的制做方法构思了一下。所以说,本文中的算法完全是凭借自己对扫雷游戏规则的总结而自行研发出来的,倘若和MS的扫雷玩法有些出入,还望各位看官见谅。
【1】出自《孙权劝学》,原文为“士别三日”,由于在下这三个月来都不曾发表博客,所以引申到“士隔三月”,各位看官休怪休怪
以下是本次开发的游戏截图:
测试地址:http://wyh.wjjsoft.com/minesweeper/
扫雷虽然属于游戏开发初级学者研究的范畴,但是由于我是那种好高骛远的家伙,所以,虽然接触游戏开发约2年了,可是到现在才来研究扫雷,实在是惭愧惭愧。
首先,我想我们需要对扫雷游戏的算法进行一系列的研究。
这个算法嘛,顾名思义,就是说如何在场景中布雷。这个算法其实很简单,就是单一地使用随机取位来完成。不过值得注意的是,可能有些朋友和我一样,一开始认为是先把数字标好再去布雷,其实应该是先布雷,再根据布雷情况给每个格子标数字。
其实这个所谓的“智能帮扫算法”是我随便给这个算法取的一个名字,可能俗气了点,看官莫怪~什么是“智能帮扫算法”呢?首先我来对其下个定义:
当玩家扫开到某个方块后,如果四周没有雷,或者这个方块四周的雷全部被扫除了,那么这时候就需要帮助玩家把四周的方块全部扫出。这样的算法叫做智能帮扫算法
如果实现了这个算法,那么点击一个方块,扫开一大片的效果就可以实现了。某些看官可能不理解为什么,我可以在这里罗嗦一下:根据定义我们可以看出,这个算法只是帮助扫出四周的方块,不过这是一个链环的过程,毕竟四周的方块被扫开后,如果这些方块满足执行帮扫算法的条件,那么帮扫算法就会在被算法扫开的方块上生效,如此渐进下去,知道被扫开的方块不满足条件为止。如果用流程图表示就是:
如果流程图还不能理解的话,就举个Y某我吃巧克力的例子吧。Y某有一天收到M君送来的一盒巧克力,我首先拆开盒子掰下一块巧克力,放进嘴里,发现是苦瓜味的,于是我就下定决心,如果我再吃到苦瓜味的,就把周围的8块都吃了。不想我接下来吃到一块就是苦瓜味的,没有办法,只有把周围八块都吃了,结果刚吃到其中一块,发现又是苦瓜味的,于是继续吃……于是就达到了吃开一片的效果。这里的吃到苦瓜味的巧克力就相当于“四周被标记的雷数等于相应数目”。这个故事的结局和扫雷不同,由于游戏中的雷数>=1,所以无论如何都会因遇到雷停下来,可是万恶的M君送来的巧克力尽然全是苦瓜味的(T_T)
要实现这个算法,说难也不算难。只需把对算法的定义翻译成代码即可。
既然是游戏,所以要实现的不仅是算法,还有就是用户界面之类的。
由于纯canvas做游戏很麻烦,所以我这次就直接使用lufylegend游戏引擎实现界面。
引擎地址:http://lufylegend.com/lufylegend
API地址:http://lufylegend.com/lufylegend/api
本文中可能多次出现某些类和函数,或者某些函数和类很重要,所以我把它们的API地址放在下面,这样以来可以方便大家查找:
在下面的讲解中,我只讲一些关键的地方,其他地方就交给看官慢慢啃吧。文末会给出完整的代码下载。
HTML5游戏嘛,肯定要有html代码:
<!DOCTYPE html>
<html>
<head>
<title>Minesweeper</title>
<meta charset="utf-8" />
<script type="text/javascript" src="./lib/lufylegend-1.9.9.simple.min.js"></script>
<script type="text/javascript" src="./lib/lufylegend.LoadingSample1-0.1.0.min.js"></script>
<script type="text/javascript" src="./js/Main.js"></script>
</head>
<body oncontextmenu="return false;">
<div id="mylegend"></div>
</body>
</html>
LInit(1000 / 30, "mylegend", 540, 640, main);
var dataList = {};
var stage;
var blockXNum = blockYNum = 10, mineNum = 12;
function main () {
var loadData = [
{path : "./js/InfoLayer.js"},
{path : "./js/ButtonTemplate.js"},
{path : "./js/MineLayer.js"},
{path : "./js/StageLayer.js"},
{name : "bg", path : "./images/bg.jpg"},
{name : "button_sheet", path : "./images/button_sheet.png"},
{name : "face_happy", path : "./images/face_happy.png"},
{name : "face_sad", path : "./images/face_sad.png"},
{name : "face_smile", path : "./images/face_smile.png"},
{name : "face_surprise", path : "./images/face_surprise.png"},
{name : "flag", path : "./images/flag.png"},
{name : "mine", path : "./images/mine.png"}
];
var loadingLayer = new LoadingSample1();
addChild(loadingLayer);
LLoadManage.load(
loadData,
function (p) {
loadingLayer.setProgress(p);
},
function (r) {
dataList = r;
loadingLayer.remove();
initGame();
}
);
}
function initGame () {
stage = new StageLayer();
addChild(stage);
}
在Main.js中,我们初始化了界面,加载了资源,以及加入舞台类(StageLayer),主要用的是lufylegend中的一些API,不熟悉的同学可以参考前面给出的API文档。不过得注意几个变量:
这个类也比较简单,先把代码贴出来:
function StageLayer () {
var s = this;
LExtends(s, LSprite, []);
var bgBmp = new LBitmap(new LBitmapData(dataList["bg"]));
bgBmp.scaleX = LGlobal.width / bgBmp.getWidth();
bgBmp.scaleY = LGlobal.height / bgBmp.getHeight();
s.addChild(bgBmp);
s.infoLayer = new InfoLayer();
s.infoLayer.x = (LGlobal.width - s.infoLayer.getWidth()) / 2;
s.infoLayer.y = 40;
s.addChild(s.infoLayer);
s.mineLayer = null;
s.createMineLayer();
}
StageLayer.prototype.createMineLayer = function () {
var s = this;
if (s.mineLayer) {
s.mineLayer.remove();
}
s.mineLayer = new MineLayer();
s.mineLayer.x = (LGlobal.width - s.mineLayer.getWidth()) / 2;
s.mineLayer.y = s.infoLayer.y + s.infoLayer.getHeight() + 30;
s.addChild(s.mineLayer);
};
这个类是舞台类,既然是舞台,那装些显示对象就是他的义务啰~看了前面的截图大家可以发现,这个游戏中主要由“剩余雷数”,“带有face的按钮”,“用去的时间”,“扫雷区”构成。这些部件我大致分了一下类:“剩余雷数”,“带有face的按钮”,“用去的时间”属于信息层,“扫雷区”属于地雷层。这样一来就又诞生了两个类:InfoLayer,MineLayer。
这个类正如上面所说,用于放置“剩余雷数”,“带有face的按钮”,“用去的时间”这些部件。具体代码如下:
function InfoLayer () {
var s = this;
LExtends(s, LSprite, []);
s.mineLeftNumTxt = null;
s.button = null;
s.timeUsedTxt = null;
s.timeUsedNum = 0;
s.preTime = 0;
s.mineLeftNum = mineNum;
s.isStart = false;
s.addMineLeftNumLayer();
s.addButton();
s.addTimeUsedLayer();
s.addEventListener(LEvent.ENTER_FRAME, function () {
if (!s.isStart) {
return;
}
s.refreshTimeUsedNumTxt();
});
}
InfoLayer.prototype.addMineLeftNumLayer = function () {
var s = this;
var mineLeftNumLayer = new LSprite();
s.addChild(mineLeftNumLayer);
s.mineLeftNumTxt = new LTextField();
s.mineLeftNumTxt.text = 10000000;
s.mineLeftNumTxt.color = "white";
s.mineLeftNumTxt.size = 30;
mineLeftNumLayer.addChild(s.mineLeftNumTxt);
mineLeftNumLayer.graphics.drawRoundRect(
2, "white",
[
-5, -5,
s.mineLeftNumTxt.getWidth() + 10,
s.mineLeftNumTxt.getHeight() + 10,
3
],
true, "black"
);
s.mineLeftNumTxt.text = s.mineLeftNum;
};
InfoLayer.prototype.addButton = function () {
var s = this, btnBmp = new LBitmap(new LBitmapData(dataList["face_smile"]));
s.button = new ButtonTemplate(btnBmp, 1.2);
s.button.x = s.getWidth() + 50;
s.button.y = -15;
s.addChild(s.button);
s.button.addEventListener(LMouseEvent.MOUSE_UP, function () {
s.timeUsedNum = 0;
s.preTime = new Date().getTime();
s.mineLeftNum = mineNum;
s.isStart = false;
s.parent.createMineLayer();
s.refreshMineLeftNumTxt();
s.refreshTimeUsedNumTxt();
s.changeFace("smile");
})
};
InfoLayer.prototype.addTimeUsedLayer = function () {
var s = this;
var timeUsedLayer = new LSprite();
timeUsedLayer.x = s.getWidth() + 50;
s.addChild(timeUsedLayer);
s.timeUsedTxt = new LTextField();
s.timeUsedTxt.text = 10000000;
s.timeUsedTxt.color = "white";
s.timeUsedTxt.size = 30;
timeUsedLayer.addChild(s.timeUsedTxt);
timeUsedLayer.graphics.drawRoundRect(
2, "white",
[
-5, -5,
s.timeUsedTxt.getWidth() + 10,
s.timeUsedTxt.getHeight() + 10,
3
],
true, "black"
);
s.timeUsedTxt.text = s.timeUsedNum;
};
InfoLayer.prototype.changeFace = function (name) {
this.button.setContent(new LBitmap(new LBitmapData(dataList["face_" + name])));
};
InfoLayer.prototype.refreshMineLeftNumTxt = function () {
this.mineLeftNumTxt.text = this.mineLeftNum;
};
InfoLayer.prototype.refreshTimeUsedNumTxt = function (e) {
var s = this, nowTime = new Date().getTime();
s.timeUsedNum += (nowTime - s.preTime) / 1000;
s.preTime = nowTime;
s.timeUsedTxt.text = parseInt(s.timeUsedNum);
};
玩过windows xp扫雷的都知道,在“扫雷区”中点击一下鼠标,那按钮上的face就会改变,所以为了在MineLayer和InfoLayer进行交互,我在InfoLayer上加了一些用于改变剩余雷数以及更改按钮上face的函数(refreshMineLeftNumTxt和changeFace)。
这个类中用到了ButtonTemplate这个类,这个类是一个按钮类,在游戏中我们就用到了一种按钮,所以就用ButtonTemplate把这些按钮的功能统一起来。
这个按钮类出现在前面讲的InfoLayer中,还会出现在下面要讲的MineLayer中,作为方块。
我把按钮主要分为两个部分:按钮背景,按钮内容。
我们在按钮中要用到的功能主要有如下几个:
Ok,该上代码了:
function ButtonTemplate (img, btnBmpScale) {
var s = this;
LExtends(s, LSprite, []);
var btnImg = dataList["button_sheet"];
var normalBmp = new LBitmap(new LBitmapData(btnImg, 0, 0, 48, 48));
var overBmp = new LBitmap(new LBitmapData(btnImg, 0, 48, 48, 48));
var downBmp = new LBitmap(new LBitmapData(btnImg, 0, 96, 48, 48));
s.button = new LButton(normalBmp, overBmp, downBmp.clone(), downBmp.clone());
s.button.scaleX = s.button.scaleY = btnBmpScale || 1;
s.button.staticMode = true;
s.addChild(s.button);
s.content = null;
if (typeof img == UNDEFINED || !img) {
return;
}
s.setContent(img)
}
ButtonTemplate.prototype.setContent = function(content) {
var s = this;
s.removeContent();
s.content = content;
s.content.x = (s.button.getWidth() - s.content.getWidth()) / 2;
s.content.y = (s.button.getHeight() - s.content.getHeight()) / 2;
s.addChild(s.content);
};
ButtonTemplate.prototype.removeContent = function() {
var s = this;
if (s.content) {
s.content.remove();
s.content = null;
}
};
ButtonTemplate.prototype.removeButton = function() {
var s = this;
if (s.button) {
s.button.remove();
}
};
ButtonTemplate.prototype.setIntoNormalState = function () {
this.button.setState(LButton.STATE_ENABLE);
};
ButtonTemplate.prototype.setIntoOverState = function () {
this.button.setState(LButton.STATE_DISABLE);
};
上面说的按钮背景就是button属性,内容就是content。
这个类是非常重要,所以需要好好的解释其中的一些代码。先看构造器:
function MineLayer () {
var s = this;
LExtends(s, LSprite, []);
s.map = new Array();
s.waitingTime = 1;
s.startTimer = false;
s.timerIndex = 0;
s.onUpCallback = null;
s.preMouseButton = null;
s.doubleDown = false;
s.completeNum = 0;
s.create();
s.addEventListener(LEvent.ENTER_FRAME, s.loop);
}
介绍一下其中的属性,
[
[-1, 2, 1],
[1, 2, -1],
[0, 1, 1]
]
接下来来看看create函数:
MineLayer.prototype.create = function () {
var s = this, positionList = new Array();
for (var i = 0; i < blockYNum; i++) {
var row = new Array();
s.map.push(row);
for (var j = 0; j < blockXNum; j++) {
var btn = new ButtonTemplate();
btn.x = j * 48;
btn.y = i * 48;
btn.positionInMap = {x : j, y : i};
btn.isFlag = false;
btn.isSwept = false;
s.addChild(btn);
btn.addEventListener(LMouseEvent.MOUSE_DOWN, function (e) {
s.onDown(e.currentTarget, e.button);
});
btn.addEventListener(LMouseEvent.MOUSE_UP, function (e) {
s.onUp(e.currentTarget, e.button);
});
row.push(0);
positionList.push({x : j, y : i});
}
}
for (var k = 0; k < mineNum; k++) {
var mineIndex = Math.floor(Math.random() * positionList.length),
o = positionList[mineIndex];
s.map[o.y][o.x] = -1;
positionList.splice(mineIndex, 1);
}
for (var m = 0; m < blockYNum; m++) {
var row = s.map[m];
for (var n = 0; n < blockXNum; n++) {
var count = 0,
list = null;
if (row[n] == -1) {
continue;
}
list = s.findBlockAround(n, m);
for (var f = 0, ll = list.length; f < ll; f++) {
if (list[f].v == -1) {
count++;
}
}
s.map[m][n] = count;
}
}
};
create函数致力于布雷以及把每个方块标上数字,这个数字就是这个方块四周有的雷数。
这里我用到了一个很重要的函数——findBlockAround:
MineLayer.prototype.findBlockAround = function (x, y) {
var s = this,
l = blockYNum,
t = blockXNum,
di = y + 1,
ti = y - 1,
ri = x + 1,
li = x - 1,
cr = null,
rl = new Array();
if (di < l) {
cr = s.map[di];
rl.push({x : x, y : di, v : cr[x]});
if (li >= 0) {
rl.push({x : li, y : di, v : cr[li]});
}
if (ri < t) {
rl.push({x : ri, y : di, v : cr[ri]});
}
}
if (ti >= 0) {
cr = s.map[ti];
rl.push({x : x, y : ti, v : cr[x]});
if (li >= 0) {
rl.push({x : li, y : ti, v : cr[li]});
}
if (ri < t) {
rl.push({x : ri, y : ti, v : cr[ri]});
}
}
if (li >= 0) {
cr = s.map[y];
rl.push({x : li, y : y, v : cr[li]});
}
if (ri < t) {
cr = s.map[y];
rl.push({x : ri, y : y, v : cr[ri]});
}
return rl;
};
这个函数是干啥的呢?噢~原来是用来寻找某个方块附近一圈的方块。也就是左上,正上,右上,正左,左下,正下,右下,正右这几个位置的方块。由于这个功能很多地方要用,所以我把它单独封装进一个函数。
再来看鼠标事件实现部分,主要由onDown,onUp,loop这个三个函数一起合作来完成:
MineLayer.prototype.onDown = function (btn, mouseButton) {
var s = this;
s.parent.infoLayer.changeFace("surprise");
if (
s.startTimer
&& (mouseButton == 0 || mouseButton == 2)
&& mouseButton != s.preMouseButton
&& !btn.isFlag
&& btn.isSwept
) {
s.startTimer = false;
s.timerIndex = 0;
s.doubleDown = true;
s.preMouseButton = mouseButton;
if (!s.isMineAroundHasBeenSwept(btn)) {
var p = btn.positionInMap,
list = s.findBlockAround(p.x, p.y);
for (var i = 0, l = list.length; i < l; i++) {
var o = list[i], b = s.getChildAt(o.y * blockXNum + o.x)
if (!b.isFlag) {
b.setIntoOverState();
}
}
}
return;
}
s.startTimer = true;
if (mouseButton == 0) {
s.onUpCallback = function () {
s.sweepThis(btn, true);
}
} else if (mouseButton == 2) {
s.onUpCallback = function () {
s.setFlagTo(btn);
}
}
};
MineLayer.prototype.onUp = function (btn, mouseButton) {
var s = this, infoLayer = s.parent.infoLayer;
infoLayer.changeFace("smile");
if (s.doubleDown) {
var p = btn.positionInMap,
list = s.findBlockAround(p.x, p.y);
s.doubleDown = false;
s.startTimer = false;
s.preMouseButton = null;
if (s.isMineAroundHasBeenSwept(btn)) {
s.sweepBlocksAround(btn, false);
} else {
for (var i = 0, l = list.length; i < l; i++) {
var o = list[i], b = s.getChildAt(o.y * blockXNum + o.x);
if (!b.isFlag) {
b.setIntoNormalState();
}
}
}
return;
}
if (typeof s.onUpCallback == "function") {
if (!infoLayer.isStart) {
infoLayer.isStart = true;
infoLayer.preTime = new Date().getTime();
}
s.onUpCallback();
s.onUpCallback = null;
}
};
MineLayer.prototype.loop = function (e) {
var s = e.currentTarget;
if (!s.startTimer) {
return;
}
if (s.timerIndex++ > s.waitingTime) {
s.timerIndex = 0;
s.startTimer = false;
}
};
主要来讲讲实现左右两键同时按下事件的实现:
首先我们得想象一下我们左右两键同时按下时的操作,大致可以简化为两个按键中其中以个按下后,在短暂时间后,另一个按键也按下,如果其中任意一个松开,那就执行同时按下对应的代码;如果超出了短暂时间才按下另一个按键,那么我们就把鼠标松开后要执行的函数设置为最后按下的那个键对应的代码;如果压根就没第二次按下,那就直接执行第一次按下对应的代码。想到这里后,我们要做的就很明确了。短暂时间的计时是交给loop函数来完成,鼠标按下和松开就分别交给了onDown和onUp。
接下来是sweepThis和sweepBlocksAround这两个函数:
MineLayer.prototype.sweepBlocksAround = function (btn) {
var s = this,
p = btn.positionInMap,
list = s.findBlockAround(p.x, p.y);
for (var i = 0, l = list.length; i < l; i++) {
var o = list[i], b = s.getChildAt(o.y * blockXNum + o.x);
if (o.v >= 0 && !b.isSwept) {
s.sweepThis(b);
} else if (o.v == -1 && !b.isFlag) {
s.sweepThis(b);
}
}
};
MineLayer.prototype.sweepThis = function (btn) {
var s = this, p = btn.positionInMap, value = s.map[p.y][p.x];
if (btn.isSwept) {
return;
}
if (btn.isFlag) {
s.setFlagTo(btn);
}
if (value == -1) {
s.gameOver("lose");
return;
}
var contentLayer = new LSprite();
contentLayer.filters = [new LDropShadowFilter()];
contentLayer.graphics.drawRect(2, "white", [0, 0, btn.getWidth(), btn.getHeight()], true, "lightgray");
var txt = new LTextField();
txt.text = (value == 0) ? "" : value;
txt.x = (contentLayer.getWidth() - txt.getWidth()) / 2;
txt.y = (contentLayer.getHeight() - txt.getHeight()) / 2;
txt.weight = "bold";
txt.color = "white";
txt.lineColor = "#0088FF";
txt.stroke = true;
txt.lineWidth = 3;
txt.size = 18;
contentLayer.addChild(txt);
btn.isSwept = true;
btn.removeButton();
btn.setContent(contentLayer);
if (s.isMineAroundHasBeenSwept(btn)) {
s.sweepBlocksAround(btn);
}
};
sweepThis的主要功能就是把某个方块(及参数btn)给扫开。然后判断这个方块四周被标记的方块数是不是等于该方块四周的雷数,如果判断通过,就通过sweepBlockAround执行“智能帮扫算法”。这个用来判断四周被标记的方块数是不是等于该方块四周的雷数的函数就是isMineAroundHasBeenSwept:
MineLayer.prototype.isMineAroundHasBeenSwept = function (btn) {
var s = this,
p = btn.positionInMap,
count = 0,
value = s.map[p.y][p.x],
list = null;
if (value == 0) {
return true;
}
list = s.findBlockAround(p.x, p.y);
for (var i = 0, l = list.length; i < l; i++) {
var o = list[i];
if (s.getChildAt(o.y * blockXNum + o.x).isFlag) {
count++;
}
}
if (count == value) {
return true;
}
return false;
};
在sweepBlockAround中,我们要接受一个参数,这个是告诉sweepBlockAround“帮扫算法”对哪个方块起作用,及以谁为中心,向四周扫开其他方块。在这个函数中,我们首先把四周的方块获得,并放入一个数组,然后遍历这个数组,如果遍历到的按钮是>=0的,并且没有被扫开,就把它扫开;如果是=-1,及代表雷,并且又没被标记就也把它扫开,因为sweepBlockAround都是在通过isMineAroundHasBeenSwept后才调用的,所以说如果出现上面的情况,说明玩家判断失误了。
最后再来看剩余的及个肤浅易懂的函数:
MineLayer.prototype.setFlagTo = function (btn) {
var s = this,
p = btn.positionInMap;
flagBmp = null,
infoLayer = null;
if (btn.isSwept) {
return;
}
flagBmp = new LBitmap(new LBitmapData(dataList["flag"]));
infoLayer = s.parent.infoLayer;
if (btn.isFlag) {
btn.isFlag = false;
infoLayer.mineLeftNum++;
if (s.map[p.y][p.x] == -1) {
s.completeNum--;
}
btn.removeContent();
} else {
btn.isFlag = true;
infoLayer.mineLeftNum--;
if (s.map[p.y][p.x] == -1) {
s.completeNum++;
}
btn.setContent(flagBmp);
}
infoLayer.refreshMineLeftNumTxt();
if (s.completeNum == mineNum && infoLayer.mineLeftNum == 0) {
for (var i = 0; i < blockYNum; i++) {
for (var j = 0; j < blockXNum; j++) {
var b = s.getChildAt(i * blockXNum + j);
if (!b.isSwept && !b.isFlag) {
s.sweepThis(b);
}
}
}
s.gameOver("win");
}
};
MineLayer.prototype.gameOver = function (r) {
var s = this, infoLayer = s.parent.infoLayer;
for (var i = 0; i < blockYNum; i++) {
var row = s.map[i];
for (var j = 0; j < blockXNum; j++) {
var v = row[j], b = s.getChildAt(i * blockXNum + j);
b.mouseEnabled = false;
b.mouseChildren = false;
if (r == "lose" && v == -1) {
b.setContent(new LBitmap(new LBitmapData(dataList["mine"])));
infoLayer.changeFace("sad");
}
}
}
if (r == "win") {
infoLayer.changeFace("happy");
}
infoLayer.isStart = false;
};
setFlagTo就是右键标小旗功能。gameOver就是游戏结束时调用的。这两个函数都涉及了和InfoLayer的交互。
运行代码,就得到了一款扫雷游戏。
最近扫上瘾了,所以就再来几把吧!!
下载地址:http://wyh.wjjsoft.com/downloads/minesweeper.zip
转载请注明出处:Yorhom’s Game Box
原文:http://blog.csdn.net/yorhomwang/article/details/45953711