"Sorry, I choose to fish" - "Minesweeper" small game development actual combat, algorithm, source code, based on Unity3D development

"I am participating in the individual competition of the Nuggets Community Game Creativity Contest. For details, please see: Game Creativity Contest "

Recommended reading

Hello everyone, I'm a Buddhist engineer ☆Quiet little magic dragon☆. I update Unity development skills from time to time. If you find it useful, remember to click three times.

I. Introduction

Today is fishing again... oh no.. A day at work, today there is a very classic Win system built-in game - "Minesweeper", it is said that after installing Win10, I can't find the "Minesweeper" game, which is very uncomfortable.

The gameplay of Minesweeper is to find a minefield without touching any mines.

After uncovering a block without mines, a number will be displayed to indicate the number of surrounding mines.

As usual, I will try to explain the steps in detail and comment the code so that everyone can understand.

2. Text

2-1. New project

(1) Project development, starting from the new project, the version of Unity I use is Unity 2019.4.7f1, the template is selected 2D, the project name is optional, just don’t Chinese:

image.png

(2) Create a directory, in the Project view, right-click to select Create→Folder, and create several new folders:

image.png

(3) The directory is as shown below:

image.png

  • Prefabs: Prefab resource folder
  • Scenes: Scene resource folder
  • Scripts: Script resource folder
  • Sprites: Image resource folder

2-2. Import resources

Next, import the required resources:

default.png null.png 1.png 2.png 3.png 4.png 5.png 6.png 7.png 8.png mine.png

Right-click all and save as a picture, and then import it into the Sprites folder of the Project view:

image.png

With all images selected, in the Inspector view, set the Pixels Per Unit to 16:

image.png

The reason why it is set to 16 is because 16X16 is a more suitable value in the game world.

2-3. Set camera properties

In the Hierarchy view, select the Main Cameras object, then find the Camera component in the Inspector view, and set the properties:

image.png

Note: Clear Flags is set to Skybox, Background is set as shown in the picture, and Size is set to 20.

2-4、制作默认方块

(1)将Project视图的Sprites目录中的default对象拖入Hierarchy视图中:

image.png

(2)选中default对象,在Inspector视图中,选择Add Componet→Physics 2D→Box Collider 2D,添加碰撞器组件:

image.png

注意:勾选Is Trigger

(3)选中default对象,拖回到Projcet视图的Prefabs文件夹内,做成一个预制体,我们将在后面的代码中去实例化生成它:

image.png

(4)Hierarchy视图中的default对象就可以删除了。

(5)新建脚本CreateBg.cs,在Projec视图的Scripts目录中,右击选择Create→C# Script:

image.png

双击打开脚本,编辑代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CreateBg : MonoBehaviour
{
    public GameObject block;//默认方块

    void Start()
    {
        //创建默认方块
        CreateBlock();
    }

    private void CreateBlock()
    {
        //创建方块父物体
        GameObject blockParent = new GameObject("blockParent");
        //创建10行10列的默认方块
        for (int i = 0; i < 10; i++)
        {
            for (int j = 0; j < 10; j++)
            {
                //Instantiate参数为:预制体 位置 旋转 父物体
                Instantiate(block, new Vector2(i, j), Quaternion.identity, blockParent.transform);
            }
        }
    }
}
复制代码

将脚本托给Main Camera对象,然后将预制体拖入Block卡槽中:

image.png

运行脚本:

image.png

是不是有点样子了,这个基本界面就做好了。

2-5、相邻的概念

让我们花一分钟的时间来分析一下相邻的概念,这是《扫雷》游戏中重要的一个部分。

单击一个非地雷的元素后,可以看到指示相邻地雷数量的数字,也就是这个数字的周围有这个数字的雷的数量,一共有9种情况:

image.png

因此,我们需要做的就是计算每个字段的相邻的地雷数量,然后得出数字,如果没有相邻的地雷,则为空。

2-6、制作数字和地雷

(1)新建一个脚本Element.cs,然后在Project视图的Prefabs文件夹中选中default对象,点击Add Componet→Element添加脚本:

image.png

(2)双击打开Element.cs,编辑代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Element : MonoBehaviour
{
    public bool mine;//判断是否是地雷

    // 不同的纹理
    public Sprite[] emptyTextures;
    public Sprite mineTexture;

    void Start()
    {
        // 随机决定它是否是地雷
        mine = Random.value < 0.15;
    }

    // 加载数字的纹理
    public void loadTexture(int adjacentCount)
    {
        if (mine)
            GetComponent<SpriteRenderer>().sprite = mineTexture;
        else
            GetComponent<SpriteRenderer>().sprite = emptyTextures[adjacentCount];
    }

    // 判断是否被点击
    public bool isCovered()
    {
        //判断当前纹理的名称是不是默认值
        return GetComponent<SpriteRenderer>().sprite.texture.name == "default";
    }

    // 鼠标点击
    void OnMouseUpAsButton()
    {
        // 是雷的话
        if (mine)
        {
            // 揭露所有雷
            // ...

            // 游戏结束
            Debug.Log("Game Over");
        }
        else
        {
            // 显示相邻的数字号
            // loadTexture(...);

            // 揭露没有地雷的地区  
            // ...

            // 判断游戏是否胜利
            // ...
        }
    }

}
复制代码

(3)选中default预制体,将对应的资源拖入Element.cs脚本的属性卡槽中:

image.png

(4)新建一个Grid.cs脚本,将脚本也添加到预制体default身上,Grid脚本将处理更加复杂的游戏逻辑,比如计算某个元素相邻的地雷,或者发现整个区域的无雷位置:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Grid : MonoBehaviour
{
    // 创建一个二维数组网格
    public static int w = 10; // 网格的长
    public static int h = 10; // 网格的高
    public static Element[,] elements = new Element[w, h];

    // 发现所有地雷
    public static void uncoverMines()
    {
        foreach (Element elem in elements)
            if (elem.mine)
                elem.loadTexture(0);
    }


    // 看看这个坐标上是否有地雷
    public static bool mineAt(int x, int y)
    {
        // 控制坐标范围
        if (x >= 0 && y >= 0 && x < w && y < h)
            return elements[x, y].mine;
        return false;
    }

    // 为一个元素计算相邻的地雷 8个方向
    public static int adjacentMines(int x, int y)
    {
        //计数器
        int count = 0;
        if (mineAt(x, y + 1)) ++count; // 上
        if (mineAt(x + 1, y + 1)) ++count; // 右上
        if (mineAt(x + 1, y)) ++count; // 右
        if (mineAt(x + 1, y - 1)) ++count; // 右下
        if (mineAt(x, y - 1)) ++count; // 下
        if (mineAt(x - 1, y - 1)) ++count; // 左下
        if (mineAt(x - 1, y)) ++count; // 做
        if (mineAt(x - 1, y + 1)) ++count; // 左上
        //返回相邻的地雷数量
        return count;
    }
}
复制代码

(5)修改Element.cs脚本代码:

Start函数修改:

void Start()
{
       // 随机决定它是否是地雷
       mine = Random.value < 0.15;

       // 在Grid注册
       int x = (int)transform.position.x;
       int y = (int)transform.position.y;
       Grid.elements[x, y] = this;
}
复制代码

OnMouseUpAsButton函数修改:

    // 鼠标点击
    void OnMouseUpAsButton()
    {
        // 是雷的话
        if (mine)
        {
            // 揭露所有雷
            Grid.uncoverMines();

            // 游戏结束
            Debug.Log("Game Over");
        }
        else
        {
            // 显示相邻的数字号
            int x = (int)transform.position.x;
            int y = (int)transform.position.y;
            loadTexture(Grid.adjacentMines(x, y));

            // 揭露没有地雷的地区  
            // ...

            // 判断游戏是否胜利
            // ...
        }
    }
复制代码

Run the program, you can see that after clicking a mine, you can see that other mines will also be found. After finding an element, you can see the adjacent numbers:

image.png

2-7, flooding algorithm

Ok, whenever the user finds an element without adjacent mines, the entire area without adjacent mines should be automatically discovered, like this:

aHR0cHM6Ly9ub29idHV0cy5jb20vY29udGVudC91bml0eS8yZC1taW5lc3dlZXBlci1nYW1lL3VuY292ZXJfbWluZWxlc3NfZWxlbWVudHMuZ2lm.gif

There are many algorithms that can do this, but by far the simplest algorithm is the flooding algorithm. If you understand recursion, the flooding algorithm is also well understood. Here is what the flooding algorithm does:

  • start with an element
  • Do what we want with this element
  • Continue recursively for each adjacent element

Then add the flooding algorithm to the Grid class:

    // 泛洪算法填充空元素
    public static void FFuncover(int x, int y, bool[,] visited)
    {
        if (x >= 0 && y >= 0 && x < w && y < h)
        {
            // 判断是否遍历过
            if (visited[x, y])
                return;

            // 设置遍历标识
            visited[x, y] = true;

            // 递归
            FFuncover(x - 1, y, visited);
            FFuncover(x + 1, y, visited);
            FFuncover(x, y - 1, visited);
            FFuncover(x, y + 1, visited);
        }
    }
复制代码

Note: The flooding algorithm recursively visits the surrounding elements of an element until it reaches each element.

Then modify our flooding algorithm, which should find out if the element it visits is a mine, and if so it shouldn't continue:

    // 泛洪算法填充空元素
    public static void FFuncover(int x, int y, bool[,] visited)
    {
        if (x >= 0 && y >= 0 && x < w && y < h)
        {
            // 判断是否遍历过
            if (visited[x, y])
                return;

            // 发现元素
            elements[x, y].loadTexture(adjacentMines(x, y));

            // 发现地雷
            if (adjacentMines(x, y) > 0)
                return;

            // 设置遍历标识
            visited[x, y] = true;

            // 递归
            FFuncover(x - 1, y, visited);
            FFuncover(x + 1, y, visited);
            FFuncover(x, y - 1, visited);
            FFuncover(x, y + 1, visited);
        }
    }
复制代码

Back in the Element.cs script, modify the OnMouseUpAsButton function to use this algorithm to find all empty elements when the user clicks one of the elements:

    // 鼠标点击
    void OnMouseUpAsButton()
    {
        // 是雷的话
        if (mine)
        {
            // 揭露所有雷
            Grid.uncoverMines();

            // 游戏结束
            Debug.Log("Game Over");
        }
        else
        {
            // 显示相邻的数字号
            int x = (int)transform.position.x;
            int y = (int)transform.position.y;
            loadTexture(Grid.adjacentMines(x, y));

            // 揭露没有地雷的地区  
            Grid.FFuncover(x, y, new bool[Grid.w, Grid.h]);

            // 判断游戏是否胜利
            // ...
        }
    }
复制代码

Run the program, when an empty element is found, it will traverse to find if there are empty elements without mines around:

aH.gif

2-8. Determine whether all mines have been found

Next, it is necessary to determine whether the player has found all the mines, then the game should be over.

Then modify the code of the Grid class and add the function isFinished:

    // 是否找到所有地雷

    public static bool isFinished()
    {
        // 遍历数组 找到没有被地雷覆盖的元素
        foreach (Element elem in elements)
            if (elem.isCovered() && !elem.mine)
                return false;
        // 没有找到 => 全是地雷 => 游戏胜利.
        return true;
    }
复制代码

Modify the code of Element.cs:

    // 鼠标点击
    void OnMouseUpAsButton()
    {
        // 是雷的话
        if (mine)
        {
            // 揭露所有雷
            Grid.uncoverMines();

            // 游戏结束
            Debug.Log("Game Over");
        }
        else
        {
            // 显示相邻的数字号
            int x = (int)transform.position.x;
            int y = (int)transform.position.y;
            loadTexture(Grid.adjacentMines(x, y));

            // 揭露没有地雷的地区  
            Grid.FFuncover(x, y, new bool[Grid.w, Grid.h]);

            // 判断游戏是否胜利
            if (Grid.isFinished())
                Debug.Log("Game Win");
        }
    }
复制代码

Run the program, you can play the game happily.

3. Summary

The general framework of the "Minesweeper" game has been developed, of course, you can also add some elements to make the game more interesting:

  • Mark mines with markers
  • Divided into more difficulty, such as easy, medium, hard
  • Switch to a more beautiful UI
  • Win and lose screen and restart
  • Add sound effects

Guess you like

Origin juejin.im/post/7083735364233330725