"죄송합니다. 낚시를 선택했습니다." - "Minesweeper" 소규모 게임 개발 실제 전투, 알고리즘, 소스 코드, Unity3D 개발 기반

"나는 너겟 커뮤니티 게임 창의 콘테스트의 개인 대회에 참가하고 있습니다. 자세한 내용은 참조: 게임 창의 콘테스트 "

추천 도서

안녕하세요 여러분 저는 불교엔지니어 ☆조용한 꼬마마룡☆ 유니티 개발 스킬을 수시로 업데이트 하고 있습니다.유용하다고 생각되시면 세번 눌러주세요.

I. 서론

오늘은 또 낚시다.. 오 안돼.. 출근하는 하루 오늘은 아주 고전적인 Win 시스템 내장 게임인 "지뢰 찾기"가 있습니다. Win10을 설치한 후 "지뢰 찾기" 게임을 찾을 수 없습니다. 매우 불편합니다.

지뢰 찾기의 게임 플레이는 지뢰를 건드리지 않고 지뢰밭을 찾는 것입니다.

지뢰가 없는 블록을 발견하면 주변 지뢰의 수를 나타내는 숫자가 표시됩니다.

평소와 같이 단계를 자세히 설명하고 모든 사람이 이해할 수 있도록 코드에 주석을 달도록 노력하겠습니다.

2. 텍스트

2-1. 새로운 프로젝트

(1) 프로젝트 개발, 새 프로젝트부터 시작하여 내가 사용하는 Unity 버전은 Unity 2019.4.7f1템플릿이 선택 2D되고 프로젝트 이름은 선택 사항이며 중국어는 사용하지 않습니다.

image.png

(2) 디렉터리를 만들고 프로젝트 보기에서 마우스 오른쪽 버튼을 클릭하여 만들기→폴더를 선택하고 여러 개의 새 폴더를 만듭니다.

image.png

(3) 디렉토리는 아래와 같습니다.

image.png

  • 프리팹: 프리팹 리소스 폴더
  • 장면: 장면 리소스 폴더
  • 스크립트: 스크립트 리소스 폴더
  • 스프라이트: 이미지 리소스 폴더

2-2. 자원 가져오기

다음으로 필요한 리소스를 가져옵니다.

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

모두 마우스 오른쪽 버튼을 클릭하고 그림으로 저장한 다음 프로젝트 보기의 스프라이트 폴더로 가져옵니다.

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");
        }
    }
复制代码

프로그램을 실행하면 즐겁게 게임을 즐길 수 있습니다.

3. 요약

"Minesweeper" 게임의 일반적인 프레임워크가 개발되었습니다. 물론 게임을 더 재미있게 만들기 위해 몇 가지 요소를 추가할 수도 있습니다.

  • 마커로 광산 표시
  • 쉬움, 보통, 어려움 등 더 많은 어려움으로 나뉩니다.
  • 더 아름다운 UI로 전환
  • 승리 및 패배 화면 및 다시 시작
  • 음향 효과 추가

추천

출처juejin.im/post/7083735364233330725