目录
前言
本节我们来做迷宫生成。生成迷宫的算法其实有很多种,最简单的比如递归。而这一节我们选择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()
{
}
}
运行游戏,如图所示: