手把手教你做3D扫雷:完结篇

发表于2017-01-06
评论0 3.7k浏览
  在这个系列的最后,我们将实现扫雷游戏点击网格功能,让我们的游戏更具可玩性。我们还要实现玩家能够赢得或输掉游戏的功能。如果你完成之前的教程,游戏现在应该就能够创建格子的区域,然后随机地把炸弹赋值给他们。当玩家用鼠标在格子上划过就会出现高亮效果,还能够放置和移除旗子。每个格子之间还能够通信,还能够计算出附近的炸弹数。

揭开格子
  我们已经添加了右键点击防止旗子的功能,现在,让我们添加左键揭开格子的功能。在OnMouseOver函数中,我们需要引用鼠标左击的方法。代码如下:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
function OnMouseOver()
{
    if(state == "idle")
    {
        renderer.material = materialLightup;
     
        if (Input.GetMouseButtonDown(0))
            UncoverTile();
     
        if (Input.GetMouseButtonDown(1))
            SetFlag();
    }
    else if(state == "flagged")
    {
        renderer.material = materialLightup;
     
        if (Input.GetMouseButtonDown(1))
            SetFlag();
    }
}

  当鼠标左键点击时,UncoverTile函数将被调用。确保你创建了这个方法,这样才不会出Bug!

01
02
03
04
05
06
07
08
09
10
11
function UncoverTile()
{
    if(!isMined)
    {
        state = "uncovered";
        displayText.renderer.enabled = true;
        renderer.material = materialUncovered;
    }
    else
        Explode();
}

  如果运行成功了,我们需要声明一个新的材质。

1
public var materialUncovered: Material;

  创建区别于基础色的颜色。就比如你的基础色为蓝色,你就应该选择绿色代表已经揭开的状态。千万别选择红色,因为接下来我们会用这个颜色表示揭开炸弹后的状态。当我们调用这个函数后,会发生如下事情:
  ·  第一步会检查格子是否确实有炸弹;
  ·  如果没有炸弹,这个区域便会被设置为uncovered状态,激活文本显示状态,向我们显示周围炸弹数,然后设置uncovered材质;
  ·  接着,格子不能够被点击第二次,也不会重复高亮,这就意味着格子响应事件只会在鼠标真正点击之后发生。
  在我们运行测试前,我们需要确保当鼠标滑过并离开格子后材质不会被改变。为了实现这个,我们需要调整OnMouseExit函数,如下:

1
2
3
4
5
function OnMouseExit()
{
    if(state == "idle" || state == "flagged")
        renderer.material = materialIdle;
}

  这样,如果格子没有被揭开,颜色只会变成黑色。试一下呗!你应该能揭开格子了。现在,即便格子上有炸弹,也不会有反应。

 

让空的格子被间接发现
  这是个难题。在扫雷游戏中,当你揭开一个格子时,挨着的空的格子就会被间接揭开,然后接着传开。这里考虑一下:

 

  事实上我们看不到炸弹和数字,只有平常的格子:

 

  当一个周围炸弹数为0的格子被揭开后,其周围的格子你会被自动揭开,然后传递给周围的格子。

 

  新揭开的格子会检测他们周围的格子信息,如果周围的格子也没有炸弹,也会被自动揭开。

 

  这样的传递会像波一样传递,直到临近的格子有炸弹才会停止传递。

 

  这就能实现扫雷中出现的空白区域的情况。为了能够实现这一功能,我们需要两个方法:UncoverAdjacentTile和UncoverTileExternal。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
private function UncoverAdjacentTiles()
{
    for(var currentTile: Tile in adjacentTiles)
    {
        //uncover all adjacent nodes with 0 adjacent mines
        if(!currentTile.isMined && currentTile.state == "idle" && currentTile.adjacentMines == 0)
            currentTile.UncoverTile();
         
        //uncover all adjacent nodes with more than 1 adjacent mine, then stop uncovering
        else if(!currentTile.isMined && currentTile.state == "idle" && currentTile.adjacentMines > 0)
            currentTile.UncoverTileExternal();
    }
}
public function UncoverTileExternal()
{
    state = "uncovered";
    displayText.renderer.enabled = true;
    renderer.material = materialUncovered;
}

  我们还得更新一下UncoverTile函数:

01
02
03
04
05
06
07
08
09
10
11
12
function UncoverTile()
{
    if(!isMined)
    {
        state = "uncovered";
        displayText.renderer.enabled = true;
        renderer.material = materialUncovered;
         
        if(adjacentMines == 0)
            UncoverAdjacentTiles();
    }
}

  当我们揭开一个格子,临近的格子中并没有炸弹,就会调用UncoverAdjacentTiles函数。然后就会检测附近的各自看看有没有炸弹。如果还是没有,就会自动揭开格子,然后进入下一轮的检测。如果附近有炸弹,就会只揭开当前的格子。我们需要进行小测试:为了能够容易出现空白区域,我们创建一个很大的区域9x9的81格子的游戏区域,这里只放置10个炸弹。

 

  现在游戏就可以运行了,当然现在还不能触发炸弹,这个功能之后会实现的。

炸弹的触发
  当我们揭开有炸弹的格子时,游戏就会结束,玩家也会输掉游戏。并且,其他所有的炸弹也会显示出来。为了实现这种效果,我们需要添加一个新的材质来表示爆炸的炸弹:

1
public var materialDetonated: Material;

  反正我建议使用红色表示炸弹爆炸。我们还需要另外两个方法去处理所有炸弹爆炸的效果:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
function Explode()
{
    state = "detonated";
    renderer.material = materialDetonated;
     
    for (var currentTile: Tile in Grid.tilesMined)
        currentTile.ExplodeExternal();
}
function ExplodeExternal()
{
    state = "detonated";
    renderer.material = materialDetonated;
}

  我们在UncoverTile函数中调用上面两个方法:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
function UncoverTile()
{
    if(!isMined)
    {
        state = "uncovered";
        displayText.renderer.enabled = true;
        renderer.material = materialUncovered;
         
        if(adjacentMines == 0)
            UncoverAdjacentTiles();
    }
    else
        Explode();
}

  如果格子内有炸弹,格子便会爆炸。Explode函数会发送“explode”给其他所有的炸弹,并且引爆他们。

 

赢得游戏
  当所有含炸弹的格子被正确地标定好旗子后,游戏便胜利了。这时候,那些没有被揭开的格子也会被自动揭开。那么我们怎么才能检测到这些?
让我们先在Grid类中添加一个state变量,这样我们才能检测到游戏目前的状态(游戏中,输了,赢了)。

1
static var state: String = "inGame";

  当我们处在某个游戏状态时,我们可以创建一个简单的GUI来呈现在屏幕上这些信息。我们可以用原生的GUI。

1
2
3
4
function OnGUI()
{
    GUI.Box(Rect(10,10,100,50), state);
}

  这个会告诉我们当前游戏所处状态,我们将这些状态分为inGame,gameOver和gameWon。我们也可以在Tile中检测,实现只有当前游戏状态为inGame后格子才能够响应。在OnMouseOver和OnMouseExit函数中,移除所有代码到一个if条件的代码块中,其判断条件为Grid.state是否为inGame,就像这样:

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
26
27
28
29
30
31
32
function OnMouseOver()
{
    if(Grid.state == "inGame")
    {
        if(state == "idle")
        {
            renderer.material = materialLightup;
            
            if (Input.GetMouseButtonDown(0))
                UncoverTile();
            
            if (Input.GetMouseButtonDown(1))
                SetFlag();
        }
        else if(state == "flagged")
        {
            renderer.material = materialLightup;
         
            if (Input.GetMouseButtonDown(1))
                SetFlag();
        }
    }
}
function OnMouseExit()
{
    if(Grid.state == "inGame")
    {
        if(state == "idle" || state == "flagged")
            renderer.material = materialIdle;
    }
}

  这里有两种方法检测游戏胜利:1.我们可以计算正确标记的炸弹的数量;2.我们还可以检测没有炸弹的格子是不是全都被揭开了。为了实现这个功能,我们需要一下变量,将他们添加到Grid类中:

1
2
3
static var minesMarkedCorrectly: int = 0;
static var tilesUncovered: int = 0;
static var minesRemaining: int = 0;

  别忘了在Start函数中将minesRemaining赋值给numberOfMines,其他的变量为0.Start函数如下:

01
02
03
04
05
06
07
08
09
10
function Start()
{
    CreateTiles();
     
    minesRemaining = numberOfMines;
    minesMarkedCorrectly = 0;
    tilesUncovered = 0;
     
    state = "inGame";
}

  最后一行设置了游戏的状态(这个在之后讲解restart函数中非常重要)。之后我们我们会在Update函数中检测游戏结束的条件:

1
2
3
4
5
6
7
8
function Update()
{
    if(state == "inGame")
    {
        if((minesRemaining == 0 && minesMarkedCorrectly == numberOfMines) || (tilesUncovered == numberOfTiles - numberOfMines))
            FinishGame();
    }
}

  我们可以通过改变状态为gameWon来结束游戏,揭开所有的格子,并且标记所有剩下的炸弹:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
function FinishGame()
{
    state = "gameWon";
     
    //uncovers remaining fields if all nodes have been placed
    for(var currentTile: Tile in tilesAll)     
        if(currentTile.state == "idle" && !currentTile.isMined)
            currentTile.UncoverTileExternal();
     
    //marks remaining mines if all nodes except the mines have been uncovered
    for(var currentTile: Tile in Grid.tilesMined)   
        if(currentTile.state != "flagged")
            currentTile.SetFlag();
}

  如果上面一切运行正常,我们还需要改下UncoverTile函数,如下:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
function UncoverTile()
{
    if(!isMined)
    {
        state = "uncovered";
        displayText.renderer.enabled = true;
        renderer.material = materialUncovered;
         
        Grid.tilesUncovered += 1;
         
        if(adjacentMines == 0)
            UncoverAdjacentTiles();
    }
    else
        Explode();
}

  UncoverTileExternal函数如下:

1
2
3
4
5
6
7
function UncoverTileExternal()
{
    state = "uncovered";
    displayText.renderer.enabled = true;
    renderer.material = materialUncovered;
    Grid.tilesUncovered += 1;
}

  我们还需要根据旗子是否被准确放置,来调整minesMarkedCorrextly和minesRemaining的值:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
function SetFlag()
{
    if(state == "idle")
    {
        state = "flagged";
        displayFlag.renderer.enabled = true;
        Grid.minesRemaining -= 1;
        if(isMined)
            Grid.minesMarkedCorrectly += 1;
    }
    else if(state == "flagged")
    {
        state = "idle";
        displayFlag.renderer.enabled = false;
        Grid.minesRemaining += 1;
        if(isMined)
            Grid.minesMarkedCorrectly -= 1;
    }
}

输掉游戏
  同样,我们还要设置输掉游戏的情况。我们可以在Explode函数中实现这个,在Explode添加如下代码:

1
Grid.state = "gameOver";

  当这个代码运行时,游戏的状态就转变成了gameOver,格子就不会再响应了:

给GUI添加更多的方法
  之前的步骤中我们用了UGUI来告诉玩家他们当前所处的游戏状态。现在我们要扩展来展示更多信息。代码如下:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
function OnGUI()
{
    if(state == "inGame")
    {
    }
    else if(state == "gameOver")
    {
     
    }
    else if(state == "gameWon")
    {
     
    }
}

  随着状态的改变,相应的信息就会在屏幕上显示出来。如果输掉游戏或者胜利,我们就能显示如下信息:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
function OnGUI()
{
    if(state == "inGame")
    {
    }
    else if(state == "gameOver")
    {
        GUI.Box(Rect(10,10,200,50), "You lose");
    }
    else if(state == "gameWon")
    {
        GUI.Box(Rect(10,10,200,50), "You rock!");
    }
}

  我们也可以显示被发现的炸弹数量,或者添加一个重置按钮:当游戏结束后,重新开始游戏。

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
26
27
function OnGUI()
{
    if(state == "inGame")
    {
        GUI.Box(Rect(10,10,200,50), "Mines left: " + minesRemaining);
    }
    else if(state == "gameOver")
    {
        GUI.Box(Rect(10,10,200,50), "You lose");
     
        if(GUI.Button(Rect(10,70,200,50), "Restart"))
            Restart();
    }
    else if(state == "gameWon")
    {
        GUI.Box(Rect(10,10,200,50), "You rock!");
     
        if(GUI.Button(Rect(10,70,200,50), "Restart"))
            Restart();
    }
}
function Restart()
{
    state = "loading";
    Application.LoadLevel(Application.loadedLevel);
}

  你可以测试下最终版的扫雷!

 

总结

  就这些了,我们就用Unity创建了一个属于你自己的扫雷游戏。我希望您能够喜欢这个教程,如果有任何问题,请在下面评论区留言。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引