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

简介: 今天又是摸鱼...哦不..工作的一天,今天整一个很经典的Win系统自带游戏——《扫雷》,话说安装Win10之后就找不到《扫雷》游戏了,很难受。《扫雷》游戏的玩法是,在不触碰到任何地雷的情况下去发现一个雷区。揭开一个没有地雷的块之后,将会显示一个数字来表示周围地雷的数量。跟往常一样,会尽量的将步骤详细说明,代码也进行注释,可以让大家都可以理解。

一、前言

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

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

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

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


二、正文


2-1、新建项目

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

网络异常,图片无法展示
|

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

网络异常,图片无法展示
|

(3)目录如下图所示:

网络异常,图片无法展示
|

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


2-2、导入资源

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

网络异常,图片无法展示
|
网络异常,图片无法展示
|
网络异常,图片无法展示
|
网络异常,图片无法展示
|
网络异常,图片无法展示
|
网络异常,图片无法展示
|
网络异常,图片无法展示
|
网络异常,图片无法展示
|
网络异常,图片无法展示
|
网络异常,图片无法展示
|
网络异常,图片无法展示
|

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

网络异常,图片无法展示
|

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

网络异常,图片无法展示
|

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


2-3、设置摄像机属性

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

网络异常,图片无法展示
|

注意:Clear Flags设置为Skybox,Background按照图中设置,然后Size设置为20。


2-4、制作默认方块

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

网络异常,图片无法展示
|

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

网络异常,图片无法展示
|

注意:勾选Is Trigger

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

网络异常,图片无法展示
|

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

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

网络异常,图片无法展示
|

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

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卡槽中:

网络异常,图片无法展示
|

运行脚本:

网络异常,图片无法展示
|

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


2-5、相邻的概念

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

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

网络异常,图片无法展示
|

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


2-6、制作数字和地雷

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

网络异常,图片无法展示
|

(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脚本的属性卡槽中:

网络异常,图片无法展示
|

(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));
            // 揭露没有地雷的地区  
            // ...
            // 判断游戏是否胜利
            // ...
        }
    }
复制代码

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

网络异常,图片无法展示
|


2-7、泛洪算法

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

网络异常,图片无法展示
|

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

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

然后将泛洪算法加入到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]);
            // 判断游戏是否胜利
            // ...
        }
    }
复制代码

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

网络异常,图片无法展示
|


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
  • 输赢界面以及重新开始
  • 添加音效



相关文章
|
25天前
|
算法 测试技术 开发者
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗;代码审查通过检查源代码发现潜在问题,提高代码质量和团队协作效率。本文介绍了一些实用的技巧和工具,帮助开发者提升开发效率。
28 3
|
29天前
|
存储 缓存 算法
前端算法:优化与实战技巧的深度探索
【10月更文挑战第21天】前端算法:优化与实战技巧的深度探索
21 1
|
2月前
|
大数据 UED 开发者
实战演练:利用Python的Trie树优化搜索算法,性能飙升不是梦!
在数据密集型应用中,高效搜索算法至关重要。Trie树(前缀树/字典树)通过优化字符串处理和搜索效率成为理想选择。本文通过Python实战演示Trie树构建与应用,显著提升搜索性能。Trie树利用公共前缀减少查询时间,支持快速插入、删除和搜索。以下为简单示例代码,展示如何构建及使用Trie树进行搜索与前缀匹配,适用于自动补全、拼写检查等场景,助力提升应用性能与用户体验。
58 2
|
2月前
|
算法 搜索推荐 开发者
别再让复杂度拖你后腿!Python 算法设计与分析实战,教你如何精准评估与优化!
在 Python 编程中,算法的性能至关重要。本文将带您深入了解算法复杂度的概念,包括时间复杂度和空间复杂度。通过具体的例子,如冒泡排序算法 (`O(n^2)` 时间复杂度,`O(1)` 空间复杂度),我们将展示如何评估算法的性能。同时,我们还会介绍如何优化算法,例如使用 Python 的内置函数 `max` 来提高查找最大值的效率,或利用哈希表将查找时间从 `O(n)` 降至 `O(1)`。此外,还将介绍使用 `timeit` 模块等工具来评估算法性能的方法。通过不断实践,您将能更高效地优化 Python 程序。
61 4
|
3月前
|
算法 安全 数据安全/隐私保护
Android经典实战之常见的移动端加密算法和用kotlin进行AES-256加密和解密
本文介绍了移动端开发中常用的数据加密算法,包括对称加密(如 AES 和 DES)、非对称加密(如 RSA)、散列算法(如 SHA-256 和 MD5)及消息认证码(如 HMAC)。重点讲解了如何使用 Kotlin 实现 AES-256 的加密和解密,并提供了详细的代码示例。通过生成密钥、加密和解密数据等步骤,展示了如何在 Kotlin 项目中实现数据的安全加密。
138 1
|
3月前
|
机器学习/深度学习 存储 算法
强化学习实战:基于 PyTorch 的环境搭建与算法实现
【8月更文第29天】强化学习是机器学习的一个重要分支,它让智能体通过与环境交互来学习策略,以最大化长期奖励。本文将介绍如何使用PyTorch实现两种经典的强化学习算法——Deep Q-Network (DQN) 和 Actor-Critic Algorithm with Asynchronous Advantage (A3C)。我们将从环境搭建开始,逐步实现算法的核心部分,并给出完整的代码示例。
267 1
|
3月前
|
算法 安全 数据安全/隐私保护
Android经典实战之常见的移动端加密算法和用kotlin进行AES-256加密和解密
本文介绍了移动端开发中常用的数据加密算法,包括对称加密(如 AES 和 DES)、非对称加密(如 RSA)、散列算法(如 SHA-256 和 MD5)及消息认证码(如 HMAC)。重点展示了如何使用 Kotlin 实现 AES-256 的加密和解密,提供了详细的代码示例。
77 2
|
3月前
|
机器学习/深度学习 算法 数据挖掘
【白话机器学习】算法理论+实战之决策树
【白话机器学习】算法理论+实战之决策树
|
3月前
|
消息中间件 存储 算法
这些年背过的面试题——实战算法篇
本文是技术人面试系列实战算法篇,面试中关于实战算法都需要了解哪些内容?一文带你详细了解,欢迎收藏!
|
3月前
|
算法 vr&ar C#
使用Unity进行虚拟现实开发:深入探索与实践
【8月更文挑战第24天】使用Unity进行虚拟现实开发是一个充满挑战和机遇的过程。通过掌握Unity的VR开发技术,你可以创造出令人惊叹的VR体验,为用户带来前所未有的沉浸感和乐趣。随着技术的不断进步和应用场景的不断拓展,VR开发的未来充满了无限可能。希望本文能为你提供有用的指导和启发!
下一篇
无影云桌面