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

俄罗斯方块

相关推荐
C++ 老炮儿的技术栈2 小时前
C++实现手写strlen函数
大数据·c语言·c++·编辑器
龚子亦3 小时前
【数字人开发】Unity+百度智能云平台实现短语音文本识别功能
百度·unity·游戏引擎
nvvas8 小时前
JETBRAINS IDE 开发环境自定义设置快捷键
pycharm·编辑器·intellij-idea·idea
小飞大王66617 小时前
简单实现HTML在线编辑器
前端·编辑器·html
RocketJ1 天前
推荐使用的Unity插件(行为树Behavior )
unity·游戏引擎
Tatalaluola1 天前
【Quest开发】初始项目环境配置
unity·游戏引擎·vr
future14121 天前
FairyGUI学习
学习·游戏·ui·unity
CSDN_RTKLIB2 天前
Vim 介绍:从编辑器到高效工作的利器
linux·编辑器·vim
点量云实时渲染-小芹2 天前
UE/Unity/Webgl云渲染推流网址,如何与外部网页嵌套和交互?
unity·webgl·webgl云渲染网页交互·点量云流