智能寻路贪吃蛇系列之 初级BFS寻路算法

来源:互联网 发布:算王软件打不开 编辑:程序博客网 时间:2024/05/22 17:12
 

智能寻路贪吃蛇系列之 简单贪吃蛇的MFC实现(上)(下) 中我们已经实现了一个简单的贪吃蛇Demo

但是我们要做的是会自动寻路的贪吃蛇,所以本次就实现一个简单的"智能贪吃蛇"
要实现一定的智能,肯定就要用到相应的寻路算法.我采用的是最简单的宽度优先搜索的方式 (BFS算法)
所以在具体的实现游戏之前,我们先来看一下BFS算法.
该算法在<算法导论>中有详细解说,并给出了可行的伪代码,本系列的博文的重点不在于此,所以只是简单一说,然后给出代码.
下面就给出一个例子来说明该算法的寻路过程
(说明:我们将路径抽象化为一个二维数组,在二维数组中,我们用0表示未探索过的通路,用a表示探索过的通路,用1表示不通)
具体到例子,比如说下面一个地图
0 0 1 1
1 0 0 0 
0 0 0 1
1 1 0 0 
假设起始点为(0,0),终止点为(3,3),即从左下角到右下角..
我们通过观察法得,最短的路径为:

(0,0)->(0,1)->(1,1)->(2,1)->(2,2)->(3,2)->(3,3)
下面我们就通过Bfs将该路径求出来,Bfs算法寻路过程如下所示(标蓝的字母是当前步骤搜索的节点):
<0----->
a 0 1 1 
1 0 0 0 
0 0 0 1
1 1 0 0 

<1----->

a a 1 1 
1 0 0 0 
0 0 0 1
1 1 0 0 

<2----->

a a 1 1 
1 a 0 0 
0 0 0 1
1 1 0 0 

<3----->

a a 1 1 
1 a a
0 a 0 1
1 1 0 0 

<4----->
a a 1 1 
1 a a a 
a aa 1
1 1 0 0 
<5----->
a a 1 1 
1 a a
a a a 1
1 1 a

<6----->
a a 1 1 
1 a a a 
a a a 1
1 1 a a 



这样,经过7步,我们就能从起点搜索到终点了.
这样只是找到了终点,那么最短路径是怎样求出来的呢?
下面我们就通过代码来逐步实现一个Bfs寻路算法,并把该算法应用到可爱的贪吃蛇中.
先来看一下头文件:
//Bfs.h//贪吃蛇基本寻路算法.#ifndef BFS_H_H#define BFS_H_H#include <queue>using std::queue;struct XY{int x;int y;};class Bfs{public:void InitBfs(bool **chess,XY size);//初始化图.void CalcBfs(XY st,XY en);//计算Bfs路径.void EetBfs(XY st,XY en);//得到Bfs路径.void CalcQue(XY en);//计算队列.queue<XY> m_que;private:bool **m_chess;//用矩阵表示的图.bool **m_visit;//节点是否被访问过.XY **m_parent;//每个访问过的节点的父节点.XY m_size;//图的大小.};#endif //BFS_H_H

下面就对Bfs类中的成员变量和函数做一下说明:
m_chess是一个二维数组,其中false表示通路,true表示不通,也就是我们要求最短路径的"地图"(跟前面的例子同理).
m_visit是一个跟m_chess等大的数组,用来表示每个节点的访问情况.
m_parent用来表示每个节点的父节点,我们最终得到的路径就是通过该数组得出的.
m_size就是上面三个数组的尺寸了.
还有一个公用队列m_que用来存储最终求得的路径.
InitBfs()函数用来初始化各个数组.
ClacBfs是核心算法,通过该函数得到m_parent数组.
下面就来看一下Bfs.cpp源文件:
//Bfs.cpp#include "stdafx.h"#include "Bfs.h"int dir[4][2]={{0,1},{0,-1},{1,0},{-1,0}};void Bfs::InitBfs(bool **chess,XY size){m_size=size;m_chess=new bool *[m_size.x];m_visit=new bool *[m_size.x];m_parent=new XY *[m_size.x];for(int i=0;i<m_size.x;i++){m_chess[i]=new bool [m_size.y];m_visit[i]=new bool [m_size.y];m_parent[i]=new XY [m_size.y];}for(int i=0;i<m_size.x;i++){for(int j=0;j<m_size.y;j++){m_chess[i][j]=*((bool*)chess+m_size.y*i+j);m_visit[i][j]=false;m_parent[i][j].x=-1;m_parent[i][j].y=-1;}}while(!m_que.empty())m_que.pop();}void Bfs::CalcBfs(XY st,XY en){queue<XY> temque;m_visit[st.x][st.y]=true;temque.push(st);XY head,next;int quesize;while(!temque.empty()){quesize=temque.size();while(quesize--){head=temque.front();temque.pop();if(head.x==en.x&&head.y==en.y)return;//已经达到目的了.for(int i=0;i<4;i++){next.x=head.x+dir[i][0]; next.y=head.y+dir[i][1];if(next.x<0||(next.x>(m_size.x-1))||next.y<0||(next.y>(m_size.y-1))||m_chess[next.x][next.y])continue;if(!m_visit[next.x][next.y]){m_visit[next.x][next.y]=1;temque.push(next);m_parent[next.x][next.y].x=head.x;m_parent[next.x][next.y].y=head.y;}}}}}void Bfs::CalcQue(XY en){if(en.x!=-1&&en.y!=-1){CalcQue(m_parent[en.x][en.y]);m_que.push(en);}}void Bfs::EetBfs(XY st,XY en){CalcBfs(st,en);CalcQue(en);m_que.pop();//弹出没用的起始点..}

需要说明的一点是Dir数组,该数组表示的是上下左右四个方向.
只要看过算法导论的BFS算法部分,其他的地方就非常好理解了,所以不多说了.下面我们就将该算法应用到我们的游戏中..

首先,我们可以设置一个变量,用来标记到底是人在玩还是电脑在玩,并在适当的地方更新这个值..

下一个问题是,怎样得到m_chess数组呢?
其实很简单,除了蛇的身体,剩下的部分都是通路,所以通过如下代码,我们就能得到m_chess了
bool maze[15][25];memset(maze,0,sizeof(maze));list<SnakeNode>::iterator iter=m_snake.m_snake.begin();for(unsigned i=0;i<m_snake.m_snake.size();i++,iter++){XY curpo;curpo.y=iter->rc.left/m_po.x;curpo.x=(iter->rc.top-50)/m_po.y;maze[curpo.x][curpo.y]=1;}

至于起始点和终点就更简单了,起始点就是蛇头,终点就是食物..
所以我们可以编写这样一个函数,用来得到路径:
void CSnakeDlg::SetDire(){XY size;size.x=15;size.y=25;bool maze[15][25];memset(maze,0,sizeof(maze));list<SnakeNode>::iterator iter=m_snake.m_snake.begin();for(unsigned i=0;i<m_snake.m_snake.size();i++,iter++){XY curpo;curpo.y=iter->rc.left/m_po.x;curpo.x=(iter->rc.top-50)/m_po.y;maze[curpo.x][curpo.y]=1;}RECT rect=m_snake.m_snake.back().rc;XY st;st.y=rect.left/m_po.x;st.x=(rect.top-50)/m_po.y;XY en;en.y=m_food.left/m_po.x;en.x=(m_food.top-50)/m_po.y;m_bfs.InitBfs((bool**)maze,size);m_bfs.EetBfs(st,en);}
在每次吃到食物以后,我们就重新执行一下这个函数,用来得到新的路径.
得到路径之后,我们在OnTimer()函数中添加如下代码即可自动地改变贪吃蛇的移动方向了..
if(m_ispc){XY cur=m_bfs.m_que.front();m_bfs.m_que.pop();RECT rc=m_snake.m_snake.back().rc;XY head;head.y=rc.left/m_po.x;head.x=(rc.top-50)/m_po.y;if(cur.x==head.x){if(cur.y-head.y==1)m_dr=DR_RIGHT;else if(cur.y-head.y==-1)m_dr=DR_LEFT;}else if(cur.y==head.y){if(cur.x-head.x==1)m_dr=DR_DOWN;else if(cur.x-head.x==-1)m_dr=DR_UP;}}

这样,贪吃蛇就可以欢快的吃一段时间的食物了..
但是,过一段时间后,可爱的小蛇就自己撞墙死了,跟第一篇博文中贴出来的那只蛇相比,一点都不高大上..
我总结了一下,目前来说这只蛇还有两个很大的问题:

0.由于每次都是吃到食物之后才执行一下Bfs函数,所以,得到的路径就是那一瞬间的最短路径,但是可能在蛇移动的过程中,当前的格局发生了变化,可能会有更短的路径出现,虽然这个问题不是致命的,但是也浪费了蛇吃东西的时间.
1.随着蛇身的增长,蛇的身体很可能将地图分为互不相通的几个部分,所以,当食物和蛇的头部出现在不同的部分时,Bfs算法就没辙了,这个问题是致命的!

所以,虽然我们已经实现了一个还算聪明的蛇,但它还是不够聪明,下一节,我们就解决上面的问题,让蛇变得更加聪明 ! 



[注意:本系列博文所实现的智能贪吃蛇游戏将会是开源的,在该系列的最后一篇博文中,我会放出最终的源代码]

4 0