【Unity】MiniGame编辑器小游戏(二)扫雷【Minesweeper】

更新日期:2025年6月17日。

项目源码:后续章节发布

索引

扫雷【Minesweeper】

本篇的目标是开发一个扫雷【Minesweeper】小游戏。

一、游戏最终效果

Unity编辑器小游戏:扫雷

二、玩法简介

扫雷是一款广受欢迎的益智类单人游戏,其玩法简单却富有挑战性。

玩家需要在不触发地雷的前提下,揭开所有安全的方格,并正确标记出所有地雷的位置。游戏界面由一个方格网格组成,每个方格可能隐藏着地雷或数字。数字表示该方格周围8个相邻方格中地雷的数量。

游戏开始时,所有方格都是隐藏的,玩家通过点击方格来揭示其内容。根据揭示的信息,玩家需要通过逻辑推理逐步排除雷区,找出所有安全区域。

三、正式开始

1.定义游戏窗口类

首先,定义扫雷的游戏窗口类MiniGame_Minesweeper,其继承至MiniGameWindow【小游戏窗口基类】

csharp 复制代码
    /// <summary>
    /// 扫雷
    /// </summary>
    public class MiniGame_Minesweeper : MiniGameWindow
    {
    
    }

2.规划游戏窗口、视口区域

通过覆写虚属性实现规划游戏视口区域大小:

csharp 复制代码
        /// <summary>
        /// 游戏名称
        /// </summary>
        public override string Name => "扫雷 [Minesweeper]";
        /// <summary>
        /// 游戏窗体大小
        /// </summary>
        public override Vector2 WindowSize => new Vector2(700, 530);
        /// <summary>
        /// 游戏视口区域
        /// </summary>
        public override Rect ViewportRect => new Rect(5, 25, 500, 500);

注意:游戏窗体大小必须 > 游戏视口区域。

然后通过代码打开此游戏窗口:

csharp 复制代码
        [MenuItem("MiniGame/扫雷 [Minesweeper]", priority = 2)]
        private static void Open_MiniGame_Minesweeper()
        {
            MiniGameWindow.OpenWindow<MiniGame_Minesweeper>();
        }

便可以看到游戏的窗口、视口区域如下(左侧深色凹陷区域为视口区域):

3.地图方块阵列

扫雷游戏的背景也是由一系列方块组成的,所以我们先来绘制如下这样的地图方块阵列:

①.定义方块结构体

首先,定义方块结构体Block,其代表方块阵列中的一个方块:

csharp 复制代码
        /// <summary>
        /// 地块
        /// </summary>
        public struct Block
        {
            /// <summary>
            /// 地块位置
            /// </summary>
            public Rect Position;
            /// <summary>
            /// 是否为地雷
            /// </summary>
            public bool IsMine;
            /// <summary>
            /// 周围的地雷数量(仅当不为地雷时)
            /// </summary>
            public string AroundMineCount;
            /// <summary>
            /// 是否已翻开地块
            /// </summary>
            public bool IsOpened;
            /// <summary>
            /// 是否已标记为地雷
            /// </summary>
            public bool IsSigned;
        }
②.生成方块阵列

我们设计如下三种难度等级的关卡:

名称 地图大小 地雷数量
初级 9*9 10
中级 16*16 40
高级 25*25 99
csharp 复制代码
        private readonly string[] LEVELS = new string[] { "初级(9*9)", "中级(16*16)", "高级(25*25)" };

所以游戏视口的宽度、高度是根据高级难度(方块尺寸20 * 方块宽高25 = 500)的大小来设置的:

csharp 复制代码
        private const int BLOCKSIZE = 20;

根据选择的不同难度,来生成对应的地图方块阵列:

csharp 复制代码
		//地图宽度
        private static int WIDTH = 9;
        //地图高度
        private static int HEIGHT = 9;
        //地雷总数
        private static int MINECOUNT = 10;
        
        //已标记地雷数量
        private int _signedCount = 0;
        private int _level = 0;
        private Block[,] _panel;

        /// <summary>
        /// 开始游戏
        /// </summary>
        private void StartGame()
        {
            if (_level == 0)
            {
                WIDTH = 9;
                HEIGHT = 9;
                MINECOUNT = 10;
            }
            else if (_level == 1)
            {
                WIDTH = 16;
                HEIGHT = 16;
                MINECOUNT = 40;
            }
            else if (_level == 2)
            {
                WIDTH = 25;
                HEIGHT = 25;
                MINECOUNT = 99;
            }

            GenerateMines();

            _signedCount = 0;
        }
        /// <summary>
        /// 生成地雷
        /// </summary>
        private void GenerateMines()
        {
            //生成地图
            _panel = new Block[WIDTH, HEIGHT];
            for (int h = 0; h < HEIGHT; h++)
            {
                for (int w = 0; w < WIDTH; w++)
                {
                    _panel[w, h].Position = new Rect(w * BLOCKSIZE, h * BLOCKSIZE, BLOCKSIZE, BLOCKSIZE);
                    _panel[w, h].IsMine = false;
                    _panel[w, h].AroundMineCount = "";
                    _panel[w, h].IsOpened = false;
                    _panel[w, h].IsSigned = false;
                }
            }
        }
③.绘制方块阵列

然后在OnGameViewportGUI方法中绘制方块阵列:

csharp 复制代码
		//已翻开地雷的GUIContent
        private GUIContent _mineGC;
        //未翻开、已标记方块的GUIContent
        private GUIContent _signGC;
        //已翻开方块的GUIStyle
        private GUIStyle _openedGS;
        //未翻开方块的GUIStyle
        private GUIStyle _closeGS;
        
        protected override void OnGameViewportGUI()
        {
            base.OnGameViewportGUI();
            
            DrawPanel();
        }

        /// <summary>
        /// 绘制画布
        /// </summary>
        private void DrawPanel()
        {
            for (int h = 0; h < HEIGHT; h++)
            {
                for (int w = 0; w < WIDTH; w++)
                {
                    DrawBlock(w, h);
                }
            }
        }
        /// <summary>
        /// 绘制方块
        /// </summary>
        private void DrawBlock(int x, int y)
        {
            if (_panel[x, y].IsOpened)
            {
                if (_panel[x, y].IsMine)
                {
                	//如果已翻开,且为地雷,则直接显示地雷
                    GUI.color = Color.red;
                    GUI.Box(_panel[x, y].Position, _mineGC, _openedGS);
                    GUI.color = Color.white;
                }
                else
                {
                	//如果已翻开,且不为地雷,则显示其周围的地雷数量
                    GUI.Box(_panel[x, y].Position, _panel[x, y].AroundMineCount, _openedGS);
                }
            }
            else
            {
                if (_panel[x, y].IsSigned)
                {
                	//如果未翻开,且已标记,显示标记图标
                    GUI.Box(_panel[x, y].Position, _signGC, _closeGS);
                }
                else
                {
                	//如果未翻开,且未标记,显示空白按钮
                    GUI.Box(_panel[x, y].Position, "", _closeGS);
                }
            }
        }

此时就能绘制出游戏的地图方块阵列了,比如初级(9*9)的:

注意:这里有一个选择关卡难度的过程省略了,该过程很简单便不浪费篇幅赘述了,后续在源码中即可一目了然。

四种状态的方块绘制出来大致是这样的:

4.随机生成地雷

到此时,地图上的所有方块虽然有了,但其中一个地雷也没有,所以我们需要根据当前关卡难度的总地雷数,来随机生成地雷:

回到GenerateMines方法中:

csharp 复制代码
        /// <summary>
        /// 生成地雷
        /// </summary>
        private void GenerateMines()
        {
            //生成地图
            //......

            //将地图中所有方块的索引存入一个数组
            int[] indexs = new int[WIDTH * HEIGHT];
            for (int i = 0; i < indexs.Length; i++)
            {
                indexs[i] = i;
            }
            //使用洗牌算法打乱所有方块索引数组
            Utility.ShuffleAlgorithm(indexs, MINECOUNT);
            //从方块索引数组的坐标0开始,直接设置所有地雷
            //由于方块索引数组已打乱,所以此时设置的地雷位置是随机的
            for (int i = 0; i < MINECOUNT; i++)
            {
                int index = indexs[i];
                int row = index / WIDTH;
                int col = index % WIDTH;
                //设置为地雷
                _panel[row, col].IsMine = true;
            }

            //刷新地雷周围地块上显示的地雷数量
            for (int h = 0; h < HEIGHT; h++)
            {
                for (int w = 0; w < WIDTH; w++)
                {
                    _panel[w, h].AroundMineCount = CalculateAroundMineCount(w, h);
                }
            }
        }

5.刷新方块周围的地雷数量

这个方法就很简单,发现一个地雷就++就行:

csharp 复制代码
        /// <summary>
        /// 计算一个地块周围的地雷数量(直接返回string类型,用于GUI显示,省去了类型转换过程)
        /// </summary>
        private string CalculateAroundMineCount(int x, int y)
        {
            if (_panel[x, y].IsMine)
                return "";

            int count = 0;
            for (int i = -1; i <= 1; i++)
            {
                for (int j = -1; j <= 1; j++)
                {
                    if (i == 0 && j == 0)
                        continue;

                    int newX = x + i;
                    int newY = y + j;
                    if (newX >= 0 && newX < WIDTH && newY >= 0 && newY < HEIGHT && _panel[newX, newY].IsMine)
                    {
                        count++;
                    }
                }
            }
            //如果周围一个地雷也没有,就显示空白
            return count > 0 ? count.ToString() : "";
        }

6.翻开地块、标记地块

接下来是最重要的功能翻开地块、标记地块,在OnGamePlayingEvent方法中,分别使用鼠标左键翻开地块,鼠标右键标记地块:

csharp 复制代码
        protected override void OnGamePlayingEvent(Event e, Vector2 mousePosition)
        {
            base.OnGamePlayingEvent(e, mousePosition);

            if (e.type == EventType.MouseDown)
            {
                if (e.button == 0)
                {
                    for (int h = 0; h < HEIGHT; h++)
                    {
                        for (int w = 0; w < WIDTH; w++)
                        {
                            if (!_panel[w, h].IsOpened)
                            {
                            	//如果鼠标左键点在了某一方块上
                                if (_panel[w, h].Position.Contains(mousePosition))
                                {
                                	//翻开该方块
                                    OpenBlock(w, h);
                                    Repaint();
                                    return;
                                }
                            }
                        }
                    }
                }
                else if (e.button == 1)
                {
                    for (int h = 0; h < HEIGHT; h++)
                    {
                        for (int w = 0; w < WIDTH; w++)
                        {
                            if (!_panel[w, h].IsOpened)
                            {
                            	//如果鼠标右键点在了某一方块上
                                if (_panel[w, h].Position.Contains(mousePosition))
                                {
                                	//标记该方块
                                    SignBlock(w, h);
                                    Repaint();
                                    return;
                                }
                            }
                        }
                    }
                }
            }
        }
①.连锁翻方块

不过需注意的时,翻开方块时,如果翻到了空白方块(周围地雷数为0),则应触发连锁翻方块功能。

连锁翻方块:从当前空白方块开始,尝试翻开周围的所有邻居方块,遇到不为空白的方块则停止。

csharp 复制代码
        /// <summary>
        /// 翻开地块
        /// </summary>
        private void OpenBlock(int x, int y)
        {
            if (_panel[x, y].IsOpened)
                return;

            _panel[x, y].IsOpened = true;

			//如果翻开的方块为地雷,则游戏失败
            if (_panel[x, y].IsMine)
            {
                IsGameOvered = true;
            }
            else
            {
            	//如果翻开的方块为空白方块,则连锁翻方块
                if (_panel[x, y].AroundMineCount == "")
                {
                    OpenNeighborBlock(x, y);
                }

				//每翻开一个方块,检测游戏是否通关
                IsGameSuccessed = CheckSuccess();
            }
        }
        /// <summary>
        /// 尝试翻开周围的空白地块
        /// </summary>
        private void OpenNeighborBlock(int x, int y)
        {
            for (int i = -1; i <= 1; i++)
            {
                for (int j = -1; j <= 1; j++)
                {
                    if (i == 0 && j == 0)
                        continue;

                    int newX = x + i;
                    int newY = y + j;
                    if (newX >= 0 && newX < WIDTH && newY >= 0 && newY < HEIGHT && !_panel[newX, newY].IsMine)
                    {
                        OpenBlock(newX, newY);
                    }
                }
            }
        }

注意:

1.直接设置IsGameOvered = true则表示游戏失败,将自动弹出Game Over界面;

2.直接设置IsGameSuccessed = true则表示游戏通关,将自动弹出Game Success界面;

②.标记方块

标记方块的逻辑就比翻开方块的逻辑简单得多:

csharp 复制代码
        /// <summary>
        /// 标记地块
        /// </summary>
        private void SignBlock(int x, int y)
        {
            if (_panel[x, y].IsSigned)
            {
            	//如果已标记,则取消标记
                _panel[x, y].IsSigned = false;
                _signedCount--;
            }
            else
            {
          		 //已标记的地雷数量未超上限,才允许标记
                if (_signedCount < MINECOUNT)
                {
                	//标记地雷
                    _panel[x, y].IsSigned = true;
                    _signedCount++;

                    IsGameSuccessed = CheckSuccess();
                }
            }
        }

7.检测游戏是否通关

检测游戏是否通关的逻辑为:翻开所有非地雷方块,标记所有地雷方块:

csharp 复制代码
        /// <summary>
        /// 检测游戏是否通关
        /// </summary>
        private bool CheckSuccess()
        {
            if (_signedCount != MINECOUNT)
                return false;

            for (int h = 0; h < HEIGHT; h++)
            {
                for (int w = 0; w < WIDTH; w++)
                {
                    if (_panel[w, h].IsMine)
                    {
                        if (!_panel[w, h].IsSigned) return false;
                    }
                    else
                    {
                        if (!_panel[w, h].IsOpened) return false;
                    }
                }
            }

            return true;
        }

8.绘制游戏操作说明

最后,操作说明等其他UI统一绘制在OnOtherGUI方法中:

csharp 复制代码
        protected override void OnOtherGUI()
        {
            base.OnOtherGUI();

            if (_isReady)
                return;
            
            //剩余地雷数量
            Rect rect = new Rect(ViewportRect.x + ViewportRect.width + 5, ViewportRect.y + 5, 50, 20);
            GUI.Label(rect, "Mines:");
            rect.x += 50;
            rect.width = 100;
            EditorGUI.IntField(rect, MINECOUNT - _signedCount);

            rect.Set(ViewportRect.x + ViewportRect.width + 5, ViewportRect.y + ViewportRect.height - 50, 80, 20);
            GUI.Button(rect, "Mouse Left");
            rect.x += 85;
            rect.width = 100;
            GUI.Label(rect, "Open block");

            rect.Set(ViewportRect.x + ViewportRect.width + 5, ViewportRect.y + ViewportRect.height - 25, 80, 20);
            GUI.Button(rect, "Mouse Right");
            rect.x += 85;
            rect.width = 100;
            GUI.Label(rect, "Marked as mines");
        }

这里绘制出来的效果如下:

至此,一个简单的扫雷小游戏就完成了,试玩效果如下:扫雷【Minesweeper】

9.暂停游戏、退出游戏

俄罗斯方块

相关推荐
Web极客码10 小时前
WordPress从经典编辑器升级到古腾堡编辑器
运维·编辑器·wordpress
江湖有缘10 小时前
Docker部署music-tag-web音乐标签编辑器
前端·docker·编辑器
在路上看风景20 小时前
4.5 顶点和片元
unity
LYOBOYI1231 天前
vscode界面美化
ide·vscode·编辑器
浔川python社1 天前
关于浔川代码编辑器 v5.0 网页版上线时间的通知
编辑器
在路上看风景1 天前
31. Unity 异步加载的底层细节
unity
天人合一peng1 天前
Unity中做表头时像work中整个调整宽窄
unity
浔川python社1 天前
浔川代码编辑器 v5.0 上线时间公布
编辑器
山峰哥2 天前
数据库工程与SQL调优——从索引策略到查询优化的深度实践
数据库·sql·性能优化·编辑器
Doro再努力2 天前
Vim 快速上手实操手册:从入门到生产环境实战
linux·编辑器·vim