65.9K
CodeProject 正在变化。 阅读更多。
Home

扫雷

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (20投票s)

2012年9月11日

BSD

6分钟阅读

viewsIcon

68359

downloadIcon

4891

如何用一套代码创建适用于三个平台的扫雷游戏?

引言

扫雷是一款经典的 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% 的智能手机上运行,只需一套代码。这个免费示例展示了如何创建基本-初学者级别的游戏,但您可以轻松改进游戏,添加更多关卡和其他您喜欢的功能,使其成为一款出色的游戏。

© . All rights reserved.