作为一个扫雷玩家,想重现这么经典的游戏了~于是我就试着用unity做了一个尝试,具体并不难,算是一个练习吧。我会在这里写出大致的思路,想练习的同学可以照着试试。
1.总体思路:
//手写,字不好,请见谅。
2.主要脚本
3.格子控制器
以每一个格子为单位进行操作,一个格子挂一个PressManager,里面存储了格子的信息,包括坐标,是非是雷,状态等,方便判定。格子下有图片与用来显示数字的文本。
1.改变状态方法:
用枚举提高可读性,状态值有
unopen,signed,opened,mine,worrySign,doom六种。
设立一个方法,改变格子的显示,同时改变它的状态。
2.点击的获取
这个工程中涉及较复杂的鼠标事件,我用了EventSystem来处理。具体是:创建pointerEnter方法与pointerExit方法,分别关联两个方法,然后创建一个bool变量,enter将他变为true,exit将他变为false,这样它就表示鼠标在格子里,然后Input.GetMouseButtonUp(0),即可获得点击格子的判定。
3.点击方法:
左键点击,判断,格子是未打开时,调用主控制器方法,传递坐标,剩下的交给主控完成。
右键点击可以让格子在标记与未打开间循环。
中键点击时,判断,当格子是打开状态时,通知主控。
4.两个接口方法改变显示
一个改变图片,一个改变文本。
4.主控制器
1.存储信息
,包括一个用来存储格子的二维数组,横宽有几个格子,总雷数信息,剩余的空格(用来判定胜利)剩余雷数。
2.初始化方法
删除旧网格,创建新网格,把格子存到数组里,初始化每个格子的数据(name,位置,坐标,初始状态等)初始化所有全局变量,反正所有东西都要new一遍。
public void Restart() { for (int i = 0; i < allPress.GetLength(0); i++) { for (int j = 0; j < allPress.GetLength(1); j++) { if (allPress[i, j] != null) { Destroy(allPress[i, j].gameObject); allPress[i, j] = null; } } } for (int i = 0; i < MaxX; i++) { for (int j = 0; j < MaxY; j++) { PressController newGameObject = Instantiate(pressProabs, transform).GetComponent<PressController>(); allPress[i, j] = newGameObject; newGameObject.gameObject.GetComponent<RectTransform>().anchoredPosition = new Vector2(30 * i + 30 , -30 * j -30); newGameObject.gameObject.name = "Press("+i+","+j+")"; newGameObject.x = i; newGameObject.y = j; newGameObject.OnVuleChanged(PressType.unopen); } } GameOver = false; UIManager.instance.optionGO.SetActive(false); UIManager.instance.time = 0f; space = MaxX * MaxY; mine = MaxMineNumber; UIManager.instance.UIRestart(); firstClick = true; }//初始化,重置格子
3.通用方法:OperateNearby与CalculateNearby
OperateNearby用来对附近所有格子进行操作,这里类似于泛洪算法。
可以打开附近所有格子,标记附近所有格子。
CalculateNearby用来计算附近格子处于某种状态的数量,
比如雷的数量,已标记的数量,未打开的数量。
注意这两个方法不能越界。
这两个操作用的多,先封装起来,使用时输入参数即可。标识位使方法进行分化,最好用枚举。
void OperateNearby(int x,int y,int n)//操作附近格子,n为标志位,0打开1标记 { //Debug.Log("something"); for (int i = -1; i <= 1; i++) { for (int j = -1; j <= 1; j++) { int TrueX = x + i, TrueY = y + j; if (i == 0 && j == 0) continue; if (TrueX < 0 || TrueX >= MaxX || TrueY < 0 || TrueY >= MaxY) continue; if (allPress[TrueX, TrueY].pressType == PressType.unopen) //下面为标志位区分的部分,0表示打开格子,1表示标记格子 switch (n){ case 0: instance.Onclick(TrueX, TrueY); break; case 1: if (allPress[TrueX, TrueY].pressType == PressType.unopen) { allPress[TrueX, TrueY].OnVuleChanged(PressType.signed); instance.mine--; } break; } } } }
int CalculateNearby(int x,int y,int n)//计算附近格子状态数,n为标志位,0雷1已标记2未打开 { int number = 0; for (int i = -1; i <= 1; i++) { for (int j = -1; j <= 1; j++) { int TrueX = x + i, TrueY = y + j; if (i == 0 && j == 0) continue; if (TrueX < 0 || TrueX >= MaxX || TrueY < 0 || TrueY >= MaxY) continue; switch (n) { case 0: if (allPress[TrueX, TrueY].ismine) number++;break; case 1: if (allPress[TrueX, TrueY].pressType == PressType.signed) number++; break; case 2: if (allPress[TrueX, TrueY].pressType == PressType.unopen) number++; break; } } } return number; }
4.处理左键点击格子事件
分情况,第一次点击时,布下雷区,不是第一次时,判断是否触雷,触雷调用died方法,没触雷打开格子,如果格子附近雷数为0,打开附近的格子。
public void Onclick(int x, int y) { if (firstClick) { for (int k = 0; k < MaxMineNumber;) { int MineX = Random.Range(0, MaxX), MineY = Random.Range(0, MaxY); if (Mathf.Abs(MineX - x) <= 1 && Mathf.Abs(MineY - y) <= 1) continue; //不能不在第一次点击格子附近 if (allPress[MineX, MineY].ismine) continue; //已经布雷,换个地方 allPress[MineX, MineY].ismine = true; k++; firstClick = false;//这是个boll变量,用来判断是否是第一次点击 UIManager.instance.beginTime = true; } Onclick(x, y); }//首次点击,放置雷 else{ if (allPress[x, y].ismine) { allPress[x, y].OnVuleChanged(PressType.doom); Died(); } else { if (allPress[x, y].pressType == PressType.unopen) { int mineNumber = CalculateNearby(x, y, 0); allPress[x, y].OnVuleChanged(PressType.opened); allPress[x, y].ShowMineNumber(mineNumber); space--; if (space - MaxMineNumber <= 0) Win();//胜利判断 if (mineNumber == 0) OperateNearby(x, y, 0);//若数字为0,打开附近 } //如果是空格,自动打开附近的格子 } } }//打开格子,第一次点击时放置雷
5.处理中键点击格子事件
由于实现定义了两个方法,这里就比较简单了。
public void OnMouse3Click(int x, int y,int mineNumber)//按下中键的事件,1打开2标记 { if (mineNumber== CalculateNearby(x, y, 1)) OperateNearby(x, y, 0); //计算附近的标记数量,如果标出所有雷,打开附近 if (mineNumber == CalculateNearby(x, y, 2)+ CalculateNearby(x, y, 1)) OperateNearby(x, y, 1); //计算附近未打开的格子,如果等于雷数数字,全部标记 }
6.触雷的处理与胜利判断
控制UI显示失败,比较简单不说了
每次打开空格时,将剩余空格数-1,当剩余空格+总雷数>=横*宽时胜利,显示UI。
5.UI控制器
我使用一个UI控制器控制UI,以简化脚本。
1.更新小人图标,雷数与时间
在Update里,注意计时器在第一次点击开始,胜利或触雷时停止
2.根据格子数控制窗口大小与布局
public void UIRestart() { crossRTF.sizeDelta = new Vector2( 30 * (MainController.MaxX + 1), 30 * (MainController.MaxY + 1)); //控制格子背景大小,正好可以放下所有格子 crossRTF.anchoredPosition = new Vector2(0, crossRTF.sizeDelta.y/2f+25); //重置格子的位置 Screen.SetResolution ( 80 + 30 * MainController.MaxX, 155 + 30 * MainController.MaxY, false );//控制游戏窗口大小的语句 }//根据格子数控制窗口大小与布局
这里注意各个UI元素锚点的设置。
6.设置界面
用来设置宽高与雷数7.其他:
还有一些更新雷数,记时之类的,太简单就不写了。
全文完,感谢阅读!