unity小项目——扫雷

时间:2022-01-12 21:01:25

    作为一个扫雷玩家,想重现这么经典的游戏了~于是我就试着用unity做了一个尝试,具体并不难,算是一个练习吧。我会在这里写出大致的思路,想练习的同学可以照着试试。

1.总体思路:

unity小项目——扫雷

//手写,字不好,请见谅。

2.主要脚本

unity小项目——扫雷

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.设置界面

用来设置宽高与雷数

unity小项目——扫雷

7.其他:

还有一些更新雷数,记时之类的,太简单就不写了。

全文完,感谢阅读!