扫雷

思考并回答以下问题:

  • 请阐述扫雷开发思路。
  • 如何拓展扫雷的游戏玩法?

游戏规则

扫雷示意图

扫雷的布局

游戏区包括雷区、地雷计数器(记录剩余地雷数)和计时器(记录游戏时间)。在确定大小的矩形雷区中随机布置一定数量的地雷(初级为9×9个方块10个雷,中级为16×16个方块40个雷,高级为16×30个方块99个雷,自定义级别可以自己设定雷区大小和雷数,但是雷区大小不能超过24×30个方块),玩家需要尽快找出雷区中的所有不是地雷的方块,而不许踩到地雷。

扫雷的基本操作

游戏的基本操作包括左键单击、右键单击、左右键双击三种。其中左键用于打开安全的格子,推进游戏进度;右键用于标记地雷,以辅助判断,或为接下来的双击做准备;双击在一个数字周围的地雷标记完时,相当于对数字周围未打开的方块均进行一次左键单击操作。

左键单击:在判断出不是雷的方块上按下左键,可以打开该方块。如果方块上出现数字,则该数字表示以该方块为中心的3×3区域中的地雷数。如果方块上为空(相当于0),则可以递归地打开与空相邻的方块;如果不幸触雷,则游戏结束。

图1表示在游戏区中间/边缘/角落点击方块时数字显示地雷数的范围,红点所在处为鼠标左键单击方块。

图1 范围示意图

右键单击:在判断为地雷的方块上按下右键,可以标记地雷(显示为小红旗)。重复一次或两次操作可取消标记(如果在游戏菜单中勾选了“标记(?)”,则需要两次操作来取消标雷)。

左右键双击:同时按下左键和右键完成左右键双击。当双击位置周围已标记雷数等于该位置数字时操作有效,相当于对该数字周围未打开的方块均进行一次左键单击操作。地雷未标记完全时使用双击无效。若数字周围有标错的地雷,则游戏结束,标错的地雷上会显示一个“×”。

游戏结束

当一个地雷被踩中时,所有地雷都将显示,游戏失败;玩家猜出所有地雷,游戏成功。

程序思路

雷区绘制

用于绘制整个雷区,我们可以利用二维数组记录每个方块的位置信息,便于接下来对方块的状态和信息进行存储跟踪。每一个方块都有两个属性:布尔值isMine,用于标记当前方块是否有雷,初始值为false;整型变量NearByMines,用于记录周围的地雷数,初始值为0。

建立雷区后,我们会通过随机数选择其中的某些方块,将它们的isMine值更改为true,同时将与地雷方块相邻的方块的NearByMines值加1。

例如建立以下雷区后,给[1][0]和[1][2]这两个方块赋予地雷,则下列雷区内方块的值将会改变。

左键单击

我们都知道扫雷有三种点击方式,其中左键单击中的算法最为核心。这里需要分两种基本情况讨论。

1.单击到的方块没有雷。

这是扫雷中最核心的部分,这里举一个简单的例子来更好的帮助理解:在用户单击雷区中的方块[1][2]时,我们需要通过递归遍历算法和计数器,检测其周围八个方块([0][1],[0][2],[0][3],[1][1],[1][3],[2][1],[2][2],[2][3])是否有地雷,有几个地雷。当然,在检测前,我们还需要判断这八个方块是否超过雷区边界,超过雷区边界的方块则不需要检测(如点击方块[0][0]时只需遍历方块[0][1],[1][0],[1][1]即可)。

当我们点击到的方块周围八个方块均没有地雷时,则需分别以其周围八个方块为中心继续遍历,直到接近一个附近有地雷的方块后递归停止。其实这段算法用到的就是一个递归函数。

1
2
3
4
5
6
7
8
9
10
11
遍历函数(方块坐标,是否遍历)
{
If(方块在雷区范围内)
{
  If(周围有地雷)
遍历结束;
Else
//这里需要写八个函数语句,对周围八个方块各自都进行遍历
遍历函数(方块坐标,遍历);
}
}

2.单击到的方块存在雷

在用户不慎点击到地雷,我们需要用遍历算法对雷区中所有的方块进行判断,显示地雷,此时游戏结束。

右键单击

对于右键单击,处理起来就比左键单击容易一点了。我们只需获取当前点击的方块数组下标,将其显示的图片加载为红旗即可。当方块显示状态为红旗时,鼠标左键需被禁用,只有再次右键单击取消该状态的时候鼠标左键才能被取消禁用。

左右键双击

左右键双击需判断已经打开的方块上的数字X与X周围八块方块中被标记的方块总数是否相等。若相等,则在X上双击后可打开X周围八个方块中未被标记且未被点击的方块。

1
2
3
4
5
6
7
8
If(方块数字 == 周围标记总数)
{
  遍历周围八个方块
  If(标记错误)
       遍历加载所有地雷,游戏结束
Else
       翻开未翻开的方块
}

游戏结束

如果在玩家点击过程中点到地雷,则通过遍历加载所有地雷,玩家失败,游戏结束。

如果玩家成功,需通过遍历确定所有翻开的方块均无雷,游戏结束,玩家胜利。

游戏流程图

程序实现

点击下载扫雷资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 场景初始化
public class init_script : MonoBehaviour
{
// 定义方块,方块大小,以及雷区的行列数
public Transform tile;
int tileSize = 32;
const int tilesAcross = 10;
const int tilesDown = 10;

// 布置雷区,实例化方块
void Start()
{
print("start");

// 数据清理
PlayerPrefs.DeleteAll();

// 生成雷区
for(int y = 0; y < tilesAcross; y++)
{
for(int x = 0;x < tilesDown; x++)
{
// 实例化方块
Transform newTile = (Transform)Instantiate(tile, new Vector3(x * tileSize, y * tileSize, 0), Quaternion.identity);
}
}

print("over");
}

void Update()
{

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 单个方块的管理
public class tile_script : MonoBehaviour
{
// 方块的三种状态,未显示、显示、标旗
public enum State
{
Unrevealed,
Revealed,
Flag
}

// 方块的初始状态是未显示状态
State state = State.Unrevealed;

SpriteRenderer renderPhoto;

public bool mine;
public bool mouseOver;
public int x,y;

public void Reset()
{
state = State.Unrevealed;
mine = false;
mouseOver = false;

if (renderPhoto != null)
{
renderPhoto.sprite = Photo.get().unrevealed;
}
}

public void SetPosition(int x, int y)
{
this.x = x;
this.y = y;
}

public void MakeMine()
{
mine = true;
}

void OnMouseEnter()
{
mouseOver = true;
}

void OnMouseExit()
{
mouseOver = false;
}
}
1
2
3
4
5
6
7
public init_script : MonoBehaviour
{
...
public tile_script[,] grid = new tile_script[tilesAcross, tilesDown];
public List<tile_script> Mines = new List<tile_script>();
...
}
0%