“对不起,我选择摸鱼”—《扫雷》小游戏开发实战,算法、源代码,基于Unity3D开发

1,245 阅读5分钟

“我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛

推荐阅读

大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有用记得一键三连哦。

一、前言

今天又是摸鱼...哦不..工作的一天,今天整一个很经典的Win系统自带游戏——《扫雷》,话说安装Win10之后就找不到《扫雷》游戏了,很难受。

《扫雷》游戏的玩法是,在不触碰到任何地雷的情况下去发现一个雷区。

揭开一个没有地雷的块之后,将会显示一个数字来表示周围地雷的数量。

跟往常一样,会尽量的将步骤详细说明,代码也进行注释,可以让大家都可以理解。

二、正文

2-1、新建项目

(1)项目开发,从新建项目开始,我使用的Unity版本是Unity 2019.4.7f1,模板就选择2D,项目名称随意,别中文就行:

image.png

(2)创建目录,在Project视图,右击选择Create→Folder,新建几个文件夹:

image.png

(3)目录如下图所示:

image.png

  • Prefabs:预制体资源文件夹
  • Scenes:场景资源文件夹
  • Scripts:脚本资源文件夹
  • Sprites:图片资源文件夹

2-2、导入资源

接下来将需要的资源导入:

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

全部右键另存为图片,然后导入到Project视图的Sprites文件夹内:

image.png

选中所有图片,在Inspector视图中,设置Pixels Per Unit为16:

image.png

之所以设置为16,是因为16X16这个单位在游戏世界中是一个比较适合的值。

2-3、设置摄像机属性

在Hierarchy视图中,选中Main Cameras对象,然后在Inspector视图中找到Camera组件,设置属性:

image.png

注意:Clear Flags设置为Skybox,Background按照图中设置,然后Size设置为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));

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

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

运行程序,就可以看到点击一个雷之后,就可以看到其他雷也会被发现,发现一个元素后,可以看到相邻的数字:

image.png

2-7、泛洪算法

好的,每当用户发现一个没有相邻地雷的元素时,就应该自动发现没有相邻地雷的整个区域,如下所示:

aHR0cHM6Ly9ub29idHV0cy5jb20vY29udGVudC91bml0eS8yZC1taW5lc3dlZXBlci1nYW1lL3VuY292ZXJfbWluZWxlc3NfZWxlbWVudHMuZ2lm.gif

有很多算法都可以做到这一点,但是目前为止最简单的算法还是泛洪算法,如果理解递归,泛洪算法也是很好理解的,下面就是泛洪算法所做的工作:

  • 从某种元素开始
  • 用这个元素做我们想做的事
  • 对每个相邻元素递归地继续

然后将泛洪算法加入到Grid类中:

    // 泛洪算法填充空元素
    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);
        }
    }

注意:泛洪算法递归地访问某个元素的周围的元素,直到它访问到每个元素为止。

接着修改我们的泛洪算法,这个算法应该发现它访问的元素是否是一个地雷,如果是的话就不应该继续下去:

    // 泛洪算法填充空元素
    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);
        }
    }

回到Element.cs脚本,修改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));

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

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

运行程序,在发现一个空元素的时候,会遍历就寻找周围是否存在没有地雷的空元素:

aH.gif

2-8、判断是否已经找到所有地雷

接下来,需要判断玩家是否已经找到所有的雷,那么游戏就应该结束了。

接着修改Grid类的代码,添加函数isFinished:

    // 是否找到所有地雷

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

修改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");
        }
    }

运行程序,就可以愉快的玩游戏了。

三、总结

《扫雷》游戏的大体框架就开发完成了,当然,你也可以添加一些元素让游戏更加有趣:

  • 用标记标记地雷
  • 分成更多难度,比如简单、中等、困难
  • 切换更加漂亮的UI
  • 输赢界面以及重新开始
  • 添加音效