前言
数据结构准备
迷宫生成算法
迷宫寻路算法
前言
本次带来迷宫相关的算法,迷宫的算法涉及到不少经典的图论算法,在游戏中NPC这些算法被大量的运用,深入了解和学习这些算法是为开发游戏打下坚实的基础。除了纯算法以外,我还借用了OpenGL将这些算法的演算过程可视化出来,借用这些动画演算,可加深对算法的理解,枯燥的算法一下子有趣了起来呢!
本工程全部源码及可执行程序可在github下载:https://github.com/ZeusYang/Breakout。其中的Maze目录就是本次迷宫的项目文件了,可执行程序exe在Maze/x64/Release下,编译的64位程序,可直接运行。
程序操作说明:1、2、3数字键是生成迷宫指令,分别是深度优先、随机Prim、四叉树分割迷宫生成算法,A/a、B/b、C/c字符键是迷宫寻路指令,分别是深度优先、广度优先、A星算法迷宫寻路。按下之后再按Enter即自动开始对应的操作。注意先生成迷宫再进行寻路,否则没有意义,因为一开始都是封闭墙。
数据结构准备
迷宫本质上是一个二维平面,我们用一个二维数组表示,然后数组中的每个元素都是一个迷宫单元。定义迷宫单元[x,y],每个迷宫单元有上、下、左、右四面墙,初始时四面墙都存在。为了方面,我们定义下面的结构体:enum Neighbor { LEFT = 0, UP = 1, RIGHT = 2, DOWN = 3 };
struct Cell {//迷宫单元
int neighbors[4];//四个方向的邻居
int visited;//记录是否访问过了
//以下用于寻路算法
glm::ivec2 prev;//记录前驱
//用于A星算法的open表、closed表
bool inOpen, inClosed;
//启发式函数fn = gn + hn
//其中gn为起点到n的实际距离,hn为n到终点的哈密顿
int gn, hn;
Cell() :visited(0),inOpen(false),inClosed(false) {
neighbors[LEFT] = neighbors[UP] = neighbors[RIGHT] = neighbors[DOWN] = 0;
}
};
然后声明一个类–MazeAlgorithm,在这里我们将要实现六个算法,每个算法的数据结构如下:const int row, col;//迷宫单元的行、列数
static std::vector<:vector>> cells;//迷宫单元矩阵
//迷宫生成算法一数据结构:深度优先的栈
std::stack<:ivec2> record;
//迷宫生成算法二数据结构:随机Prim算法的链表
std::list<:ivec2> prim;
//迷宫生成算法三数据结构:四叉树广度优先的队列
std::queue<:pair glm::ivec2>> recursive;
//迷宫寻路算法一数据结构:深度优先的栈
std::stack<:ivec2> path_dfs;
//迷宫寻路算法一数据结构:广度优先的队列
std::queue<:ivec2> path_bfs;
//迷宫寻路算法一数据结构:A星算法的优先队列
std::priority_queue,Compare> path_astar;
以上仅仅是一部分,具体的细节请看源码。
迷宫生成算法
这里我实现的迷宫生成算法有三个,分别是:深度优先、随机Prim、四叉树分割。
深度优先
就是表面上的意思,深度优先的方法生成迷宫,当然跟普通的深度优先搜索有点差别,它加入了随机性,先看伪代码:将起点作为当前迷宫单元并标记为已访问
while 还存在未标记的迷宫单元
if 当前迷宫单元有未被访问过的的相邻的迷宫单元 then
随机选择一个未访问的相邻迷宫单元
将当前迷宫单元入栈
移除当前迷宫单元与相邻迷宫单元的墙
标记相邻迷宫单元并用它作为当前迷宫单元
else if 栈不空
栈顶的迷宫单元出栈
令其成为当前迷宫单元
算法的主要思想就是,每次在当前迷宫单元中寻找与其相邻的未访问过的迷宫单元,然后选择这些邻居其中的一个访问下去,直到所有的单元都被访问到。就是从起点开始随机走,走不通了就返回上一步,从下一个能走的地方再开始随机走。
那么如何实现呢,我们用一个栈来进行深度优先遍历,栈的元素是数组的下标。下面代码中的frame纯属用于演示动画,可去掉直接得结果,还有栈的初始化请在源代码中Generation_Init()函数查看。std::stack<:ivec2> record;
...
bool MazeAlgorithm::Generator_Dfs() {
frame = 5;//用于演示动画
while (!record.empty() && frame--) {//当队列或者frame不减到0时
cells[cur.x][cur.y].visited = 1;//标记当前的位置为访问过的了
bool hasNeigh = false;//是否有邻居未访问
std::vector<:pair>> tmp; //记录未访问的邻居, tmp.second代表它是哪个邻居
glm::ivec2 loc;
//寻找是否存在未访问的邻居
for (auto x = 0; x < 4; ++x) {
loc = glm::ivec2(cur.x + to[x][0], cur.y + to[x][1]);
if (CouldMove(loc) &