Unity游戏教程初步(六):迷宫算法与预制体

目录

前言

项目需求

Prim算法生成迷宫

1 算法思路介绍

2 代码示例

3 用于测试的代码

预制体(预制件)

1 定义

2 构建迷宫墙体预制体

完善工程


前言

本节我们来做迷宫生成。生成迷宫的算法其实有很多种,最简单的比如递归。而这一节我们选择prim算法生成迷宫,因为这种算法生成的迷宫较为自然,并且算法原理简易。

项目需求

使用prim算法和预制体生成迷宫。

Prim算法生成迷宫

1 算法思路介绍

本部分中的所有图示采用RPG Maker XP建立。顺便吐槽一句,现在网上流传的有关prim算法生成迷宫的算法描述(那个据说来自维基的)简直是语文不及格产物。

Prim算法与图论有关(我在article105中介绍过基本的图搜索算法和一个使用Ruby书写的搜索模版),它最开始被用于在加权连通图里搜索一个最小生成树。个人认为它还有些像用堆实现的二叉树遍历算法。其具体资料请读者自行学习,笔者在此只给出一个用于迷宫生成场景下较为通俗的描述。

我们假设在区域内有一个N*M列的迷宫。对于迷宫的每个格子,它们只有两个状态:通路/墙。在初始状态下,这个迷宫里的所有格子都是墙。设定一个的类Wall,其数据为[Vector2Int pos, Vector2Int relative],也就是其位置向量pos与其相对格子的位置向量relative(在后续步骤中会解释)。同时,我们建立一个墙的空列表M。

然后我们选定一个起点A,将此位置的墙移除:

将与起点A相邻的所有墙加入M中,其中A作为这些墙的父格子。对于一面墙而言,我们称在其父格子另一边的格子为相对格子。它们的例子如图:

在M中随机选择一个元素x。对于x,如果其相对格子为墙,则移除相对格子和x的墙,然后将相对格子作为父格子,将相邻墙加入列表。否则则从M中移除x。如此循环直到M中为空。

2 代码示例

我们会创建一个墙的类Wall与生成迷宫的类Maze。

Wall.cs

public class Wall //内部类Wall

{

    public Vector2Int pos; //位置向量

    public Vector2Int relative; //相对格子向量

    public Wall(Vector2Int pos,Vector2Int anc){

        this.pos=pos;

        ancToRelative(anc);

    }

    public void ancToRelative(Vector2Int ancestor){ //输入一个父格子,求出其相对格子并赋值

        this.relative=pos*2-ancestor; //int不能左乘Vector2Int(因为这里的*是Vector2Int的运算符重载)

    }

}

Maze.cs

public class Maze //内部类Maze

{

    private Vector2Int[] e={new Vector2Int(1,0),new Vector2Int(-1,0),new Vector2Int(0,1),new Vector2Int(0,-1),}; //四方向

    private int width;

    private int height; //迷宫矩阵的宽和高 默认[宽,高],Maze位置的游标在<[0..width],[0..height]>

    private Vector2Int origin; //起点

    public List<Wall> M; //list

    public List<Wall> W; //list

    public List<Vector2Int> maze; //最终的迷宫列表

    public Maze(int width,int height,Vector2Int origin){

        this.width=width;

        this.height=height;

        this.origin=origin;

        this.M=new List<Wall>();

        this.W=new List<Wall>();

        this.maze=new List<Vector2Int>();

        this.maze.Add(origin);

    }

    bool findMaze(Vector2Int pos){ //只判断此位置是否已移除墙

        if(this.maze.Contains(pos)){

            return true;

        }

        return false;

    }

    bool borderExam(Vector2Int pos){ //边界检查

        if(pos.x>=0 &&pos.x<width && pos.y>=0 &&pos.y<height){

            return true;

        }

        return false;

    }

    void createWall(Vector2Int anc){ //传入移除墙的格子,创建相邻墙

        foreach(Vector2Int i in this.e){

            Vector2Int index=anc+i; //上下左右格子的位置

            if(borderExam(index) && !findMaze(index)){ //检查确定这个位置确实是墙,并且没有越界

                this.M.Add(new Wall(index,anc)); //将墙加入M中

            }

        }

    }

    void funcM(){ //随机取出M中的墙,执行步骤

        int index=Random.Range(0,this.M.Count); 

        Wall wall=this.M[index];

        if(findMaze(wall.pos)){ //之前已经移除了此墙

            this.M.RemoveAt(index);

            return;

        }

        if(findMaze(wall.relative) || !borderExam(wall.relative)){ //相对位置已移除墙,或者此墙的相对位置无效

           this.M.RemoveAt(index); //此墙移除

        }

        else{ 

            this.M.RemoveAt(index);

            this.maze.Add(wall.pos);

            this.maze.Add(wall.relative); //后面填什么无所谓,给相对位置挖墙

            createWall(wall.relative); //加入新的墙

        }

    }

    public void exec(){ //入口?

        createWall(this.origin); //起点

        while(M.Count!=0){

            funcM();

        }

    }

}

在生成迷宫时,声明一个Maze类的实例并且调用其exec方法,就可得到一个迷宫。

3 用于测试的代码

此代码在Start方法内调用,可以在控制台生成一份迷宫示意图。

 string a="";

        Maze maze=new Maze(30,20,new Vector2Int(0,0));

        maze.exec();

        for(int i=0;i<30;i++){

            for(int j=0;j<20;j++){

                if(maze.maze.Contains(new Vector2Int(i,j))){

                    a=a+"-";

                }

                else{

                    a=a+"#";

                }

            }

            a=a+"\n";

        }

        print(a);

预制体(预制件)

-本节相关内容请读者参考:

-预制件 - Unity 手册,《预制件》

-Object-Instantiate - Unity 脚本 API,《Object.Instantiate

1 定义

预制体(英文为prefab,在unity手册中被称为预制件)可以看做是一个文件化的游戏对象,一个已经构建好了的样板。如果从编程的角度进行类比,预制体可以被看成一个用于派生类的基类,或者是一个单纯的被复制对象。预制体以实体文件(.prefab)的形式存在于unity中,并可以在有需要时动态加载进游戏场景。

预制体通常被应用于如下两个场景中:一,被经常使用的游戏对象(比如森林场景中的一棵树);二,在场景被加载时不应当存在于场景中的游戏对象(比如说闯关游戏中的通关画面)。

2 构建迷宫墙体预制体

首先确认预制体的形状和大小。由于迷宫的场地是正方形的,我们采用以正方形为底的四棱柱作为预制体的形状,则底边长设定为scale1单位,高度为scale2单位。

创建一个Cube,调整其大小和位置直到满足上文中的条件。然后,重命名Cube为prefab,选中prefab并将其拖动到Project选项卡下的游戏资源文件夹内部,可以看到出现了一个同名文件,即为预制体。

将预制体文件拖动到Hierarchy选项卡下,可以看到预制体又出现在场景中了。需要注意的是,如果要编辑预制体文件本身,请点击预制体右边的“>”,在场景中直接编辑的不是预制体,而只是其的一个副本。预制体在场景中以蓝色显示。

完善工程

将目的区域的大小调整到与球体相同的水平,也就是scale0.1大小(顺便要调整变色和音效生效的区域)。

创建一个空游戏对象MazeController,搭载用于生成迷宫的脚本MazeController.cs。

敲代码。

MazeController.cs

using System.Collections;

using System.Collections.Generic;

using System.Text;

using System.Threading.Tasks;

using UnityEngine;

using Random=UnityEngine.Random;

public class Wall //内部类Wall

{

    public Vector2Int pos; //位置向量

    public Vector2Int relative; //相对格子向量

    public Wall(Vector2Int pos,Vector2Int anc){

        this.pos=pos;

        ancToRelative(anc);

    }

    public void ancToRelative(Vector2Int ancestor){ //输入一个父格子,求出其相对格子并赋值

        this.relative=pos*2-ancestor; //int不能左乘Vector2Int(因为这里的*是Vector2Int的运算符重载)

    }

}

public class Maze //内部类Maze

{

    private Vector2Int[] e={new Vector2Int(1,0),new Vector2Int(-1,0),new Vector2Int(0,1),new Vector2Int(0,-1),}; //四方向

    private int width;

    private int height; //迷宫矩阵的宽和高 默认[宽,高],Maze位置的游标在<[0..width],[0..height]>

    private Vector2Int origin; //起点

    public List<Wall> M; //list

    public List<Wall> W; //list

    public List<Vector2Int> maze; //最终的迷宫列表

    public Maze(int width,int height,Vector2Int origin){

        this.width=width;

        this.height=height;

        this.origin=origin;

        this.M=new List<Wall>();

        this.W=new List<Wall>();

        this.maze=new List<Vector2Int>();

        this.maze.Add(origin);

    }

    bool findMaze(Vector2Int pos){ //只判断此位置是否已移除墙

        if(this.maze.Contains(pos)){

            return true;

        }

        return false;

    }

    bool borderExam(Vector2Int pos){ //边界检查

        if(pos.x>=0 &&pos.x<width && pos.y>=0 &&pos.y<height){

            return true;

        }

        return false;

    }

    void createWall(Vector2Int anc){ //传入移除墙的格子,创建相邻墙

        foreach(Vector2Int i in this.e){

            Vector2Int index=anc+i; //上下左右格子的位置

            if(borderExam(index) && !findMaze(index)){ //检查确定这个位置确实是墙,并且没有越界

                this.M.Add(new Wall(index,anc)); //将墙加入M中

            }

        }

    }

    void funcM(){ //随机取出M中的墙,执行步骤

        int index=Random.Range(0,this.M.Count); 

        Wall wall=this.M[index];

        if(findMaze(wall.pos)){ //之前已经移除了此墙

            this.M.RemoveAt(index);

            return;

        }

        if(findMaze(wall.relative) || !borderExam(wall.relative)){ //相对位置已移除墙,或者此墙的相对位置无效

           this.M.RemoveAt(index); //此墙移除

        }

        else{ 

            this.M.RemoveAt(index);

            this.maze.Add(wall.pos);

            this.maze.Add(wall.relative); //后面填什么无所谓,给相对位置挖墙

            createWall(wall.relative); //加入新的墙

        }

    }

    public void exec(){ //入口?

        createWall(this.origin); //起点

        while(M.Count!=0){

            funcM();

        }

    }

}

//上略

public class MazeController : MonoBehaviour

{

    public GameObject wall; //墙体预制体

    public Transform destArea; //终点区域的位置

    const int Width=10;

    const int Height=10; 

    // Start is called before the first frame update

    void Start()

    {

        Vector3 correct1=new Vector3(0.5f,0,0.5f); //墙体中心修正

        Vector3 correct2=new Vector3(-5,0,-5); //平面偏移修正

        Vector2Int dest=new Vector2Int((int)(destArea.position.x+4.5f),(int)(destArea.position.y+4.5f)); //目的区域位置修正

        Maze maze=new Maze(Width,Height,dest); //按照平台的大小生成迷宫

        maze.exec();

        //可惜C#里没有Range

        for(int x=0;x<Width;x++){

            for(int y=0;y<Height;y++){

                if(!maze.maze.Contains(new Vector2Int(x,y))){ //本区域不为空

                    Instantiate(wall,new Vector3(x,2,y)+correct1+correct2,Quaternion.identity); //Quaternion.identity是0角度

                }

            }

        }

    }

    // Update is called once per frame

    void Update()

    {

    }

}

运行游戏,如图所示:

  • 2
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值