在这个系列的最后,我们将实现扫雷游戏点击网格功能,让我们的游戏更具可玩性。我们还要实现玩家能够赢得或输掉游戏的功能。如果你完成之前的教程,游戏现在应该就能够创建格子的区域,然后随机地把炸弹赋值给他们。当玩家用鼠标在格子上划过就会出现高亮效果,还能够放置和移除旗子。每个格子之间还能够通信,还能够计算出附近的炸弹数。
揭开格子
我们已经添加了右键点击防止旗子的功能,现在,让我们添加左键揭开格子的功能。在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!
| function UncoverTile() { if(!isMined) { state = "uncovered"; displayText.renderer.enabled = true; renderer.material = materialUncovered; } else Explode(); } |
如果运行成功了,我们需要声明一个新的材质。
| public var materialUncovered: Material; |
创建区别于基础色的颜色。就比如你的基础色为蓝色,你就应该选择绿色代表已经揭开的状态。千万别选择红色,因为接下来我们会用这个颜色表示揭开炸弹后的状态。当我们调用这个函数后,会发生如下事情:
· 第一步会检查格子是否确实有炸弹;
· 如果没有炸弹,这个区域便会被设置为uncovered状态,激活文本显示状态,向我们显示周围炸弹数,然后设置uncovered材质;
· 接着,格子不能够被点击第二次,也不会重复高亮,这就意味着格子响应事件只会在鼠标真正点击之后发生。
在我们运行测试前,我们需要确保当鼠标滑过并离开格子后材质不会被改变。为了实现这个,我们需要调整OnMouseExit函数,如下:
| 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函数:
| function UncoverTile() { if(!isMined) { state = "uncovered"; displayText.renderer.enabled = true; renderer.material = materialUncovered; if(adjacentMines == 0) UncoverAdjacentTiles(); } } |
当我们揭开一个格子,临近的格子中并没有炸弹,就会调用UncoverAdjacentTiles函数。然后就会检测附近的各自看看有没有炸弹。如果还是没有,就会自动揭开格子,然后进入下一轮的检测。如果附近有炸弹,就会只揭开当前的格子。我们需要进行小测试:为了能够容易出现空白区域,我们创建一个很大的区域9x9的81格子的游戏区域,这里只放置10个炸弹。
现在游戏就可以运行了,当然现在还不能触发炸弹,这个功能之后会实现的。
炸弹的触发
当我们揭开有炸弹的格子时,游戏就会结束,玩家也会输掉游戏。并且,其他所有的炸弹也会显示出来。为了实现这种效果,我们需要添加一个新的材质来表示爆炸的炸弹:
| 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变量,这样我们才能检测到游戏目前的状态(游戏中,输了,赢了)。
| static var state: String = "inGame"; |
当我们处在某个游戏状态时,我们可以创建一个简单的GUI来呈现在屏幕上这些信息。我们可以用原生的GUI。
| 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类中:
| static var minesMarkedCorrectly: int = 0; static var tilesUncovered: int = 0; static var minesRemaining: int = 0; |
别忘了在Start函数中将minesRemaining赋值给numberOfMines,其他的变量为0.Start函数如下:
| function Start() { CreateTiles(); minesRemaining = numberOfMines; minesMarkedCorrectly = 0; tilesUncovered = 0; state = "inGame"; } |
最后一行设置了游戏的状态(这个在之后讲解restart函数中非常重要)。之后我们我们会在Update函数中检测游戏结束的条件:
| 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函数如下:
| 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添加如下代码:
当这个代码运行时,游戏的状态就转变成了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创建了一个属于你自己的扫雷游戏。我希望您能够喜欢这个教程,如果有任何问题,请在下面评论区留言。