一、前言
今天又是摸鱼...哦不..工作的一天,今天整一个很经典的Win系统自带游戏——《扫雷》,话说安装Win10之后就找不到《扫雷》游戏了,很难受。
《扫雷》游戏的玩法是,在不触碰到任何地雷的情况下去发现一个雷区。
揭开一个没有地雷的块之后,将会显示一个数字来表示周围地雷的数量。
跟往常一样,会尽量的将步骤详细说明,代码也进行注释,可以让大家都可以理解。
二、正文
2-1、新建项目
(1)项目开发,从新建项目开始,我使用的Unity版本是Unity 2019.4.7f1
,模板就选择2D
,项目名称随意,别中文就行:
(2)创建目录,在Project视图,右击选择Create→Folder,新建几个文件夹:
(3)目录如下图所示:
- Prefabs:预制体资源文件夹
- Scenes:场景资源文件夹
- Scripts:脚本资源文件夹
- Sprites:图片资源文件夹
2-2、导入资源
接下来将需要的资源导入:
全部右键另存为图片,然后导入到Project视图的Sprites文件夹内:
选中所有图片,在Inspector视图中,设置Pixels Per Unit为16:
之所以设置为16,是因为16X16这个单位在游戏世界中是一个比较适合的值。
2-3、设置摄像机属性
在Hierarchy视图中,选中Main Cameras对象,然后在Inspector视图中找到Camera组件,设置属性:
注意:Clear Flags设置为Skybox,Background按照图中设置,然后Size设置为20。
2-4、制作默认方块
(1)将Project视图的Sprites目录中的default对象拖入Hierarchy视图中:
(2)选中default对象,在Inspector视图中,选择Add Componet→Physics 2D→Box Collider 2D,添加碰撞器组件:
注意:勾选Is Trigger
(3)选中default对象,拖回到Projcet视图的Prefabs文件夹内,做成一个预制体,我们将在后面的代码中去实例化生成它:
(4)Hierarchy视图中的default对象就可以删除了。
(5)新建脚本CreateBg.cs,在Projec视图的Scripts目录中,右击选择Create→C# Script:
双击打开脚本,编辑代码:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class CreateBg : MonoBehaviour { public GameObject block;//默认方块 void Start() { //创建默认方块 CreateBlock(); } private void CreateBlock() { //创建方块父物体 GameObject blockParent = new GameObject("blockParent"); //创建10行10列的默认方块 for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { //Instantiate参数为:预制体 位置 旋转 父物体 Instantiate(block, new Vector2(i, j), Quaternion.identity, blockParent.transform); } } } } 复制代码
将脚本托给Main Camera对象,然后将预制体拖入Block卡槽中:
运行脚本:
是不是有点样子了,这个基本界面就做好了。
2-5、相邻的概念
让我们花一分钟的时间来分析一下相邻的概念,这是《扫雷》游戏中重要的一个部分。
单击一个非地雷的元素后,可以看到指示相邻地雷数量的数字,也就是这个数字的周围有这个数字的雷的数量,一共有9种情况:
因此,我们需要做的就是计算每个字段的相邻的地雷数量,然后得出数字,如果没有相邻的地雷,则为空。
2-6、制作数字和地雷
(1)新建一个脚本Element.cs,然后在Project视图的Prefabs文件夹中选中default对象,点击Add Componet→Element添加脚本:
(2)双击打开Element.cs,编辑代码:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Element : MonoBehaviour { public bool mine;//判断是否是地雷 // 不同的纹理 public Sprite[] emptyTextures; public Sprite mineTexture; void Start() { // 随机决定它是否是地雷 mine = Random.value < 0.15; } // 加载数字的纹理 public void loadTexture(int adjacentCount) { if (mine) GetComponent<SpriteRenderer>().sprite = mineTexture; else GetComponent<SpriteRenderer>().sprite = emptyTextures[adjacentCount]; } // 判断是否被点击 public bool isCovered() { //判断当前纹理的名称是不是默认值 return GetComponent<SpriteRenderer>().sprite.texture.name == "default"; } // 鼠标点击 void OnMouseUpAsButton() { // 是雷的话 if (mine) { // 揭露所有雷 // ... // 游戏结束 Debug.Log("Game Over"); } else { // 显示相邻的数字号 // loadTexture(...); // 揭露没有地雷的地区 // ... // 判断游戏是否胜利 // ... } } } 复制代码
(3)选中default预制体,将对应的资源拖入Element.cs脚本的属性卡槽中:
(4)新建一个Grid.cs脚本,将脚本也添加到预制体default身上,Grid脚本将处理更加复杂的游戏逻辑,比如计算某个元素相邻的地雷,或者发现整个区域的无雷位置:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Grid : MonoBehaviour { // 创建一个二维数组网格 public static int w = 10; // 网格的长 public static int h = 10; // 网格的高 public static Element[,] elements = new Element[w, h]; // 发现所有地雷 public static void uncoverMines() { foreach (Element elem in elements) if (elem.mine) elem.loadTexture(0); } // 看看这个坐标上是否有地雷 public static bool mineAt(int x, int y) { // 控制坐标范围 if (x >= 0 && y >= 0 && x < w && y < h) return elements[x, y].mine; return false; } // 为一个元素计算相邻的地雷 8个方向 public static int adjacentMines(int x, int y) { //计数器 int count = 0; if (mineAt(x, y + 1)) ++count; // 上 if (mineAt(x + 1, y + 1)) ++count; // 右上 if (mineAt(x + 1, y)) ++count; // 右 if (mineAt(x + 1, y - 1)) ++count; // 右下 if (mineAt(x, y - 1)) ++count; // 下 if (mineAt(x - 1, y - 1)) ++count; // 左下 if (mineAt(x - 1, y)) ++count; // 做 if (mineAt(x - 1, y + 1)) ++count; // 左上 //返回相邻的地雷数量 return count; } } 复制代码
(5)修改Element.cs脚本代码:
Start函数修改:
void Start() { // 随机决定它是否是地雷 mine = Random.value < 0.15; // 在Grid注册 int x = (int)transform.position.x; int y = (int)transform.position.y; Grid.elements[x, y] = this; } 复制代码
OnMouseUpAsButton函数修改:
// 鼠标点击 void OnMouseUpAsButton() { // 是雷的话 if (mine) { // 揭露所有雷 Grid.uncoverMines(); // 游戏结束 Debug.Log("Game Over"); } else { // 显示相邻的数字号 int x = (int)transform.position.x; int y = (int)transform.position.y; loadTexture(Grid.adjacentMines(x, y)); // 揭露没有地雷的地区 // ... // 判断游戏是否胜利 // ... } } 复制代码
运行程序,就可以看到点击一个雷之后,就可以看到其他雷也会被发现,发现一个元素后,可以看到相邻的数字:
2-7、泛洪算法
好的,每当用户发现一个没有相邻地雷的元素时,就应该自动发现没有相邻地雷的整个区域,如下所示:
有很多算法都可以做到这一点,但是目前为止最简单的算法还是泛洪算法,如果理解递归,泛洪算法也是很好理解的,下面就是泛洪算法所做的工作:
- 从某种元素开始
- 用这个元素做我们想做的事
- 对每个相邻元素递归地继续
然后将泛洪算法加入到Grid类中:
// 泛洪算法填充空元素 public static void FFuncover(int x, int y, bool[,] visited) { if (x >= 0 && y >= 0 && x < w && y < h) { // 判断是否遍历过 if (visited[x, y]) return; // 设置遍历标识 visited[x, y] = true; // 递归 FFuncover(x - 1, y, visited); FFuncover(x + 1, y, visited); FFuncover(x, y - 1, visited); FFuncover(x, y + 1, visited); } } 复制代码
注意:泛洪算法递归地访问某个元素的周围的元素,直到它访问到每个元素为止。
接着修改我们的泛洪算法,这个算法应该发现它访问的元素是否是一个地雷,如果是的话就不应该继续下去:
// 泛洪算法填充空元素 public static void FFuncover(int x, int y, bool[,] visited) { if (x >= 0 && y >= 0 && x < w && y < h) { // 判断是否遍历过 if (visited[x, y]) return; // 发现元素 elements[x, y].loadTexture(adjacentMines(x, y)); // 发现地雷 if (adjacentMines(x, y) > 0) return; // 设置遍历标识 visited[x, y] = true; // 递归 FFuncover(x - 1, y, visited); FFuncover(x + 1, y, visited); FFuncover(x, y - 1, visited); FFuncover(x, y + 1, visited); } } 复制代码
回到Element.cs脚本,修改OnMouseUpAsButton函数,使用该算法在用户单击其中一个元素时发现所有空元素:
// 鼠标点击 void OnMouseUpAsButton() { // 是雷的话 if (mine) { // 揭露所有雷 Grid.uncoverMines(); // 游戏结束 Debug.Log("Game Over"); } else { // 显示相邻的数字号 int x = (int)transform.position.x; int y = (int)transform.position.y; loadTexture(Grid.adjacentMines(x, y)); // 揭露没有地雷的地区 Grid.FFuncover(x, y, new bool[Grid.w, Grid.h]); // 判断游戏是否胜利 // ... } } 复制代码
运行程序,在发现一个空元素的时候,会遍历就寻找周围是否存在没有地雷的空元素:
2-8、判断是否已经找到所有地雷
接下来,需要判断玩家是否已经找到所有的雷,那么游戏就应该结束了。
接着修改Grid类的代码,添加函数isFinished:
// 是否找到所有地雷 public static bool isFinished() { // 遍历数组 找到没有被地雷覆盖的元素 foreach (Element elem in elements) if (elem.isCovered() && !elem.mine) return false; // 没有找到 => 全是地雷 => 游戏胜利. return true; } 复制代码
修改Element.cs的代码:
// 鼠标点击 void OnMouseUpAsButton() { // 是雷的话 if (mine) { // 揭露所有雷 Grid.uncoverMines(); // 游戏结束 Debug.Log("Game Over"); } else { // 显示相邻的数字号 int x = (int)transform.position.x; int y = (int)transform.position.y; loadTexture(Grid.adjacentMines(x, y)); // 揭露没有地雷的地区 Grid.FFuncover(x, y, new bool[Grid.w, Grid.h]); // 判断游戏是否胜利 if (Grid.isFinished()) Debug.Log("Game Win"); } } 复制代码
运行程序,就可以愉快的玩游戏了。
三、总结
《扫雷》游戏的大体框架就开发完成了,当然,你也可以添加一些元素让游戏更加有趣:
- 用标记标记地雷
- 分成更多难度,比如简单、中等、困难
- 切换更加漂亮的UI
- 输赢界面以及重新开始
- 添加音效