找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索

[大神交流] [外文翻译] 使用Unity2D制作经典游戏《扫雷》(下)

[复制链接]

350

主题

368

帖子

1376

积分

版主

Rank: 7Rank: 7Rank: 7

积分
1376
vr爱好者 版主 7670 楼主
跳转到指定楼层
发表于 2016-9-23 10:44:45
关于相邻
让我们花点时间去了解下相邻地雷的关系,这将成为我们扫雷游戏的一个很重要的部分。

提示:相邻的意思是该物体的周围或者是相邻格子之间。

在点击一个方块(defualt)之后,显示的是一个数字,这将意味着该数字的附近对应的地雷数。该数字的值最高时8。换句话说,该格子(显示8的格子)的上,下,左,右,上左,上右,下坐,下右一共是8。如下图所示:

这里有9个不同的情况:

             
             
             

因此我们不得不去计算所有相邻之间有地雷的情况,并且去绘制这些值。如果附近没有地雷,那就不用给值。

添加更多的图片
好了,我们为了绘制那些数字,可以使用自带的UI系统,也可以为每个数字绘制一张图片,如下图:

                               

注意:你可以右键每一张图片,然后选择另存为,把他们都保存到ProjectAsset文件夹下。

我们也需要一张地雷的图片:





提示:你可以右键上面的地雷图片,然后选择另存为,把他都保存到Project的Asset文件夹下。



我们将所有的图片都保存到Project的Asset下后,我们找到这些图片,然后如下图所示在Inspector面板下进行导入设置:



代码部分

接下让我们写些代码吧!我们将在Project Area面板下右键,Create->C# Script ,为该脚本取名为:Element:



但是我们不这样做,现在我们在Hierarchy面板下选择所有方块(default),然后在Inspector面板下选择Add Component->Script->Element ,这便给所有的方块(default)添加了Element脚本,比上面的方法方便多了:



让我们在Project Area面板下双击把脚本打开:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

using UnityEngine;
using System.Collections;

public class Element : MonoBehaviour {

   // Use this for initialization
   void Start () {

   }

   // Update is called once per frame
   void Update () {

   }
}





我们可以把Update函数给删除咯,因为我们不需要它。让我们添加一个Bool变量,用来判断是否是地雷:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


01

02

03

04

05

06

07

08

09

10

11

12

using UnityEngine;
using System.Collections;

public class Element : MonoBehaviour {

   // Is this a mine?
   public bool mine;

   // Use this for initialization
   void Start () {

   }
}






提示:这个mine是一个public类型的变量,以便于其他类能访问它。Start函数在游戏开始时只调用一次。

现在我们可以在Start函数里使用 Random.value来为mine变量随机赋值:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


01

02

03

04

05

06

07

08

09

10

11

12

13

using UnityEngine;
using System.Collections;

public class Element : MonoBehaviour {

   // Is this a mine?
   public bool mine;

   // Use this for initialization
   void Start () {
       // Randomly decide if it's a mine or not
       mine = Random.value < 0.15;
   }
}






提示:Random.value方法返回一个0和1之间的随机数,我们想要每个方块下面有15%的几率是地雷,因此我们使用Random.value < 0.15。

让我们来创建一个小的帮助功能,我们希望能够从默认的纹理切换到空的纹理。首先我们先定义一些纹理变量:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

using UnityEngine;
using System.Collections;

public class Element : MonoBehaviour {

   // Is this a mine?
   public bool mine;

   // Different Textures
   public Sprite[] emptyTextures;
   public Sprite mineTexture;

   // Use this for initialization
   void Start () {
       // Randomly decide if it's a mine or not
       mine = Random.value < 0.15;
   }
}






提示:Sprite是Textures另一种类型,Sprite[]是以个数组(Array)类型。

现在我们可以在Inspector面板里看到一个新的插槽:


我们可以把这些纹理拖进这些插槽里,现在我们把纹理都拖拽进来,如下图所示:


现在我们可以通过创建一个LoadTexture函数来使用我们的Sprite变量:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

using UnityEngine;
using System.Collections;

public class Element : MonoBehaviour {

   // Is this a mine?
   public bool mine;

   // Different Textures
   public Sprite[] emptyTextures;
   public Sprite mineTexture;

   // Use this for initialization
   void Start () {
       // Randomly decide if it's a mine or not
       mine = Random.value < 0.15;
   }

   // Load another texture
   public void loadTexture(int adjacentCount) {
       if (mine)
           GetComponent<SpriteRenderer>().sprite = mineTexture;
       else
           GetComponent<SpriteRenderer>().sprite = emptyTextures[adjacentCount];
   }
}






提示:这个函数第一检查这个块是否是一个地雷,如果是地雷,就去加载mine texture(地雷纹理)。如果不是地雷,就加载emptyTextures其中一个(数字纹理),取决于这个adjacentCount传入参数。GetComponent<SpriteRenderer>().sprite就是用来改变目前的纹理。

我们可以通过Start函数来测试一下:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


1

2

3

4

5

6

7

// Use this for initialization
void Start () {
   // Randomly decide if it's a mine or not
   //mine = Random.value < 0.15;

   // TEST
   loadTexture(1);
}






现在我们按下Play按钮,我们就可以看到单独的块全加载了数字1的纹理:



现在我们回到Start函数把值改回来:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


1

2

3

4

// Use this for initialization
void Start () {
   // Randomly decide if it's a mine or not
   mine = Random.value < 0.15;
}






此后,我们需要知道这个块是否是掩盖着的(如:没有点击时的状态),因此我们添加一个函数,用来在当前纹理的名字和default简单的比较:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


1

2

3

// Is it still covered?
public bool isCovered() {
   return GetComponent<SpriteRenderer>().sprite.texture.name == "default";
}






提示:如果当前纹理的名字等于default,说明现在这个块是掩盖着的。反之,我们将加载地雷纹理或者数字纹理。

我们将为我们的Element 脚本添加一个函数,去检测鼠标按下。每一个块都有一个Collider2D 组件。这就意味着,我们无论点击任何一个方块,这个OnMouseUpAsButton函数都会通过Unity去调用。当然,我们脚本里必须要有这个名为OnMouseUpAsButton的函数,因此让我们为我们的脚本添加上吧:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


1

2

void OnMouseUpAsButton() {
   // ToDo: do stuff..
}






在我们点击方块后,会有两种情况发生。一种情况是有地雷,一种情况是没有地雷:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


1

2

3

4

5

6

7

8

9

void OnMouseUpAsButton() {
   // It's a mine
   if (mine) {
       // ToDo: do stuff..
   }
   // It's not a mine
   else {
       // ToDo: do stuff..
   }
}






如果这是一个地雷,其它地雷都将会暴露出来(我们很快就可以实现了-_-)并且Game Over(这个我就不用翻译了吧,累= =)。

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


01

02

03

04

05

06

07

08

09

10

11

12

13

void OnMouseUpAsButton() {
   // It's a mine
   if (mine) {
       // ToDo: uncover all mines
       // ...

       // game over
       print("you lose");
   }
   // It's not a mine
   else {
       // ToDo: do stuff..
   }
}






如果这不是一个地雷,将会有几种事情发生。首先,我们应该用正确的数量去加载empty texture(数字纹理),这取决于周围的地雷数量。在我们点击后,如果方块的附近没有任何地雷,我们将翻开没有地雷的白色区域块。如下图所示:



我们也应该知道,如果我们找到所有的地雷,我们将获得游戏的胜利。在这里,我们先给我们要做的事情添加注释:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

void OnMouseUpAsButton() {
       // It's a mine
       if (mine) {
           // ToDo: uncover all mines
           // ...

           // game over
           print("you lose");
       }
       // It's not a mine
       else {
           // ToDo show adjacent mine number
           //loadTexture(...);

           // ToDo uncover area without mines
           // ...

           // ToDo find out if the game was won now
           // ...
       }
   }






上面代码中的所有TODO都有一个共同的特点,他们不仅仅需要该方块自身的信息,还需要其他方块的信息。因此,让我们创建更多的脚本去管理所有的方块吧。~!!

Grid布局
创建这个类(Grid)

Gird类是一个帮助类,它可以让我们了解目前所有方块的状态,可以实现更复杂的游戏逻辑,例如,我们可以知道一个方块附近的地雷数量:

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

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

using UnityEngine;
using System.Collections;

public class Grid : MonoBehaviour {

   // Use this for initialization
   void Start () {

   }

   // Update is called once per frame
   void Update () {

   }
}






这个脚本不要任何父类,因此让我们删掉MonoBehaviour,并把继承父类的Start方法和Update函数给删掉咯:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


1

2

3

4

5

using UnityEngine;
using System.Collections;

public class Grid {

}






Elenment类的2维数组
我们的Gird类应该保存在我们游戏中所有方块的状态的跟追。我们可以使用一个二维数组(也可以看作是一个矩阵或表格),如下面代码所示:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


1

2

3

4

5

6

7

8

using UnityEngine;
using System.Collections;

public class Grid {
   // The Grid itself
   public static int w = 10; // this is the width
   public static int h = 13; // this is the height
   public static Element[,] elements = new Element[w, h];
}






提示:这里创建了一个13行10列的二维数组,换句话说,该数组就是对应我们游戏中13行10列的方块。如果我们想要访问Position:X=0,Y=1位置的方块,这便对应于数组Element[0,1]。

为数组赋值

现在让我们回到Element脚本中去,在Start函数为每一个方块对应的二维数组位置赋值:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


1

2

3

4

5

6

7

8

9

// Use this for initialization
void Start () {
   // Randomly decide if it's a mine or not
   mine = Random.value < 0.15;

   // Register in Grid
   int x = (int)transform.position.x;
   int y = (int)transform.position.y;
   Grid.elements[x, y] = this;
}






提示:上面代码中的transform.position.x和transform.position.y的类型是float类型,因此我们必须在使用他们之前强制转换为int类型。代码中的this指的是该方块自身。

翻开所有的地雷
好了,让我们一起回到Gird脚本中,用一个函数去翻开所有的地雷。如果当前点击的方块是地雷,将加载所有的地雷纹理:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码
[color=white !important]
[color=white !important]?


1

2

3

4

5

// Uncover all Mines
public static void uncoverMines() {
   foreach (Element elem in elements)
       if (elem.mine)
           elem.loadTexture(0);
}






提示:这是一个静态类型的函数,这样做的目的是方便调用。

让我们再一次回到Element脚本中,修改一下OnMouseUpAsButton函数,在点击到一个地雷的情况下,使用刚刚创建的uncoverMines函数:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

void OnMouseUpAsButton() {
   // It's a mine
   if (mine) {
       // Uncover all mines
       Grid.uncoverMines();

       // game over
       print("you lose");
   }
   // It's not a mine
   else {
       // ToDo show adjacent mine number
       //loadTexture(...);

       // ToDo uncover area without mines
       // ...

       // ToDo find out if the game was won now
       // ...
   }
}






现在,如果我们点击Play按钮。现在我可以试着点击一个方块,如果该方块是地雷,将加载所有的地雷哟:


计算附近的地雷
下一步,我们将为Gird类添加其他的函数,给定方块X,Y的位置。这个函数将计算出附近的地雷数量。这听起来有点复杂,但最后这个函数的目的就是找到一个方块附近的方向,如下:

  • top(上)
  • top-right (上右)
  • right (右)
  • bottom-right (下右)
  • bottom (下)
  • bottom-left (下左)
  • left (左)
  • top-left (上左)


并且添加一个计数器。

首先,我们先为Gird类添加一个函数,这个函数将简单的检查地雷是否在正确的坐标位置:


[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


1

2

3

4

5

6

// Find out if a mine is at the coordinates
public static bool mineAt(int x, int y) {
   // Coordinates in range? Then check for mine.
   if (x >= 0 && y >= 0 && x < w && y < h)
       return elements[x, y].mine;
   return false;
}






提示:我们必须检查这个坐标位置是否在Element数组范围内,这样做事为了避免抛出数组下标超出错误。

现在我们创建adjacentMines函数,该函数用X坐标和Y坐标作为传入参数,并且返回count(计数器)。

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码
[color=white !important]
[color=white !important]?


1

2

3

4

5

6

7

8

// Count adjacent mines for an element
public static int adjacentMines(int x, int y) {
   int count = 0;

   // ToDo count adjacent mines
   // ...

   return count;
}






之后,我们就要检查方块的附近了:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

// Count adjacent mines for an element
public static int adjacentMines(int x, int y) {
   int count = 0;

   if (mineAt(x,   y+1)) ++count; // top
   if (mineAt(x+1, y+1)) ++count; // top-right
   if (mineAt(x+1, y  )) ++count; // right
   if (mineAt(x+1, y-1)) ++count; // bottom-right
   if (mineAt(x,   y-1)) ++count; // bottom
   if (mineAt(x-1, y-1)) ++count; // bottom-left
   if (mineAt(x-1, y  )) ++count; // left
   if (mineAt(x-1, y+1)) ++count; // top-left

   return count;
}






(上面的注释英文就不用翻译了吧。。。)

现在,让我回到Element脚本中,并再一次改变OnMouseUpAsButton函数:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

void OnMouseUpAsButton() {
   // It's a mine
   if (mine) {
       // uncover all mines
       Grid.uncoverMines();

       // game over
       print("you lose");
   }
   // It's not a mine
   else {
       // show adjacent mine number
       int x = (int)transform.position.x;
       int y = (int)transform.position.y;
       loadTexture(Grid.adjacentMines(x, y));

       // ToDo uncover area without mines
       // ...

       // ToDo find out if the game was won now
       // ...
   }
}






现在,你可以点击Play按钮,然后去随便点击翻开一个方块,你将会看到一些数字咯:



翻开一个区域
好了,用户翻开的区域如果没有地雷,将会直接翻开一个空白区域。如下图所示:



有许多的算法可以做这事儿,但目前最简单的还是Flood Full(泛洪填充)算法(想要更加了解该算法的朋友,可以百度下关键字)。如果我们已经了解递归的知识,那理解Flood Full将会很简单。简单的说,下面是就Flood Full算法要做的事情:

  • starting in some element (在方块中开始)
  • do whatever we want with that element (我们想要用这个方块 方块做些什么)
  • continue recursively for each neighbor element(对每一个相邻的方块继续递归)


现在我们将为Gird类添加一个默认的Flood Full算法:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

// Flood Fill empty elements
public static void FFuncover(int x, int y, bool[,] visited) {
   // visited already?
   if (visited[x, y])
       return;

   // set visited flag
   visited[x, y] = true;

   // recursion
   FFuncover(x-1, y, visited);
   FFuncover(x+1, y, visited);
   FFuncover(x, y-1, visited);
   FFuncover(x, y+1, visited);
}






提示:这个visited变量是以个二维数组,简单的保存这个算法是否已经被特定的方块访问,剩下的部分就是4个Flood Full算法的递归。这其实没做任何事情,这其实只是访问了每一个方块:

我们应该确保这个算法访问的方块不会超出数组的下标,通过X坐标和Y坐来检查:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

// Flood Fill empty elements
public static void FFuncover(int x, int y, bool[,] visited) {
   // Coordinates in Range?
   if (x >= 0 && y >= 0 && x < w && y < h) {
       // visited already?
       if (visited[x, y])
           return;

       // set visited flag
       visited[x, y] = true;

       // recursion
       FFuncover(x-1, y, visited);
       FFuncover(x+1, y, visited);
       FFuncover(x, y-1, visited);
       FFuncover(x, y+1, visited);
   }
}






我们的算法应在翻开每一个方块后去访问它。当接近一个附近有地雷的方块时,我们就不继续去访问它了:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

// Flood Fill empty elements
public static void FFuncover(int x, int y, bool[,] visited) {
   // Coordinates in Range?
   if (x >= 0 && y >= 0 && x < w && y < h) {
       // visited already?
       if (visited[x, y])
           return;

       // uncover element
       elements[x, y].loadTexture(adjacentMines(x, y));

       // close to a mine? then no more work needed here
       if (adjacentMines(x, y) > 0)
           return;

       // set visited flag
       visited[x, y] = true;

       // recursion
       FFuncover(x-1, y, visited);
       FFuncover(x+1, y, visited);
       FFuncover(x, y-1, visited);
       FFuncover(x, y+1, visited);
   }
}






在C#里面修改Flood Full算法,这是多么容易实现啊!

现在我们可以回到Element脚本中,使用算法去翻开没有地雷的方块在我们点击其中一个方块的时候:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

void OnMouseUpAsButton() {
   // It's a mine
   if (mine) {
       // uncover all mines
       Grid.uncoverMines();

       // game over
       print("you lose");
   }
   // It's not a mine
   else {
       // show adjacent mine number
       int x = (int)transform.position.x;
       int y = (int)transform.position.y;
       loadTexture(Grid.adjacentMines(x, y));

       // uncover area without mines
       Grid.FFuncover(x, y, new bool[Grid.w, Grid.h]);

       // ToDo find out if the game was won now
       // ...
   }
}






提示:我们只在目前方块的位置中调用该算法。

现在,你可以点击Play按钮,并且翻开一个没有地雷的方块,你将会看到下图所示的效果:



所有的地雷被发现
还有最后一件事情要去做,当用户找到所有的地雷时,玩家获得胜利:

让我们再一次回到Gird类中,写一些代码,用来检测当用户找出所有地雷位置的情况:

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


1

2

3

4

5

6

7

public static bool isFinished() {
   // Try to find a covered element that is no mine
   foreach (Element elem in elements)
       if (elem.isCovered() && !elem.mine)
           return false;
   // There are none => all are mines => game won.
   return true;
}






提示:如果用户在玩的途中翻中了地雷,则该函数返回false,如果用户找出所有地雷的位置,则返回true。

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

[C#] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码

[color=white !important]
[color=white !important]?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

void OnMouseUpAsButton() {
   // It's a mine
   if (mine) {
       // uncover all mines
       Grid.uncoverMines();

       // game over
       print("you lose");
   }
   // It's not a mine
   else {
       // show adjacent mine number
       int x = (int)transform.position.x;
       int y = (int)transform.position.y;
       loadTexture(Grid.adjacentMines(x, y));

       // uncover area without mines
       Grid.FFuncover(x, y, new bool[Grid.w, Grid.h]);

       // find out if the game was won now
       if (Grid.isFinished())
           print("you win");
   }
}






现在我们可以点击Play按钮来畅享游戏了



总结
这个就是我们的Unity2D《扫雷》教程,在此教程中,我们学到了许多UnityC#编程知识,也简单的了解了Flood Full算法。此后你们可以将该算法打包成一个工具类,便能应用到任何游戏中去了。

回复

使用道具 举报

*滑动验证:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则