【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.暂停游戏、退出游戏

俄罗斯方块

相关推荐
nnsix9 小时前
Unity PicoVR开发 实时预览Unity场景 在Pico设备中(串流)
unity·游戏引擎
一只一只14 小时前
Unity之UGUI Button按钮组件详细使用教程
unity·游戏引擎·ugui·button·ugui button
智源研究院官方账号16 小时前
众智FlagOS 1.6发布,以统一架构推动AI硬件、软件技术生态创新发展
数据库·人工智能·算法·架构·编辑器·硬件工程·开源软件
WarPigs17 小时前
Unity阴影
unity·游戏引擎
一只一只18 小时前
Unity之Invoke
unity·游戏引擎·invoke
咬人喵喵20 小时前
SVG 答题类互动模板汇总(共 16 种/来自 E2 编辑器)
编辑器·svg·e2 编辑器
tealcwu21 小时前
【Unity踩坑】Simulate Touch Input From Mouse or Pen 导致检测不到鼠标点击和滚轮
unity·计算机外设·游戏引擎
漫步星河21 小时前
unityEditor Note 编辑器笔记本
编辑器
ThreePointsHeat21 小时前
Unity WebGL打包后启动方法,部署本地服务器
unity·游戏引擎·webgl
咬人喵喵21 小时前
16 类春节核心 SVG 交互方案拆解(E2 编辑器实战)
前端·css·编辑器·交互·svg