使用Unity 2D实现经典的扫雷游戏(上)
相信我们很多人都玩过扫雷游戏,它是一个单人解谜游戏,最早于上世纪60年代发布。游戏的目标是探索雷区,努力不触发地雷。每显示一个无雷区域,游戏会显示一个数字,表明四周的地雷数量。这为游戏增加了一个不错的策略因素。
这篇教程使用Unity 2D实现经典的扫雷游戏将非常简单,仅有85行代码以及一些像素美术素材。我们将学习一些有关Unity编程以及如何实现广受欢迎的泛洪填充算法。
说明:泛洪填充算法 https://en.wikipedia.org/wiki/Flood_fill
开发准备
本篇教程无需任何特殊的Unity技巧,除了一些有关游戏对象和变换等内容的基本知识。理解递归的概念将对泛洪填充算法的实现很有帮助。
本篇教程可以使用Unity 5.0.0f4及更高版本来进行实现。
项目设置
启动Unity,选择New Project创建新项目,并将项目命名为minesweeper,选择任意一个保存位置,比如C:\,选择项目类型为2D,点击Create Project创建新项目。
现在我们可以修改摄像机,确保游戏处于屏幕中心。首先选择层级窗口中的Main Camera ,然后将Background Color设置为黑色。我们还需要将Size 与Position 修改设置为下图所示的样子。
基础项目设置就完成了。
默认元素
让我们把默认元素添加到游戏。默认元素是那些我们尚未点击的区块,用于隐藏它下面真实的内容。
首先我们需要一些可用的图片。为了简单起见,可以使用类似 Paint.NET这样的绘图工具,画一个16x16像素的图像。
保存到Assets文件夹后,我们可以在Project项目窗口选择它。
然后我们可以在检视窗口中修改它的导入设置。需要注意,导入设置指定了图像在最终游戏中的大小以及是否要进行压缩。
现在我们可以将图像从Project项目区域拖入到场景。
我们在场景窗口或层级窗口中选中默认元素,然后检查检视窗口。我们要将它的位置设为x=0,y=0。x是水平位置,而y是垂直位置。因为这是个2D游戏,不需要第三个维度,所以我们将z设为0。
我们希望能在用户单击一个元素时获得通知。Unity已经提供了一个函数可用于实现此目的,不过仅对有碰撞器的元素才有效。一个碰撞器可以使我们的对象成为物理世界的一部分。现在我们的默认元素只是游戏世界中的一个图像。一旦为它添加了碰撞器,它就能成为物理世界的一部分,就像一面墙一样。
要添加一个碰撞器,可以在检视窗口中选择:Add Component->Physics 2D->Box Collider 2D。这样,它现在是物理世界的一部分了。
如果我们点击运行,就能看到游戏中的第一个元素。
添加更多元素
我们的2D扫雷游戏如果只有一个元素那就太无聊了。我们可以通过刚才的流程,或者右击层级窗口中的default游戏对象并选择“Duplicate” 进行复制,以添加更多的元素。
我们将复制的元素放x=1,y=0的位置。
现在我们可以不停地复制元素,直到有10(水平)x 13(垂直)个元素。
底部元素的坐标是x=0,y=0。右上角的元素位于x=9,y=12。中间的元素位置坐标应当要四舍五入就像这样x=2,y=3,而非x=2.04,y=3.002。
现在我们的游戏界面是不是看上去已经有点像扫雷游戏的模样了!
关于邻接
让我们花点时间了解下邻接地雷的情况,这将是我们扫雷游戏的重要部分。
点击一个非地雷元素后,用户应当能看到一个指示邻接地雷数量的数字。我们将采取一种称为8向邻接检测的手段。或者换而言之,我们不仅会检测顶/底/左/右,同时还要检测左上/右上/左下/右下的元素。
这里使我们会遇到的九种不同情况:
所以我们要计算每个块的邻接地雷数量,然后绘制数字。如果没有邻接地雷,则什么也不绘制。
添加更多图像:数字与地雷
为了要绘制那些数字,我们可以使用Unity的GUI系统或为简单地为每个数字快速绘制一个纹理。然后将它们保存到项目的Assets文件夹中。
我们还需要用到一个地雷的图像
我们将所有那些图像保存到Project窗口中后,要选择它们,并在检视窗口中为它们应用以下这些导入设置。
开始编码
现在开始编写代码。右击Project窗口,选择Create->C# Script,并命名为Element。
这个脚本目前没有任何作用,让我们选择层级窗口中的所有default元素,然后通过点击检视窗口中的Add Component->Script->Element,将脚本添加到它们上面。
在Project项目窗口中双击并打开脚本。
using UnityEngine;
using System.Collections;
public class Element : MonoBehaviour {
// 初始化
void Start () {
}
// 每帧调用一次Update
void Update () {
}
}
我们可以移除Update函数,因为不需要它。然后我们添加一个变量,表明当前元素是否是一个地雷。
using UnityEngine;
using System.Collections;
public class Element : MonoBehaviour {
// 这是一颗地雷吗?
public bool mine;
// 初始化
void Start () {
}
}
变量mine是公共的,这样其它元素才能看到它。Start函数会在游戏开始时被调用。
通过在Start函数中使用Random.value,现在我们可以随机决定这个元素是否是一颗地雷。Random.value总会返回一个介于0和1之间的新随机数。如果我们希望元素有15%的概率是一颗地雷,所以我们将使用Random.value<0.15。
using UnityEngine;
using System.Collections;
public class Element : MonoBehaviour {
// 这是一颗地雷吗?
public bool mine;
// 初始化
void Start () {
// 随机决定它是否是一颗地雷
mine = Random.value < 0.15;
}
}
现在让我们创建一个小小的辅助函数。我们希望能随时从默认纹理切换为空纹理、某个数字纹理或地雷纹理。
首先我们要定义一些纹理变量。
using UnityEngine;
using System.Collections;
public class Element : MonoBehaviour {
// 这是一颗地雷吗?
public bool mine;
// 不同纹理
public Sprite[] emptyTextures;
public Sprite mineTexture;
// 初始化
void Start () {
// 随机决定它是否是一颗地雷
mine = Random.value < 0.15;
}
}
Sprite是纹理的另一种说法。Sprite[]则是一个数组,也就是不止一个Sprite。
现在我们可以在检视窗口中看到一些新的栏位。
可以将纹理拖动其中。让我们把Project项目窗口中的纹理一个个拖入到这些栏位。
现在我们可以通过loadTexture函数使用我们的Sprite变量了。
using UnityEngine;
using System.Collections;
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];
}
}
这个函数会首先检测元素是不是地雷。如果是,就加载地雷纹理。如果不是,就加载emptyTextures(数字)中的一个,具体根据adjacentCount而定。GetComponent<SpriteRenderer>().sprite 就是我们更改当前纹理的方式。
可以对Start函数暂时做下修改,以测试我们的函数。
//初始化
void Start () {
// 随机决定它是否是一颗地雷
//mine = Random.value < 0.15;
// 测试
loadTexture(1);
}
如果按下运行,我们可以看到每个元素都载入了数字1的纹理,如下图所示,则代表正确。
现在我们可以把Start函数还原回去了。
// 初始化
void Start () {
// 随机决定它是否是一颗地雷
mine = Random.value < 0.15;
}
随后我们将需要知道某个元素是否仍被覆盖,例如:尚未点击。所以让我们添加一个小函数,简单地将当前纹理名与默认名做下比较。
//是否被覆盖?
public bool isCovered() {
return GetComponent<SpriteRenderer>().sprite.texture.name == "default";
}
只要默认纹理还在就说明元素还未被显示。反之,如果我们加载了一个不同的纹理,比如地雷或某个数字,就说明它已被显示。
我们还要给Element脚本添加一个函数,用于检测鼠标的点击。每个元素都已经附加了一个Collider2D组件,这意味着只要点击某个元素,Unity就会调用函数OnMouseUpAsButton。当然,这首先要在脚本中有这么一个函数:
void OnMouseUpAsButton() {
// ToDo: do stuff..
}
当点击一个元素时,可能会发生二种情况:要么是地雷,要么不是。
void OnMouseUpAsButton() {
// 这是一个地雷
if (mine) {
// ToDo: do stuff..
}
//这不是一个地雷
else {
// ToDo: do stuff..
}
}
如果这是个地雷,那应当显示所有地雷,然后游戏结束。
void OnMouseUpAsButton() {
//这是一个地雷
if (mine) {
// ToDo: 显示所有的地雷
// ...
// 游戏结束
print("you lose");
}
//这不是一个地雷
else {
// ToDo: do stuff..
}
}
如果不是地雷,那可能会发生好几种情况。首先,我们要根据邻接地雷数量载入正确数字的空纹理。如果点击的元素没有任何邻接地雷,那我们应该显示整个没有地雷的元素区域,如下图所示。
我们还应该判断是否所有除地雷外的元素都已被显示,这种情况下游戏就已获胜。这是第一个版本的代码。
void OnMouseUpAsButton() {
// I这是个地雷
if (mine) {
// ToDo: 显示所有的地雷
// ...
//游戏结束
print("you lose");
}
// 这不是个地雷
else {
// 显示邻接地雷数量
//loadTexture(...);
//显示所有无雷区域
// ...
//判断游戏是否已获胜
// ...
}
}
我们所有的ToDo功能都有一个共同点:它们除了需要元素本身的信息之外,都需要访问其它元素。所以让我们再创建一个脚本,用来处理所有元素。
小结
使用Unity 2D实现经典扫雷游戏的上篇,就为大家介绍到这里。在下篇中将采用网格来处理更加复杂的游戏逻辑,从而完整的实现游戏。尽请期待!更多精彩Unity教程尽在Unity中文官方论坛(Unitychina.cn)!
推荐阅读
官方活动
直播预告 | 使用Shader Graph着色器视图快速创建炫酷特效
直播时间:4月11日 20:00-21:00
活动网址:https://connect.unity.com/events/unitychina-shadergraph
活动信息:截至至4月20日 16:00
活动网址:https://connect.unity.com/challenges/universal
Unite Beijing 2018 及 Training Day
活动信息:5月11-13日 北京国家会议中心
售票官网: http://unite2018.csdn.net/ 或者直接扫描下图二维码进行购票!
点击“阅读原文”访问Unity中文官方论坛!