大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有用记得一键三连哦。
一、前言
今天又是摸鱼...哦不..工作的一天,今天整一个很经典的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:
双击打开脚本,编辑代码:
代码语言:javascript复制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,编辑代码:
代码语言:javascript复制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脚本将处理更加复杂的游戏逻辑,比如计算某个元素相邻的地雷,或者发现整个区域的无雷位置:
代码语言:javascript复制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函数修改:
代码语言:javascript复制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函数修改:
代码语言:javascript复制 // 鼠标点击
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类中:
代码语言:javascript复制 // 泛洪算法填充空元素
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);
}
}
注意:泛洪算法递归地访问某个元素的周围的元素,直到它访问到每个元素为止。
接着修改我们的泛洪算法,这个算法应该发现它访问的元素是否是一个地雷,如果是的话就不应该继续下去:
代码语言:javascript复制 // 泛洪算法填充空元素
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函数,使用该算法在用户单击其中一个元素时发现所有空元素:
代码语言:javascript复制 // 鼠标点击
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:
代码语言:javascript复制 // 是否找到所有地雷
public static bool isFinished()
{
// 遍历数组 找到没有被地雷覆盖的元素
foreach (Element elem in elements)
if (elem.isCovered() && !elem.mine)
return false;
// 没有找到 => 全是地雷 => 游戏胜利.
return true;
}
修改Element.cs的代码:
代码语言:javascript复制 // 鼠标点击
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
- 输赢界面以及重新开始
- 添加音效