[Unity] 使用Unity 2D实现经典的扫雷游戏(下)

查看:695 |回复:1 | 2019-11-14 18:02:06

您需要 登录 才可以下载或查看,没有账号?注册

x
本帖最后由 kanisen 于 2019-11-14 17:20 编辑

我们将来实现整个扫雷游戏


u=886208527,3948948419&fm=26&gp=0.jpg



在使用
Unity 2D实现经典的扫雷游戏上篇中,我们分享了如何创建项目,游戏中的元素以及完成了第一个版本的编码。今天下篇,我们将来实现整个扫雷游戏。

创建类

网格将给予我们辅助,它用于访问所有元素,处理更加复杂的游戏逻辑。例如:计算某个特定元素的邻接地雷数量,或是显示整个无雷元素区域。

我们现在创建一个新的C#脚本,命名为:Grid。


  1. using UnityEngine;
  2. using System.Collections;

  3. public class Grid : MonoBehaviour {
  4.     //初始化
  5.     void Start () {
  6.     }

  7.     //每帧调用一次Update

  8.     void Update () {
  9.     }
  10. }
点击此处复制文本


脚本不必是附加到一个游戏对象上的类型,所以我们移除MonoBehaviour定义,以及Start和Update函数。

  1. using UnityEngine;
  2. using System.Collections;

  3. public class Grid {

  4. }
点击此处复制文本


元素二维数组

网格需要跟踪游戏中的每一个元素。我们可以使用一个二维数组,也称为矩阵来实现。

下面的代码会创建一个宽度为10,高度为13的新的二维数组,或者说:10*13个元素。如果我们要访问位于x=0,y=1的元素,可以写成elements[0,1]。


  1. using UnityEngine;
  2. using System.Collections;

  3. public class Grid {
  4.     // 网格本身
  5.     public static int w = 10; // 这是宽度
  6.     public static int h = 13; //这是高度
  7.     public static Element[,] elements = new Element[w, h];
  8. }
点击此处复制文本



在网格中注册

让我们快速切换到Element脚本,修改Start函数,以便每个元素能将自己自动注册到网格。


  1. //初始化
  2. void Start () {
  3.     //随机决定它是否是一颗地雷

  4.     mine = Random.value < 0.15;

  5.     // 注册到网格
  6.     int x = (int)transform.position.x;
  7.     int y = (int)transform.position.y;
  8.     Grid.elements[x, y] = this;
  9. }
点击此处复制文本



transform.position的x和y坐标类型是float,因此我们必须在使用之前将它们转换为int。this值是元素本身的引用。

显示所有地雷

现在返回到我们的Grid类,实现显示所有地雷的函数。这非常简单,因为我们只需要遍历每个元素,为标记为地雷的元素加载地雷纹理。


  1. //显示所有地雷
  2. public static void uncoverMines() {
  3.     foreach (Element elem in elements)
  4.         if (elem.mine)
  5.             elem.loadTexture(0);
  6. }
点击此处复制文本



我们只需简单的检查每个元素的mine变量,并为相应元素使用loadTexture函数。loadTexture函数需要输入邻接地雷数量,但这对本身是地雷的元素而言并不重要,所以我们使用0就可以了。函数是公共和静态的,因为我们希望能在所有地方都能使用它,而不仅仅是在Grid类之内。

点击Element脚本,修改下OnMouseUpAsButton函数,以便当用户点击一个地雷时,它会使用我们刚创建的uncoverMines函数。


  1. void OnMouseUpAsButton() {
  2.     // 这是个地雷
  3.     if (mine) {
  4.         // 显示所有地雷
  5.         Grid.uncoverMines();

  6.         //游戏结束
  7.         print("you lose");
  8.     }


  9.     //这不是个地雷
  10.     else {
  11.         //显示邻接地雷数量
  12.         //loadTexture(...);
  13.         // 显示无雷区域
  14.         // ...
  15.         //判断游戏是否已获胜
  16.         // ...
  17.     }
  18. }
点击此处复制文本



如果我们按下运行,并单击元素直至触雷,我们就能看到其它所有的雷也都被同时显示了。



计算邻接地雷数量

现在我们将向Grid类添加另一个函数。给定一个位于x,y的元素,这个函数将能计算出其邻接地雷的数量。这听起来有点复杂,但函数最后仅仅是查看了8个周围的元素(上、右、右上、右下、左、左上、左下、下),碰到一个地雷元素就为计数器加1。

所以我们首先要为Grid类添加一个小小的辅助函数。这个函数负责检测某个特定位置是否是地雷。


  1. //判断给定坐标处是否是地雷
  2. public static bool mineAt(int x, int y) {
  3.     //坐标是否在范围内?然后检测是否是地雷。
  4.     if (x >= 0 && y >= 0 && x < w && y < h)
  5.         return elements[x, y].mine;
  6.     return false;
  7. }
点击此处复制文本



我们必须检查坐标是否在elements数组的范围内,防止出现elements[-1,-1]这样会产生错误的访问。

现在我们可以创建实际的adjacentMines函数,以x和y坐标为参数,以counter为返回值。


  1. //计算一个元素的邻接地雷数
  2. public static int adjacentMines(int x, int y) {
  3.     int count = 0;
  4.     //计算邻接地雷
  5.     // ...
  6.     return count;
  7. }
点击此处复制文本



此后我们需要检查所有相邻的元素。


  1. //计算一个元素的邻接地雷数
  2. public static int adjacentMines(int x, int y) {
  3.     int count = 0;

  4.     if (mineAt(x,   y+1)) ++count; // 上
  5.     if (mineAt(x+1, y+1)) ++count; // 右上
  6.     if (mineAt(x+1, y  )) ++count; // 右
  7.     if (mineAt(x+1, y-1)) ++count; //右下
  8.     if (mineAt(x,   y-1)) ++count; // 下
  9.     if (mineAt(x-1, y-1)) ++count; //左下
  10.     if (mineAt(x-1, y  )) ++count; // 左
  11.     if (mineAt(x-1, y+1)) ++count; // 左上
  12.     return count;
  13. }
点击此处复制文本



让我们返回到Element脚本,再次修改OnMouseUpAsButton函数。


  1. void OnMouseUpAsButton() {
  2.     //这是个地雷
  3.     if (mine) {
  4.         // 显示所有地雷
  5.         Grid.uncoverMines();
  6.         //游戏结束
  7.         print("you lose");
  8.     }


  9.     //这不是个地雷
  10.     else {
  11.         // 显示邻接地雷数
  12.         int x = (int)transform.position.x;
  13.         int y = (int)transform.position.y;
  14.         loadTexture(Grid.adjacentMines(x, y));

  15.         //显示所有无雷区域
  16.         // ...
  17.         //判断游戏是否已获胜
  18.         // ...
  19.     }
  20. }
点击此处复制文本



如果按下运行,我们现在能在显示一个元素后看到邻接的地雷数量。



显示一个区域

每当用户显示一个没有任何邻接地雷的元素,整个无邻接地雷的元素区域应当被全部自动显示,如下图所示。



有很多算法可以实现这个功能,但最简单的是泛洪算法。如果理解递归,泛洪就相当简单。简而言之,泛洪算法主要完成以下这三步:


  • 从某个元素开始
  • 完成对这个元素所需的操作
  • 以递归方式继续处理每个邻接的元素

我们先从为Grid类添加默认的泛洪算法开始。


  1. // 泛洪空元素
  2. public static void FFuncover(int x, int y, bool[,] visited) {
  3.     // 已访问过?
  4.     if (visited[x, y])
  5.         return;

  6.     // 设置访问标志
  7.     visited[x, y] = true;

  8.     // 递归
  9.     FFuncover(x-1, y, visited);
  10.     FFuncover(x+1, y, visited);
  11.     FFuncover(x, y-1, visited);
  12.     FFuncover(x, y+1, visited);
  13. }
点击此处复制文本



visited变量是一个二维数组,仅用于跟踪算法是否已访问了某个特定元素。剩下的是对4个邻接元素进行默认泛洪递归。或者说算法从某个元素开始,然后继续递归处理上下左右的元素,直到它访问完每个元素。它不做任何实际的事,仅仅是对每个元素访问一次。

我们还应该确保算法不会试图访问网格之外的任何元素,因此要检测x和y坐标是否在0到width或height之间。


  1. // 泛洪空元素
  2. public static void FFuncover(int x, int y, bool[,] visited) {
  3.     // 坐标是否在范围内?
  4.     if (x >= 0 && y >= 0 && x < w && y < h) {
  5.         // 已访问过?
  6.         if (visited[x, y])
  7.             return;

  8.         // 设置访问标志
  9.         visited[x, y] = true;

  10.         // 递归
  11.         FFuncover(x-1, y, visited);
  12.         FFuncover(x+1, y, visited);
  13.         FFuncover(x, y-1, visited);
  14.         FFuncover(x, y+1, visited);
  15.     }
  16. }
点击此处复制文本



我们的算法应当显示每个它访问过的元素,并在碰到地雷时停止。


  1. // 泛洪空元素
  2. public static void FFuncover(int x, int y, bool[,] visited) {
  3.     // 坐标是否在范围内?
  4.     if (x >= 0 && y >= 0 && x < w && y < h) {
  5.         //已访问过?
  6.         if (visited[x, y])
  7.             return;

  8.         // 显示元素
  9.         elements[x, y].loadTexture(adjacentMines(x, y));

  10.         // 接近地雷了?那不必继续下去了
  11.         if (adjacentMines(x, y) > 0)
  12.             return;

  13.         // 设置访问标志
  14.         visited[x, y] = true;

  15.         //递归
  16.         FFuncover(x-1, y, visited);
  17.         FFuncover(x+1, y, visited);
  18.         FFuncover(x, y-1, visited);
  19.         FFuncover(x, y+1, visited);
  20.     }
  21. }
点击此处复制文本



现在回到Element脚本,在用户点击某个元素时,使用算法来显示所有空元素。


  1. void OnMouseUpAsButton() {
  2.     // 这是个地雷
  3.     if (mine) {
  4.         // 显示所有地雷
  5.         Grid.uncoverMines();

  6.         // 游戏结束
  7.         print("you lose");
  8.     }
  9.     // 这不是个地雷
  10.     else {
  11.         // 显示邻接地雷数量
  12.         int x = (int)transform.position.x;
  13.         int y = (int)transform.position.y;
  14.         loadTexture(Grid.adjacentMines(x, y));

  15.         // 显示无雷区域
  16.         Grid.FFuncover(x, y, new bool[Grid.w, Grid.h]);

  17.         // 判断游戏是否已获胜
  18.         // ...
  19.     }
  20. }
点击此处复制文本



我们在当前元素位置调用了算法,并使用了一个大小与网格相当的新boolean数组作为参数。泛洪算法将会使用这个数组跟踪已访问元素。

如果我们按下运行,显示一个空元素(即没有邻接地雷),即可看到泛洪的作用。



检测是否已找到所有地雷

还有最后一件事需要完成,我们还需要在用户显示某个元素时,判断游戏是否已经获胜。这个算法也很简单。

让我们返回到Grid类,编写代码查找尚未被显示的地雷。


  1. public static bool isFinished() {

  2.     foreach (Element elem in elements)
  3.         if (elem.isCovered() && !elem.mine)
  4.             return false;
  5.     // 这里没有 => 这是所有的地雷了 => 游戏胜利
  6.     return true;
  7. }
点击此处复制文本



算法只是简单地查找仍未显示且不是地雷的元素。如果寻找到一个,则返回false,因为用户还没完成。如果寻找不到,则返回true,游戏则获胜,因为所有未显示的元素都包含地雷。

现在我们可以使用Element脚本中的isFinished函数:


  1. void OnMouseUpAsButton() {

  2.     // 这是个地雷
  3.     if (mine) {
  4.         // 显示所有地雷
  5.         Grid.uncoverMines();

  6.         //游戏结束
  7.         print("you lose");
  8.     }
  9.     //这不是个地雷
  10.     else {
  11.         //显示邻接地雷数
  12.         int x = (int)transform.position.x;
  13.         int y = (int)transform.position.y;
  14.         loadTexture(Grid.adjacentMines(x, y));

  15.         //显示所有无雷区域
  16.         Grid.FFuncover(x, y, new bool[Grid.w, Grid.h]);

  17.         // 判断游戏是否已获胜
  18.         if (Grid.isFinished())
  19.             print("you win");
  20.     }
  21. }
点击此处复制文本



如果我们按下运行,即可愉快的开始游戏了。



结语

这就是我们的Unity 2D扫雷游戏教程。这一次我们学习了很多有关Unity和C#编程的知识。了解泛洪算法,并能用任何编程语言实现它,对每个开发者来说都是非常有用的。







评分

参与人数 1元素币 +10 活跃度 +12 展开 理由
源支始 + 10 + 12

查看全部评分

2019-11-14 18:02:06  
 赞 赞 0

使用道具 登录

1个回答,把该问题分享到群,邀请大神一起回答。
2#
Element 的脚本是哪里来的啊?手动创建的咯?
回复 收起回复
2021-5-1 23:31:02   回复
 赞 赞 0

使用道具 登录

CG 游戏行业专业问题

手机游戏引擎手游引擎Unity3D技术
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表