扫雷






4.97/5 (20投票s)
如何用一套代码创建适用于三个平台的扫雷游戏?

引言
扫雷是一款经典的 Microsoft Windows 操作系统游戏。游戏的目标是在不引爆地雷的情况下清除一个抽象的地雷阵。这款游戏已被移植到多个平台。在本文中,我们将向您展示如何为智能手机创建这款游戏。由于我们是跨平台开发这款游戏,因此可以在 Apple iOS、Android 和 Bada 设备上玩。
背景
为了克服平台差异,我们将使用 Moscrif SDK。由于其原生语言使用面向对象的 JavaScript,因此开发过程简单、快速且经济高效。支持的平台包括 iOS、Android 和 Bada。
用户界面
用户界面非常简单。它只包含一个包含地雷的表格、一个计时器、一个地雷计数器和一个菜单。当用户输掉或赢得游戏后,菜单会下拉并显示结束消息和时间。整个用户界面均使用矢量图形创建。使用矢量图形的主要优点是它们在调整大小时能保持所有细节,并且尺寸更小。
地雷表格
地雷表格有九行九列,包含十个地雷。这与 Microsoft Windows 版本游戏的初学者级别类似。表格中的每个单元格都有几种不同的外观
- 正常未覆盖单元格
- 带有数字的未覆盖单元格
- 带有地雷的未覆盖单元格
- 周围单元格中地雷数量为零的未覆盖单元格
- 标记的单元格
地雷爆炸
当用户找到地雷时,表格中的所有地雷都会爆炸。爆炸效果由大约 40 帧组成,以创建真正流畅的动画
图片:爆炸帧

开发过程
我们使用 Moscrif 游戏框架来创建这款游戏。我们游戏的主要部分是 gameScene,它用于放置地雷表格,并包含用于菜单按钮的 menu layer。
表格单元格
表格中的所有单元格都创建为 MineCell 类的实例。该类有一个 state 属性,该属性表示相邻单元格中有多少地雷,否则标签为 -1,表示该特定单元格包含地雷。单元格在不同情况下(覆盖单元格、未覆盖单元格、标记单元格等)有许多不同的外观。每种不同情况的外观由单独的函数绘制
示例:选择合适的绘制方法
// draws the cell
function draw(canvas)
{
canvas.save();
canvas.translate(this.x, this.y);
canvas.scale(0.95, 0.95);
if (this._uncovered == false)
if(this._mark)
this._drawMarked(canvas);
else
this._drawCovered(canvas);
else
this._drawUncovered(canvas);
canvas.restore();
}
覆盖单元格和标记单元格的绘制方法非常简单。它们只绘制带有渐变的圆角矩形,并在其中绘制矢量图形(用于标记单元格)。
示例:绘制标记单元格
function _drawMarked(canvas)
{
canvas.drawRoundRect(this.width / -2, this.width / -2, this.width / 2, this.width / 2, this.width / 10, this.width / 10, res.paints.markedCell);
canvas.drawPath(res.vectors.flag, res.paints.flag);
}
绘制未覆盖单元格稍微复杂一些。如果单元格周围有地雷,则仅在地雷覆盖单元格相同的背景上绘制地雷数量。此方法还可以绘制地雷爆炸的动画帧。如果当前帧大于零,则会绘制该帧。所有动画帧都存储在资源中的 explosion 数组中。
示例:绘制未覆盖单元格
function _drawUncovered(canvas)
{
// draw bacground
if (this._state > 0)
canvas.drawRoundRect(this.width / -2, this.width / -2, this.width / 2, this.width / 2, this.width / 10, this.width / 10, res.paints.coveredCell);
else
canvas.drawRoundRect(this.width / -2, this.width / -2, this.width / 2, this.width / 2, this.width / 10, this.width / 10, res.paints.uncoveredCell);
// draw animation if the cell is animated
if (this.frame > 0) {
canvas.drawBitmapNine(res.images.explosion[this.frame], this.width / -2, this.width / -2, this.width / 2, this.width / 2);
return;
}
// draw text or mine icon
switch (this._state)
{
case _STATE_MINE:
res.paints.text.color = 0xffffffff;
canvas.drawPath(res.vectors.mine, res.paints.text);
break;
case 0:
break;
case 1:
res.paints.text.color = 0xff7DFAFF;
canvas.drawText("1", this.textDimensions[0].w / -2, this.textDimensions[0].h / 2, res.paints.text);
break;
case 2:
res.paints.text.color = 0xff6EFF5B;
canvas.drawText("2", this.textDimensions[1].w / -2, this.textDimensions[0].h / 2, res.paints.text);
break;
case 3:
res.paints.text.color = 0xffFFC300;
canvas.drawText("3", this.textDimensions[2].w / -2, this.textDimensions[0].h / 2, res.paints.text);
break;
default:
res.paints.text.color = 0xffFFC300;
canvas.drawText(this._state.toString(), this.textDimensions[3].w/ -2, this.textDimensions[0].h / 2, res.paints.text);
break;
}
}
地雷爆炸
地雷爆炸是水下爆炸的简单动画。它包含 39 帧,在 _drawUncovered 方法中绘制到表格单元格中。爆炸由 explode 函数启动。该函数启动一个计时器,该计时器以固定间隔增加帧数。
爆炸:开始爆炸
// start explosion
function explode(delay = 0)
{
this.timer = new Timer(res.integers.explosionDuration / res.images.explosion.length, res.images.explosion.length);
this.timer.onTick = function()
{
// increase frame number
if (this super.frame < res.images.explosion.length-1) {
this super._uncovered = true;
this super.frame += 1;
} else {
// zero timer variable after last tiper repetition (after dimer dispose)
this super.timer = 0;
}
}
this.timer.start(delay);
}
用户事件
地雷单元格对两个用户事件做出反应
- pointerPressed -> 用户触摸屏幕时调用
- pointerReleased -> 用户从屏幕上抬起手指时调用。
这些事件会触发以下三种反应
- 标记单元格 -> 当用户按住单元格的时间间隔为 400 毫秒时
- 取消标记单元格 -> 当用户点击或按住已标记的单元格 400 毫秒时
- 揭开单元格 -> 当用户点击未标记的单元格时
指针按下
当用户点击某个单元格时,计时器会启动以检查该单元格是否为长按。400 毫秒后,单元格将被标记或取消标记。
示例:启动计时器
function pointerPressed(x, y)
{
super.pointerPressed(x, y);
// do nothing if user press uncovered cell or game is paused
if(this._uncovered || this.scene.paused)
return;
// start timer
this._timer = new Timer(1, false);
this._timer.onTick = function()
{
var self = this super;
if(!self._mark) {
self._mark = true;
this super.scene.counter.count --;
} else {
self._mark = false;
this super.scene.counter.count ++;
}
self._timer = null;
}
this._timer.start(res.integers.timeInterval);
}
指针释放
如果此事件在 pointer pressed 事件启动的计时器结束之前调用,则表示用户触摸单元格的时间不到 400 毫秒(短按)。在这种情况下,单元格将被取消标记或揭开。
pointer released 在 MineCell 类对象内部调用。但是,需要让游戏场景知道单元格已被揭开,以便例如:如果用户找到了地雷,则开始所有地雷的爆炸;或者如果用户找到了周围没有地雷的单元格,则揭开更多单元格。为了通知游戏场景单元格已被揭开,我们使用了 open 函数。
示例:指针释放
function pointerReleased(x, y)
{
super.pointerReleased(x, y);
// do nothing if user press uncovered cell or game is paused
if(this._uncovered || this.scene.paused)
return;
// dispose timer
if (this._timer != null) {
this._timer.dispose();
this._timer = null;
// unmark or uncover cell
if (this._mark) {
this._mark = false;
this.scene.counter.count ++;
} else {
this._uncovered = true;
if(this._state == 0) {
// if _state is 0 we need to uncover larger area (all 0 in surounding cells). it manages uncover function
this.uncover(this.row, this.column);
} else {
// if _state is _state_MINE we found mine
if(this._state == _STATE_MINE) {
// inform game scene that mine was uncovered
this.open(true);
} else
// inform game scene that cell without mine was uncovered
this.open(false);
}
}
}
}
游戏场景
游戏场景创建了地雷表格、地雷计数器、计时器和菜单。地雷随机分布在九行九列的表格中。
创建表格
表格是通过两个 for 循环创建的——一个用于行,一个用于列。
示例:创建表格
function _createTable()
{
var cellSide = res.integers.cellWidth;
// left and top border of table
var left = (System.width - 9*cellSide) / 2 + cellSide / 2;
var top = System.height - this.rows*cellSide;
// create cells
for (var i = 0; i<this.columns; i++) {
this.table[i] = new Array();
for (var q = 0; q<this.rows; q++) {
this.table[i][q] = new MineCell({
row : i,
column : q,
x : left + i*cellSide,
y : top + q*cellSide,
});
this.table[i][q].uncover = function(x,y) {this super._uncoverNull(x,y); };
this.table[i][q].open = function(mine) { this super._open(mine); };
this.table[i][q].scene = this;
this.add(this.table[i][q]);
}
}
}
地雷分布
地雷是随机分布的。因此,扫雷游戏不能保证 100% 的确定性。分布算法由两个循环组成。“for 循环”对每个地雷重复一次。在 for 循环中,我们添加了一个 do-while 循环。地雷的坐标在此循环内随机生成。如果该位置已经放置了一些地雷,则生成新的坐标。此算法可能不是现有算法中最快的,但它足够快速且简单,可以在我们的游戏中 K 使用。在最坏的情况下,随机位置的生成次数最多为(地雷数量)2 / 2。
示例:生成随机地雷位置
// deploy mines tu the table and also calculate all numbers
function _placeMines()
{
var x,y;
// calculate random positions of mines
for(var i = 0; i<this.mines; i++) {
do {
// generate x and y position
x = rand(this.columns - 1);
y = rand(this.rows - 1);
// if there is mine on generated position generate new position
} while (this.table[x][y]._state == -1);
this.table[x][y]._state = -1;
this.minesList[i] = new Array();
this.minesList[i][0] = x;
this.minesList[i][1] = y
this._addToNeighbours(x,y)
}
}
图片:生成随机地雷位置

数字计算器
单元格中的数字表示周围正方形中地雷的数量。这些数字通过 addToNeighbours 函数计算(在之前的代码中提到)。此函数的行为非常简单。它只为每个周围的单元格加一,前提是该单元格不包含地雷。此函数对每个地雷调用一次。
示例:计算数字
// add one to all neighbours of mine
function _addToNeighbours(x,y)
{
for (var i = x-1; i < x + 2; i++) {
for (var q = y-1; q < y + 2; q++) {
if (this._inTable(i,q) && !(y == q && x == i) && this.table[i][q]._state != -1)
this.table[i][q]._state += 1;;
}
}
}
揭开空白单元格
当用户揭开一个没有地雷的单元格时,整个周围没有地雷的区域将被揭开。在我们的游戏中,我们使用 _uncoverNull 函数来解决这个问题。这是一个递归函数。该函数揭开单元格,然后如果揭开的单元格的值为零,它会调用 _uncoverNull 来揭开所有周围的单元格。
示例:揭开零单元格区域
// uncover cell with no number with all null neighbours.
function _uncoverNull(r, c)
{
this.table[r][c].uncovered = true;
this._open(false);
if (this.table[r][c]._state == 0) {
for (var i = r-1; i < r + 2; i++) {
for (var q = c-1; q < c + 2; q++) {
if (this._inTable(i,q) && !this.table[i][q].uncovered && !this.table[i][q].mark)
this._uncoverNull(i, q);
}
}
}
}
Menu
菜单是作为单独的图层添加到游戏场景中的。它绘制菜单并创建菜单按钮。菜单包含开始新游戏、重新开始游戏和退出游戏(退出游戏仅在 Android 和 Bada 上显示)的按钮。当用户找到地雷或赢得游戏时,菜单会向下滚动并显示消息以及游戏时间。
图片:菜单

菜单通过动画效果上下滚动。此效果由Animator 对象创建。Animator 对象根据动画的持续时间和过渡时间调用 addSubject 函数中设置的回调函数。回调函数只有一个参数 state->,它决定了动画的当前位置(从 0-开始到 1-结束)。在此函数中,我们更改了菜单的 y 位置,从而导致其垂直移动。
示例:向下滚动菜单
function show()
{
var animator = new Animator({
transition: Animator.Transition.easeInOut,
duration: res.integers.menuAnimationDuration, // length of animation in miliseconds
});
animator.addSubject(function(state) { // state starts from 1.0 to 0.0
this super.y = res.integers.menuHeight / -4 + state * 3* res.integers.menuHeight / 4;
});
animator.onComplete = function()
{
this super.showed = true;
}
animator.play();
}
摘要
本文展示了如何为移动平台创建扫雷游戏,该游戏可以在市场上大约 90% 的智能手机上运行,只需一套代码。这个免费示例展示了如何创建基本-初学者级别的游戏,但您可以轻松改进游戏,添加更多关卡和其他您喜欢的功能,使其成为一款出色的游戏。