人们总是对未知充满好奇,迫使他们满怀热情去求知,去解答。对于游戏也应如此,想让玩家对游戏满怀热情,玩游戏时不会感觉太无聊,我认为最重要的就是能够让玩家时刻对游戏接下来发生的事充满期待,激发玩家的好奇心。这对游戏自身的玩法性充满挑战。
优秀的画质,耐人寻味的剧情,题材新颖,有趣的游戏玩法等等都是一款优秀的游戏所不可或缺的。
迷宫,因为其复杂性和不可预知性让一大批人深深为之着迷。对于迷宫的解释,百度百科上是这样说的:
迷宫 指的是充满复杂通道,很难找到从其内部到达入口或从入口到达中心的道路,道路复杂难辨,人进去不容易出来的建筑物。通常比喻复杂艰深的问题或难以捉摸的局面。
所以说,在游戏中,迷宫能够提高游戏的玩法性和趣味性,玩家在迷宫中运用自己的智慧找到出口,成就感瞬间爆棚,也就更想要继续玩下去。
迷宫玩法,通常在解谜冒险类游戏中出现,其他类型的游戏也有一定的涉及,如果再在迷宫中加入一些随机事件,相信游戏的随机性和趣味性也会大大提高。
正是因为这么多的原因,作为游戏开发者,更应该学习开发迷宫玩法,其中迷宫生成算法必不可少。
经典的迷宫生成算法有四种:递归回溯算法,递归分割算法,随机Prim算法,Kruskal+并查集。关于详细介绍,许多大佬的博客解释的很清晰,这里不再赘述,作者选了一种生成的迷宫比较自然的随机Prim算法。
算法见解
下面是个人对算法的一些小见解
前提:算法考虑的均是方形迷宫
迷宫生成的基本流程是
1.先生成一个由有限个周围四面均是墙的封闭区域组成的方形大区域,例如5×5的迷宫就是由25个这种封闭区域组成的。
2.开始消除迷宫中间的部分墙壁(边框除外),只需保证迷宫内任意相邻两区域是互通的,这样就可以推出迷宫内任意两区域都是互通的。有了这个保障,迷宫的出口和入口就可以在四个边框上随意指定位置,不会出现无解的迷宫,也增加了生成迷宫的随机性。同时,让迷宫中的每一块区域充分利用,不会出现某一块区域永远无法到达的情况。
了解流程之后,首先就要考虑迷宫的存储方式,一种方式是用二维数组存储,例如用数字0代表墙壁,数字1代表道路就像这样:
int[,] maze = {
{ 0,1,0,0,0}
,{ 0,1,1,0,0}
,{ 0,0,1,1,0}
,{ 0,0,0,1,1}
,{ 0,0,0,0,0}
};
还有一种方式是用两个二维数组,适合墙壁没有厚度或者厚度很小的迷宫,一个存储所有行的墙壁(类似横着的线)信息,一个存储所有列的墙壁(类似竖着的线)信息,数字0代表没有墙壁,数字1代表墙壁存在,也可以用bool变量表示。
作者在这里选用第二种,因为和算法结合性较好。
随机Prim算法描述
1.在初始生成的全区域封闭迷宫中随机选择一个区域作为当前区域
2.将区域四周未消除的墙加入列表中
3.循环执行以下方法,直至列表为空
随机从列表选择一面墙
如果墙两边区域存在一区域未被连通,就消除这面墙,并将这面墙两边区域附近未消除的墙加入列表(迷宫边框的墙壁除外)
从列表中移除这面墙
4.随机选取迷宫边框上的两个墙壁分别作为出口和入口(出口和入口可能会非常接近)
了解了原理,接下来就是在Unity实现,先看效果图
代码实现
首先定义一个迷宫类
public class MazeWall
{
//true表示墙壁存在,false表示墙壁不存在
public bool[,] rowWall;//存储迷宫所有行的墙壁信息
public bool[,] colWall;//存储迷宫所有列的墙壁信息
public int rowsum;//迷宫有多少行
public int colsum;//迷宫有多少列
/// <summary>
/// 判断区域是否连通,四面墙有一面墙打通即为连通
/// </summary>
/// <param name="rowindex"></param>
/// <param name="colindex"></param>
/// <returns></returns>
public bool this[int rowindex, int colindex]
{
get
{
//检查是否越界
if (rowindex >= rowsum || colindex >= colsum)
Debug.LogError("越界");
if (rowindex < 0 || colindex < 0)
Debug.LogError("越界");
//有一面墙不存在即为连通
return !(rowWall[rowindex, colindex] && rowWall[rowindex+1, colindex ] &&
colWall[rowindex, colindex] && colWall[rowindex, colindex + 1]);
}
}
//构造函数,初始化迷宫信息,
public MazeWall(int a,int b)
{
rowsum = a;
colsum = b;
rowWall = new bool[rowsum+1, colsum];
colWall = new bool[rowsum, colsum + 1];
for (int i = 0; i < rowsum + 1; i++)
{
for (int j = 0; j < colsum; j++)
{
rowWall[i, j] = true;
}
}
for (int i = 0; i < rowsum; i++)
{
for (int j = 0; j < colsum + 1; j++)
{
colWall[i, j] = true;
}
}
}
/// <summary>
/// 消除两相邻区域之间墙壁
/// </summary>
/// <param name="area1"></param>
/// <param name="area2"></param>
public void OpenArea(MeshArea area1, MeshArea area2)
{
if (area1.rownum == area2.rownum)
{
colWall[area1.rownum, Mathf.Max(area1.colnum, area2.colnum)] = false;
return;
}
if (area1.colnum == area2.colnum)
{
rowWall[Mathf.Max(area1.rownum, area2.rownum),area1.colnum ] = false;
}
}
/// <summary>
/// 随机生成出口入口,返回为(int,int)元组,为了之后判断,防止出口和入口重叠
/// </summary>
/// <returns></returns>
public (int,int) StartRom()
{
int i=Random.Range(1, 5);//在四条边随机选择
int j;//在某条边上随机选择某一墙壁
switch (i)
{
case 1:
j = Random.Range(0, colsum);
rowWall[0, j] = false;
return(0,j);
case 2:
j = Random.Range(0, rowsum);
colWall[j, colsum] = false;
return (j, colsum);
case 3:
j = Random.Range(0, colsum);
rowWall[rowsum, j] = false;
return (rowsum, j);
default:
j = Random.Range(0, rowsum);
colWall[j, 0] = false;
return (j, 0);
}
}
}
然后定义一个网格区域的类
/// <summary>
/// 网格区域类
/// </summary>
public class MeshArea
{
public int rownum;//格子的行数
public int colnum;//格子的列数
public MeshArea(int row,int col)
{
rownum = row;
colnum = col;
}
}
然后创建一个基础的生成迷宫算法类,封装一些基本的迷宫操作与判断,用于让具体的算法实现类继承,提高程序的扩展性。
public class CreateMazeAlgoritnm
{
protected static MazeWall mazeWall;
/// <summary>
/// 随机选择一个开始区域
/// </summary>
protected static MeshArea RandChooseBeginArea()
{
int RandomRowIndex = Random.Range(0, mazeWall.rowsum);
int RandomColIndex = Random.Range(0, mazeWall.colsum);
return new MeshArea(RandomRowIndex, RandomColIndex);
}
/// <summary>
/// 获得未访问的邻接相通区域
/// </summary>
protected static List<MeshArea> GetNearbyArea(MeshArea area)
{
List<MeshArea> nerabyAreas = new List<MeshArea>();
if (area.rownum > 0)
if (!mazeWall[area.rownum - 1, area.colnum])
nerabyAreas.Add(new MeshArea(area.rownum - 1, area.colnum));
if (area.rownum < mazeWall.rowsum - 1)
if (!mazeWall[area.rownum + 1, area.colnum])
nerabyAreas.Add(new MeshArea(area.rownum + 1, area.colnum));
if (area.colnum > 0)
if (!mazeWall[area.rownum, area.colnum - 1])
nerabyAreas.Add(new MeshArea(area.rownum, area.colnum - 1));
if (area.colnum < mazeWall.colsum - 1)
if (!mazeWall[area.rownum, area.colnum + 1])
nerabyAreas.Add(new MeshArea(area.rownum, area.colnum + 1));
return nerabyAreas;
}
/// <summary>
/// 判断区域是否被打通
/// </summary>
/// <param name="area"></param>
/// <returns></returns>
protected static bool checkArea(MeshArea area)
{
return mazeWall[area.rownum, area.colnum];
}
/// <summary>
/// 检测墙是否需要打通
/// </summary>
/// <param name="wall"></param>
/// <returns></returns>
protected static bool checkWall(KeyValuePair<MeshArea, MeshArea> wall)
{
bool Conduction1 = checkArea(wall.Key);
bool Conduction2 = checkArea(wall.Value);
return !Conduction1 || !Conduction2;
}
}
随机Prim算法实现类,继承了上面的类
/// <summary>
/// 算法实现
/// </summary>
public class Prim : CreateMazeAlgoritnm
{
//维护的墙列表
private static List<KeyValuePair<MeshArea, MeshArea>> walls = new List<KeyValuePair<MeshArea, MeshArea>>();
public static MazeWall Generate(MazeWall wall)
{
mazeWall = wall;
walls.Clear();
//封闭全部墙壁
//随机选择一个开始区域
MeshArea fistArea = RandChooseBeginArea();
AddNerabyWall(fistArea);
while (walls.Count > 0)
{
int randomIndex = Random.Range(0, walls.Count);
var _wall = walls[randomIndex];
if (checkWall(_wall))
{
mazeWall.OpenArea(_wall.Key, _wall.Value);
if (checkArea(_wall.Key))
{
AddNerabyWall(_wall.Key);
}
if (checkArea(_wall.Value))
{
AddNerabyWall(_wall.Value);
}
}
walls.Remove(_wall);
}
//随机选择迷宫起点终点
(int,int)a=mazeWall.StartRom();
//避免出口和入口重叠
while (mazeWall.StartRom() == a) { }
return mazeWall;
}
/// <summary>
/// 把区域附近未打通的墙加入维护的墙列表
/// </summary>
/// <param name="area"></param>
private static void AddNerabyWall(MeshArea area)
{
List<MeshArea> areas = GetNearbyArea(area);
for (int i = 0; i < areas.Count; ++i)
walls.Add
(
new KeyValuePair<MeshArea, MeshArea>
(
area, areas[i]
)
);
}
}
最后就是在unity编辑器中创建一个空对象,绑定下面的脚本,用于在场景中显示迷宫
using UnityEngine;
public class GameManager : MonoBehaviour
{
public int row;//在编辑器中指定迷宫行数
public int col;//在编辑器中指定迷宫列数
public GameObject Wall;//在编辑器中指定墙体Prefeb
MazeWall mazeWall;
void Start()
{
CreateMaze();
}
private void CreateMaze()
{
mazeWall = new MazeWall(row,col);
Prim.Generate(mazeWall);
//生成行的所有墙壁
for (int i = 0; i < mazeWall.rowWall.GetLength(0) ; i++)
{
for (int j = 0; j < mazeWall.rowWall.GetLength(1); j++)
{
if (mazeWall.rowWall[i, j])
{
Instantiate(Wall, new Vector3(j + 0.5f, 0.5f, i), Quaternion.Euler(0, 90, 0));
}
}
}
//生成列的所有墙壁
for (int i = 0; i < mazeWall.colWall.GetLength(0); i++)
{
for (int j = 0; j < mazeWall.colWall.GetLength(1) ; j++)
{
if (mazeWall.colWall[i, j])
{
Instantiate(Wall, new Vector3(j, 0.5f, i + 0.5f), Quaternion.Euler(0, 0, 0));
}
}
}
}
}
以上就是代码的实现,由于作者实力有限,程序的规范性和扩展性做的不好,命名不规范,程序的封装性也有欠缺,欢迎路过的大佬指正。
结语
作者写博客的目的为了和大家共同学习,共同进步,勤于思考,养成写博客的习惯,当然,作者更希望能有大佬帮助批评更正,不吝赐教