前言

数据结构准备

迷宫生成算法

迷宫寻路算法

前言

本次带来迷宫相关的算法,迷宫的算法涉及到不少经典的图论算法,在游戏中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) && !cells[loc.x][loc.y].visited) {//有未访问的邻居

tmp.push_back({ loc,x });//加入tmp中,然后随机选择一个

hasNeigh = true;

}

}

if (hasNeigh) {//从未访问的邻居中随机选择一个

int got = rand() % tmp.size();

record.push(cur);//当前迷宫单元入栈

//拆掉cur和tmp[got]之间的墙

cells[cur.x][cur.y].neighbors[tmp[got].second] = 1;

cells[tmp[got].first.x][tmp[got].first.y].neighbors[(tmp[got].second + 2) % 4] = 1;

//令当前标记变为该邻居

cur = tmp[got].first;

}

else {//没找到一个未被访问的邻居,是时候回溯了

cur = record.top();

record.pop();

}

}

//栈尾空代表已经结束了

if (record.empty())return true;

else return false;

}

然后用OpenGL做出的动画如下:

可以看到就是沿着一条路一直走下去,没路再回溯。这就是深度优先。

随机Prim

与深度优先不同,随机Prim算法是随机地在迷宫单元列表中随机选取一个迷宫单元,新加入列表和之前加入列表的迷宫单元有同等的概略被选中。对于选中的迷宫单元,标记为被访问状态,并把它从列表中删除,然后依旧查看它的四面邻居的情况,从所有被访问过的邻居中随机选一个,打通这个邻居和当前迷宫单元之前的墙,对所有未被访问过的邻居我们将其放入列表中。注意到我们有删除操作,但是又要求随机访问。这里我采用了链表,我想了下可以用另一种方法替代,但是对于规模不是非常巨大的来说是几乎没什么差别。废话不多说,伪代码如下:list = 迷宫单元的列表,这里是索引

set = 暂存一个迷宫单元的被访问过的邻居

将起点加入list中

while list不空

从list中随机选一个元素cur

将cur从list中删除,标记cur的迷宫单元为被访问状态

对于cur的四个邻居

该邻居被访问过,加入set中

否则加入list中

if set非空

从中随机选一个,打通cur和被选中的迷宫单元之间的墙

随机Prim的等概率性使得所有的迷宫单元优先级几乎等同,因此其分支更多,生成的迷宫更复杂,难度更大。bool MazeAlgorithm::Generator_Prim() {

frame = 5;//frame同上

//prim为list

while (!prim.empty() && frame--) {

//随机从list中选一个

int choice = rand() % prim.size();

auto it = prim.begin();

std::advance(it, (choice == 0) ? 0 : choice);

cur = *it;

//标记为已访问过,然后从List删除

cells[cur.x][cur.y].visited = 1;

prim.erase(it);

//查看邻居的情况

std::vector<:pair int>> tmp; //记录未访问的邻居

//四个邻居

for (auto x = 0; x < 4; ++x) {

glm::ivec2 loc = glm::ivec2(cur.x + to[x][0], cur.y + to[x][1]);

if (CouldMove(loc)) {//边界检查

//被访问过,加入tmp中,接下来要随机抽取这些

if (cells[loc.x][loc.y].visited == 1)tmp.push_back(std::pair<:ivec2 int>(loc, x));

else if (cells[loc.x][loc.y].visited == 0) {

//未被访问过,加入list中,并标记为2,代表他们在list中

prim.push_back(loc);

cells[loc.x][loc.y].visited = 2;

}

}

}

//有未被访问过的邻居

if (!tmp.empty()) {

//从中随机选一个,打通他们之间的墙

int got = rand() % tmp.size();

cells[cur.x][cur.y].neighbors[tmp[got].second] = 1;

cells[tmp[got].first.x][tmp[got].first.y].neighbors[(tmp[got].second + 2) % 4] = 1;

}

}

if (prim.empty())return true;

else return false;

}

可以看到这种方法有点广度优先的影子,这是因为迷宫单元之间的优先级等同。此算法生成迷宫难度最大。

四叉树分割

在有些地方那个也叫递归分割,但实际上可以不用递归,它的本质上就是一颗四叉树。每一次在当前的迷宫范围内用十字分割成四个子空间,在十字四个方向中随机三个墙上挖洞,随机对每个子空间进行同样的操作,知道子空间不可再分。可以看到原理非常简单,但生成迷宫的效率却是最高的,然后此法生成的迷宫教为简单,直路较多。我们直接对迷宫单元数组进行操作,采用广度优先遍历四叉树的方法,每次划分四个子空间。伪代码如下:queue = {(r1,r2,c1,c2)|r1为最小行,r2为最大行,c1和c2同理,换成列}

将迷宫矩阵范围(0,rows,0,cols)放入queue中

while queue不空

从queue取队头元素,出队

if r1 < r2 且 c1 < c2 then

在r1和r2之间选取随机数r

在c1和c2之间选取随机数c

用(r,c)对该范围进行分割

在(r,c)的四个方向上随机选三个,打通他们的墙

然后用(r,c)十字分割当前的范围,将四个子空间入队

else if r1 < r2

此时子空间变成了一条竖线,我们只在行方向上进行操作和分割

然后两个子空间加入队列

else if c1 < c2

此时子空间变成了一条横线,我们只在列方向上进行操作和分割

然后两个子空间加入队列

利用递归实现此算法非常简洁明了,但是我为了能够追踪演算过程采用了bfs方法实现,比较繁琐,如下,更多细节请查看源代码:bool MazeAlgorithm::Generator_Recursive() {

frame = 10;

//recursive是queue,其中的元素为pair<:ivec2>

//first为行范围,second为列范围

while (!recursive.empty() && frame--) {

std::pair<:ivec2 glm::ivec2> head = recursive.front();

recursive.pop();

//head.first == head.second情况下变成了一条线,需要特殊处理

if (head.first.x < head.first.y && head.second.x < head.second.y) {

glm::ivec2 center;

//在[head.first,head.first)之间选择一个坐标,根据这个坐标进行分割

center.x = head.first.x + rand() % (head.first.y - head.first.x);

center.y = head.second.x + rand() % (head.second.y - head.second.x);

int subRow[2], subCol[2];//存储四个方向上的随机数

//在center四个方向上随机选取

subRow[0] = head.first.x + rand() % (center.x - head.first.x + 1);

subRow[1] = center.x + 1 + rand() % (head.first.y - center.x);

subCol[0] = head.second.x + rand() % (center.y - head.second.x + 1);

subCol[1] = center.y + 1 + rand() % (head.second.y - center.y);

//获取四个方向上的随机迷宫单元

glm::ivec2 meta[4];

meta[LEFT] = glm::ivec2(center.x, subCol[0]);

meta[UP] = glm::ivec2(subRow[0], center.y);

meta[RIGHT] = glm::ivec2(center.x, subCol[1]);

meta[DOWN] = glm::ivec2(subRow[1], center.y);

int notOpen = rand() % 4;//随机选一个迷宫单元不打通,剩下的三个都打通

for (auto x = 0; x < 4; ++x) {

if (x != notOpen) {//在这三个迷宫单元挖洞

//左、右打通它的下面,上、下打通它的右面

glm::ivec2 near = (x % 2 == 0) ? glm::ivec2(meta[x].x + 1, meta[x].y)

: glm::ivec2(meta[x].x, meta[x].y + 1);

//哪面墙

int which = (x % 2 == 0) ? DOWN : RIGHT;

//打通meta[x]和near之间的墙

cells[meta[x].x][meta[x].y].neighbors[which] = 1;

cells[near.x][near.y].neighbors[(which + 2) % 4] = 1;

}

}

//然后再对当前的四个子空间进行同样处理,入队

recursive.push(std::pair<:ivec2 glm::ivec2>({ glm::ivec2(head.first.x,center.x),

glm::ivec2(head.second.x,center.y) }));

recursive.push(std::pair<:ivec2 glm::ivec2>({ glm::ivec2(head.first.x,center.x),

glm::ivec2(center.y + 1,head.second.y) }));

recursive.push(std::pair<:ivec2 glm::ivec2>({ glm::ivec2(center.x + 1,head.first.y),

glm::ivec2(head.second.x,center.y) }));

recursive.push(std::pair<:ivec2 glm::ivec2>({ glm::ivec2(center.x + 1,head.first.y),

glm::ivec2(center.y + 1,head.second.y) }));

}

else if (head.first.x < head.first.y) {//子空间变成了一条竖线,其他同上

int rm = head.first.x + rand() % (head.first.y - head.first.x);

cells[rm][head.second.x].neighbors[DOWN] = 1;

cells[rm + 1][head.second.x].neighbors[UP] = 1;

recursive.push(std::pair<:ivec2 glm::ivec2>({ glm::ivec2(head.first.x,rm),

glm::ivec2(head.second.x,head.second.x) }));

recursive.push(std::pair<:ivec2 glm::ivec2>({ glm::ivec2(rm + 1,head.first.y),

glm::ivec2(head.second.x,head.second.x) }));

}

else if (head.second.x < head.second.y) {//子空间变成了一条横线,其他同上

int cm = head.second.x + rand() % (head.second.y - head.second.x);

cells[head.first.x][cm].neighbors[RIGHT] = 1;

cells[head.first.x][cm+1].neighbors[LEFT] = 1;

recursive.push(std::pair<:ivec2 glm::ivec2>({ glm::ivec2(head.first.x,head.first.x),

glm::ivec2(head.second.x,cm) }));

recursive.push(std::pair<:ivec2 glm::ivec2>({ glm::ivec2(head.first.x,head.first.x),

glm::ivec2(cm + 1,head.second.y) }));

}

}

if (recursive.empty()) return true;

else return false;

}

此方法速度很快,可以看到生成的迷宫较为简单,直路多,适合fps等类的游戏。

迷宫寻路算法

这里我实现的迷宫寻路算法有三个,分别是:深度优先搜索、广度优先搜索、A星搜索算法。

深度优先搜索

基本的图算法遍历操作,没什么特别的,深度优先搜索出来的路径不一定是最短的,它遵循的原则是找到就好。bool MazeAlgorithm::Pathfinding_Dfs() {

//深度优先遍历寻路

frame = 10;

while (!path_dfs.empty() && frame--) {

glm::ivec2 head = path_dfs.top();

path_dfs.pop();

//寻找邻居

for (auto x = 0; x < 4; ++x) {

glm::ivec2 loc(head.x + to[x][0], head.y + to[x][1]);

if (CouldMove(loc) && !cells[loc.x][loc.y].visited //未访问

&& cells[head.x][head.y].neighbors[x] == 1) {//且无墙隔着

cells[loc.x][loc.y].prev = head;//记录前驱,然后要倒推路径

path_dfs.push(loc);

cells[loc.x][loc.y].visited = 1;

//找到终点了

if (loc == glm::ivec2(row - 1, col - 1)) {

//清空dfs栈,停止搜索

while (!path_dfs.empty())path_dfs.pop();

}

}

}

}

if (path_dfs.empty()) {//从终点倒退路径到起点

GetSolution();

return true;

}

else return false;

}

广度优先搜索

也是基本的图算法,它像病毒爆发一样向着终点蔓延,广度优先搜索得到的路径是最短的,它遵循的原则是一起找,谁先找到就谁是答案。bool MazeAlgorithm::Pathfinding_Bfs() {

//广度优先遍历寻路->最短路径

frame = 10;

while (!path_bfs.empty() && frame--) {

glm::ivec2 head = path_bfs.front();

path_bfs.pop();

//寻找邻居

for (auto x = 0; x < 4; ++x) {

glm::ivec2 loc(head.x + to[x][0], head.y + to[x][1]);

if (CouldMove(loc) && !cells[loc.x][loc.y].visited //未访问

&& cells[head.x][head.y].neighbors[x] == 1) {//且无墙隔着

cells[loc.x][loc.y].prev = head;//记录前驱,然后要倒推路径

path_bfs.push(loc);

cells[loc.x][loc.y].visited = 1;

//找到终点了

if (loc == glm::ivec2(row - 1, col - 1)) {

//清空dfs栈,停止搜索

while (!path_bfs.empty())path_bfs.pop();

}

}

}

}

if (path_bfs.empty()) {//从终点倒退路径到起点

GetSolution();

return true;

}

else return false;

}

广度优先遍历像病毒蔓延一样。

A星搜索算法

A星搜索算法是比较经典的寻路算法了,我在前面的博文中有一篇关于A星算法,这里不再赘述和。我采用的启发式函数是fn = gn + hn,其中gn为起点到n的实际距离,hn为n到终点的哈密顿距离,采用优先队列实现,更多细节请看源代码。bool MazeAlgorithm::Pathfinding_Astar() {

frame = 10;

while (!path_astar.empty() && frame--) {

Node head = path_astar.top();

path_astar.pop();

//标记为放入closed表

cells[head.index.x][head.index.y].inOpen = false;

cells[head.index.x][head.index.y].inClosed = true;

cells[head.index.x][head.index.y].visited = 1;

找到终点了

if (head.index == glm::ivec2(row - 1, col - 1)) {

//清空queue,停止搜索

while (!path_astar.empty())path_astar.pop();

break;

}

for (auto x = 0; x < 4; ++x) {//查看邻居

glm::ivec2 loc(head.index.x + to[x][0], head.index.y + to[x][1]);

if (CouldMove(loc) && cells[head.index.x][head.index.y].neighbors[x] == 1) {//无墙隔着

if (cells[loc.x][loc.y].inClosed)continue;//已在closed表中,不管它

if (!cells[loc.x][loc.y].inOpen) {//不在open表中,加入open表

path_astar.push(Node(loc.x, loc.y));

cells[loc.x][loc.y].inOpen = true;

cells[loc.x][loc.y].prev = head.index;

cells[loc.x][loc.y].gn = cells[head.index.x][head.index.y].gn + 1;

cells[loc.x][loc.y].hn = DirectLen(loc, glm::ivec2(row - 1, col - 1));

}

else {//已在open表中,我们进行比较,然后修改前驱

int orig = cells[loc.x][loc.y].gn + cells[loc.x][loc.y].hn;

int nows = cells[head.index.x][head.index.y].gn + cells[loc.x][loc.y].hn + 1;

if (nows < orig) {

cells[loc.x][loc.y].prev = head.index;

cells[loc.x][loc.y].gn = cells[head.index.x][head.index.y].gn + 1;

cells[loc.x][loc.y].hn = cells[loc.x][loc.y].hn;

}

}

}

}

}

if (path_astar.empty()) {//从终点倒退路径到起点

GetSolution();

return true;

}

else return false;

}

A星算法围绕启发式函数进行蔓延。

java动画迷宫寻路_[人工智能] 迷宫生成、寻路及可视化动画相关推荐

  1. c 实现走迷宫流程图_[求助]:迷宫问题 流程图

    得分:0 唉 没大大来帮一下吗 ----------------解决方案-------------------------------------------------------- #includ ...

  2. qt 3d迷宫游戏_玩迷宫也能解锁孩子空间思维,各年龄必备迷宫书单推荐(附游戏资源下载)...

    上篇的文章------- 每日一练 28 语文试卷中惊现数学问题?!(文理什么时候都是一家.)​mp.weixin.qq.com 提到了方位感是数学里很重要的一项技能,同时分享了几个锻炼方位感的小游戏 ...

  3. python制作动画的软件_分享7个好用的动画制作软件,学会它,人人可以成为动画大师...

    如果需要创建动画视频,Windows会提供大量动画制作软件.动画制作软件主要分为2D和3D二种类型.3D应用程序开发CGI视频,而2D软件包允许用户基于2D插图设置视频动画.尽管CGI动画电影在电影中 ...

  4. java keytool 导出证书_使用keytool 生成证书

    keytool 工具介绍 keytool 是java 用于管理密钥和证书的工具,其功能包括: 1 创建并管理密钥 2 创建并管理证书 3 作为CA 为证书授权 4 导入导出证书 keytool 采用k ...

  5. qt 3d迷宫游戏_机械迷宫—一款机械风格的3D立体解谜独立游戏

    解谜游戏一直是游戏类别中的一个大类,这里面各式各样的解密游戏,多不胜数.解密游戏又分很多种类,比如动作冒险新式的解密游戏,文字图像类型的解密游戏:游戏制作上有大有小,但是都有一个共同特点,就是考验玩家 ...

  6. 最强动画制作人书包_声优访谈丨恋与制作人动画中配声优访谈——夏磊

    亲爱的制作人们: 距离恋与制作人动画上线 还有6天! 今天的中配声优访谈嘉宾是 在动画中为许墨献声的 夏磊老师~ 固定布局                                       ...

  7. python能制作ppt动画效果吗_你听说过Python可以做动画吗

    如果Python可以做动画 用Python来写动画,有这么神奇吗? 这个网站的效果图如下,分为两个区域,画布区用于显示动画,代码区编写代码(不用安装任何软件哦~) image.png 零基础的人可以学 ...

  8. moba寻路_硬件商明基寻路电竞

    (图片来源:全景视觉) 经济观察报 记者 洪宇涵  曾经领跑 PC时代的明基(BenQ)仿佛迷失在了移动互联时代.曾经雄心勃勃收购了德国西门子手机部门,一跃成为了全球第四大手机制造商,却在一年后交出了 ...

  9. java迷宫队列实现_Creator 迷宫生成: DFS 与 BFS 算法实现

    前言: 我的迷宫代码的实现受到 [liuyubobobo] 的影响. liuyubobobo 迷宫的实现: GUI 部分使用 java Swing,编程语言是 Java. **我的迷宫代码实现: ** ...

最新文章

  1. Docker官方文档翻译2
  2. 解决Failed to load class org.slf4j.impl.StaticLoggerBinder
  3. udp java 编程_JAVA 网络编程之UDP编程
  4. 基本select语句的生命周期
  5. 测试http请求的Chrome插件:Postman插件的查找安装模拟测试 - 讲解篇
  6. Docker 容器遇到的乱码问题
  7. Java边缘填充_任意画一个多边形,用边缘填充算法填充
  8. MVC仓储执行存储过程报错“未提供该参数”
  9. stm32的ISP下载
  10. 奋斗路上若有你,弱水三千取一瓢——计算机操作系统以及python基本语法,第三天
  11. 轻松掌握Mybatis(上)
  12. thinkphp6+websocket 群聊实现
  13. TDengine集群搭建
  14. 选购摄像头前必看,摄像头参数科普
  15. 这就是你要找的Spring-ioc简单入门!
  16. 错别字分析——自建错词库
  17. mysql rollback to,MySQL存储过程SAVEPOINT ROLLBACK to
  18. Linux和Win10双系统出现GUN GRUB解决方法
  19. JavaScript While循环
  20. KEPServerEX  6.8.796.0  新版本发布说明

热门文章

  1. [MySQL FAQ]系列 -- 新年新思想:MySQL也能并发导入数据
  2. (摘)如何配置Windows Live Writer
  3. xml文件中删除根节点
  4. spark 源码分析之八--Spark RPC剖析之TransportContext和TransportClientFactory剖析
  5. 【js Date】时间字符串、时间戳转换成今天,明天,本月等文字日期
  6. Python入门系列——第14篇
  7. https://gogs.io/
  8. 初识php的笔记(基础知识)
  9. JNI 之 HelloWorld
  10. 图像的抽线、抽丝、抽图 神马是alpha通道