在網上參考了各大神的代碼後,瞭解了BTS算法:
要實現一定的智能,肯定就要用到相應的尋路算法.我採用的是最簡單的寬度優先搜索的方式
(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 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
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
0 a 0 1
1 1 0 0
<4----->
a a 1 1
1 a a a
a a a 1
1 1 0 0
<5----->
a a 1 1
1 a a a
a a a 1
1 1 a 0
<6----->
a a 1 1
1 a a a
a a a 1
1 1 a a
這樣,經過7步,我們就能從起點搜索到終點了.
然後,頭文件:
- //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;
- }
- }
這樣,貪吃蛇就可以歡快的吃一段時間的食物了..
但是,過一段時間後,可愛的小蛇就自己撞牆死了,跟第一篇博文中貼出來的那隻蛇相比,一點都不高大上..
我總結了一下,目前來說這隻蛇還有兩個很大的問題:
1.由於每次都是吃到食物之後才執行一下Bfs函數,所以,得到的路徑就是那一瞬間的最短路徑,但是可能在蛇移動的過程中,當前的格局發生了變化,可能會有更短的路徑出現,雖然這個問題不是致命的,但是也浪費了蛇吃東西的時間.
2.隨着蛇身的增長,蛇的身體很可能將地圖分爲互不相通的幾個部分,所以,當食物和蛇的頭部出現在不同的部分時,Bfs算法就沒轍了,這個問題是致命的!
OK,智障蛇死了......