遊戲伺服器之優化a星尋路
遊戲伺服器之a星尋路 主要用於npc找玩家。這個是個a*演算法的優化演算法。
設計上:
(1)使用開啟列表和關閉列表:限制構建二叉堆大小(目前最大是150次計算,經過統計超過1000的一般是尋路失敗),比傳統的a*演算法可以提升幾倍的效率(測試後結果,大概4、5倍)。理論參考:http://blog.csdn.net/chenjiayi_yun/article/details/20363203
(2)節點地圖:使用節點地圖記錄所有可以尋到的點(包含開啟列表和關閉列表)的最小的f值,記錄起始座標和尋路半徑,每次尋路時重新設定和清空列表。
(3)節點計算:
1)從起點開始算, 將其加入開啟列表。
2)從開啟列表出來的最小f值的點就是當前的點,可以放到關閉列表。放置到關閉列表的點將不再計算。
3)當前的點的周圍8個點都是將要計算的點。計算其g、h值和父節點。
4)如果新的要計算的點,已在開啟列表或者關閉列表,若其計算的f值小於原來計算的f值,則重新整理該節點的h值和g值和父節點。否則放置到開啟列表。
5)計算結束後(新計算的點是終點),節點地圖中已經包含是算好的f值最小路徑的節點了。從目標點往上追溯(根據地圖節點的父節點來算),就是需要的路徑了。
關鍵點(數值部分按目前使用值來算):
(1)最大尋路半徑:20格子。
(2)節點地圖:是以源格子為中心的最大尋路半徑2倍加1 為邊的正方形(大小 (2*20+1) * (2*20+1),儲存結構是陣列)。
實際處理,使用靜態節點地圖,開啟列表和關閉列表使用的儲存空間是共用同一個靜態節點地圖空間(使用的是靜態節點地圖的指標),因為路徑列點的狀態在同一時間上只可能是一個:在開啟列表、在關閉列表、或不在兩個列表。(用兩個列表的兩個節點地圖應該也可以,最後使用closelist回退路徑節點到路徑列表就可以).
靜態節點地圖是大小為(2*m_radius+1) * (2*m_radius+1)的節點列表結構(m_radius是尋路半徑大小,目前是固定20格子,可以調整,目前設定是一個屏是一個23*14格子)。
地圖節點含有成員(nPos pos(地圖位置);nPos father(上一個節點);uint32 g(g值);uint32 h(h值);)
(3)開啟列表(OpenList): 是尋路開始狀態的節點的列表(計算路徑點的開始依據).開啟列表裡含有一個f值的最小堆(列表儲存結構的最小堆二叉樹)。
(4)關閉列表(CloseList): 是尋路結束狀態的節點的列表(放到關閉列表的路徑點就不會再放到開啟列表)。
(5)a*尋路概念:
H值是從當前方塊到終點的移動量估算值(格子)。
G是從開始點A到達當前方塊的移動量(格子)。
F等於G+H。
g點的計算:point.g = nowPoint.g + offset[i][2];//g點的計算,offset是當前點的周圍的點(是個偏移計算矩陣),nowPoint是當前的點
h點的計算:point.h = calH(point.pos, toPos);//h點的計算 ,折線距離:abs(fromPos.x-toPos.x) + abs(fromPos.y-toPos.y)
計算權重
static const int offset[8][3] =
{ // 偏移計算矩陣(x,y,weight)
{-1,1,1}, {0,1,1}, {1,1,1},
{-1,0,1}, {1,0,1},
{-1,-1,1}, {0,-1,1}, {1,-1,1},
};
優化點:
(1)使用開啟列表和關閉列表。開啟列表作為將要計算的節點,關閉列表是計算結束且F值最小節點。
(3)開啟列表和關閉列表共享靜態節點地圖。
(4)尋路計算次數限制(目前150)。
(5)靜態節點地圖使用陣列實現的,預分配大小。
(6)地圖節點記錄父節點來追蹤路徑點。1、移動AI處理
/**
* \description 移動AI處理
* \return 執行是否成功
*/
bool scene_npc::doMovetoAI()
{
if(_move_timer(main_logic_thread::currentTime))//定時器時間跟待機移動速度有關
{
if(this->base->runsp)//追擊移動速度
{
_move_timer.reset(this->base->runsp, main_logic_thread::currentTime);//定時器時間變成跟追擊移動速度
bool ret = goToFindPath(this->getPos(), nPos(AIC->curAI.regionX,AIC->curAI.regionY));//根據當前位置和目標位置來計算移動
return ret;
}
}
return false;
}
從尋找到的路徑列表中,從後往前獲取移動位置
uint8 AStar::goToFindPath(const nPos &srcPos, const nPos &destPos)
{
if(!m_path.empty())
{
if(m_path[0] != destPos || m_path.size() == 1)//只要目的位置改變,則需要重尋路。不佔有目的位置。
{
m_path.clear();
}
else
{
nPos nextPos = m_path.back();
m_path.pop_back();
//由路徑最後獲取位置,作為下一個移動的目的
//對於npc,則重新整理npc物件的位置和其相關的屏索引
//每移動一次,需要等到定時器下次移動時再執行下一次移動
if(move(nextPos))
{
return ASTAR_SUCCESS;
}
}
}
else//路徑列表為空,則需要尋路
{
findPath(srcPos, destPos);//a*尋路 ,把搜尋到的路徑放到路徑列表
}
return false;
}
2、a*尋路演算法
步驟如下:
1)設定節點地圖開始節點,初始化節點地圖。初始化開啟列表、關閉列表。
2)加入開始節點到開啟列表
3)從開啟列表彈出F值最小的節點,作為本節點,並加入關閉列表
4)獲取本節點的周圍8個節點,分別作為新節點依次處理(步驟為5-10))
{
5)計算新節點的g、h值和父節點
6)檢查新節點合法性,是否在節點地圖,否則結束該節點處理
7)新節點是目的節點,放到關閉列表,尋路結束
7)新節點是否阻擋,否則結束該節點處理
8)新節點是否已經在開啟列表,是則更新其在該列表的g、h值和父節點(根據f值更小的比較),且結束該節點處理
9)新節點是否已經在關閉列表,是則更新其在該列表的g、h值和父節點(根據f值更小的比較),且結束該節點處理
10)新節點新增到開啟列表
}
11)尋路計數器加1,計數超過150則尋路結束,否則繼續步驟3)
程式碼如下:
AStarResult AStar::findPath(const nPos &fromPos, const nPos &toPos)
{
m_path.clear();
if ( fromPos == toPos )//已是在目標點
{
return ASTAR_NONE_OPT;
}
// 尋路半徑檢查
unsigned int dx = abs(fromPos.x - toPos.x);
unsigned int dy = abs(fromPos.y - toPos.y);
if ( dx > DEFAULT_MAX_RADIUS || dy > DEFAULT_MAX_RADIUS )//計算距離不可以超過尋路半徑,DEFAULT_MAX_RADIUS (20)
{
return ASTAR_TOO_FAR_OPT;
}
// A* 演算法開始
static const int offset[8][3] =
{ // 偏移計算矩陣(x,y,weight)
{-1,1,1}, {0,1,1}, {1,1,1},
{-1,0,1}, {1,0,1},
{-1,-1,1}, {0,-1,1}, {1,-1,1},
};
//節點地圖是尋路半徑的儲存空間,以源點為中心,最大尋路半徑的2倍為邊的正方形
static NodeMap nodeMap(fromPos, DEFAULT_MAX_RADIUS);
//開啟列表和關閉列表共享同一個節點地圖空間
static OpenList openList(&nodeMap);// OpenList 是尋路開始狀態的節點的列表(計算路徑點的開始依據)
static CloseList closeList(&nodeMap);// CloseList 是尋路結束狀態的節點的列表(放到關閉列表的路徑點就不會再放到開啟列表)
m_maxRadius = DEFAULT_MAX_RADIUS;//最大尋路半徑設定為20
nodeMap.reset( fromPos, m_maxRadius );//設定源節點、置空節點地圖
openList.clear();
closeList.clear();
PathPoint startPoint;//開始路徑點
startPoint.pos = fromPos;
startPoint.father = NullPos;
openList.add(startPoint);//新增源節點到開啟列表
AStarResult res = ASTAR_NONE_OPT;
unsigned int loopCounter = 0;
//每次計算最多150個尋路節點(一般尋路不會超過150個尋路,根據驗證),每次需要計算尋路節點上週邊的8個點,總計算次數最大(150*8 = 1200)
while ( loopCounter < MAX_EXTENDED_NODE_NUM )
{
//從堆的根節點開始查詢,屬於openList的f值最小的節點,如果找不到則表示尋路失敗
//否則就放置到關閉列表(那些點以後一直在節點地圖裡)
PathPoint nowPoint = openList.popMinF();
if ( nowPoint == NullPathPoint )
{
res = ASTAR_UNREACHABLE_OPT;
break;
}
closeList.add(nowPoint);
// 周圍8個節點
for ( int i=0; i<8; i++)//計算某節點的周圍8個節點
{
PathPoint point;
point.pos.x = nowPoint.pos.x + offset[i][0];
point.pos.y = nowPoint.pos.y + offset[i][1];
point.father = nowPoint.pos;
point.g = nowPoint.g + offset[i][2];//g點的計算,offset是當前點的周圍的點的矩陣(這裡只是估算,所以在放到開啟和關閉列表後會再比較更新一次g值和h值)
point.h = calH(point.pos, toPos);//h點的計算 :abs(fromPos.x-toPos.x) + abs(fromPos.y-toPos.y)
// 檢查節點合法性,是否在節點地圖(是否超過尋路的範圍)
PathPoint *p = nodeMap.checkAndGet(point.pos);
if ( !p ) continue;
// 是否到達目標點
//到達目的點的 點就放到closeList
if ( abs((int) point.pos.x - (int)toPos.x ) < 1 &&
abs( (int)point.pos.y - (int)toPos.y ) < 1 )
{
closeList.add(point);
// 構造一個toPos的PathPoint節點
point.g += 1;//g點的計算
point.h = 0;
point.father = point.pos;
point.pos = toPos;
closeList.add(point);//closeList表示計算結束(該節點的狀態會轉換為NS_CLOSELIST)(會更新該節點地圖上的節點)
res = ASTAR_SUCCESS_OPT;//尋路結束
break;
}
// 是否阻擋
if (moveable(point.pos, false) )
{
continue;
}
// 是否已經在OpenList
if ( openList.checkAndUpdate(point) )//更新該點在開啟列表的g、h值(根據f值更小的比較)
{
continue;
}
// 是否已經在CloseList,是就更新f值小的
if ( closeList.checkAndUpdate(point) )//更新該點在關閉列表的g、h值
{
continue;
}
//如果該節點是不在openList或closeList的,是可移動的節點, 就新增到開啟列表
openList.add(point);
}
if ( res != ASTAR_NONE_OPT )//如果尋路結束就跳出迴圈
{
break;
}
loopCounter++;
}
// 如果找到目的點,就回退新增到路徑列表(路徑列表開始第一個點是目標節點,一直到開始節點的下一個點)
if (res == ASTAR_SUCCESS_OPT)
{
PathPoint *pPoint = nodeMap.checkAndGet(toPos);//nodeMap裡的點是計算好的路徑點的路徑點堆儲存
//m_path路徑列表是以源路徑點為開始的,從堆節點的一直往上追溯(從父節點追溯)的路徑點的列表
while( pPoint && pPoint->pos != fromPos )
{
m_path.push_back( pPoint->pos );//把計算到的二叉樹(f值最小堆)裡的點都放到尋路列表裡。如果超出尋路半徑的範圍就結束
pPoint = nodeMap.checkAndGet( pPoint->father );
}
}
return res;
}
其中,最大拓展節點:
#define MAX_EXTENDED_NODE_NUM(150)
A*演算法最大拓展節點:
經過統計,一般可以尋到路徑的平均值不會超過50(網格,32*32畫素一個網格),超過100的值很少;
失敗情況下平均都會在1000以上(生存副本中多數尋路半徑情況統計)。
可以根據實際情況調整。
3、路徑列表的實現
使用開啟列表和貫標列表來來儲存二叉樹(路徑點的f值的最小堆)
(1)路徑節點
struct PathPoint
{
..
// 注意下面這兩個過載運算子比較的東西不一樣
bool operator < ( const PathPoint &point ) { return g+h < point.g+point.h; }
bool operator == ( const PathPoint &point ) { return pos == point.pos; }
nPos pos;
nPos father;
uint32 g;
uint32 h;
};
(2)節點地圖
節點容器是用來儲存路徑節點的,是std::vector實現的列表容器,大小是(2*m_radius+1) * (2*m_radius+1),m_radius是尋路半徑(目前預設設定是20)。
/**
* \brief 節點地圖
*
* 用節點座標x,y直接索引到PathPoint儲存位置。
* 比較佔地方,但是免去搜索操作。
* 需要預設儲存大小
*
*/
class NodeMap {
public:
NodeMap( const nPos &source, uint32 radius = DEFAULT_MAX_RADIUS );
inline void reset( const nPos &source, uint32 radius );
inline PathPoint* checkAndGet( const nPos &pos );
inline PathPoint* checkAndGet( const uint32 &x, const uint32 &y);
private:
inline uint32 calIndex(const uint32 &x, const uint32 &y) const;
inline void resize( uint32 newSize );
inline uint32 getSize() const;
inline bool checkInRangeX(const uint32 &x) const;
inline bool checkInRangeY(const uint32 &y) const;
private:
typedef std::vector<PathPoint> NodeMapType;
NodeMapType m_nodeMap;
uint32 m_radius;
nPos m_sourcePos;
};
//----------------------------
// NodeMap 實現
//----------------------------
NodeMap::NodeMap(const nPos &source, uint32 radius) : m_radius(radius), m_sourcePos(source)
{
m_nodeMap.clear();
m_nodeMap.resize(getSize());
}
void NodeMap::reset(const nPos &source, uint32 radius)
{
m_sourcePos = source;
m_radius = radius;
m_nodeMap.resize(getSize());
//這行完成的功能就是把PathPoint的值恢復成預設值.PathPoint::father沒有設定成nNullPos,但是這樣不會影響計算過程.
bzero(&(*m_nodeMap.begin()), sizeof(PathPoint) * m_nodeMap.size());
}
PathPoint* NodeMap::checkAndGet(const nPos &pos)
{
return checkAndGet(pos.x, pos.y);
}
PathPoint* NodeMap::checkAndGet(const uint32 &x, const uint32 &y)
{
if (nInvalidPos == m_sourcePos || 0 == m_radius)
{
return NULL;
}
if (!checkInRangeX(x))//檢查是否在橫座標的範圍內
{
return NULL;
}
if (!checkInRangeY(y))//檢查是否在縱座標的範圍內
{
return NULL;
}
return &(m_nodeMap[calIndex(x,y)]);
}
bool NodeMap::checkInRangeX(const uint32 &x) const
{
uint32 maxX = m_sourcePos.x + m_radius;
uint32 minX = m_sourcePos.x > m_radius ? m_sourcePos.x-m_radius : 0;
if (x > maxX || x < minX)
{
return false;
}
else
{
return true;
}
}
bool NodeMap::checkInRangeY(const uint32 &y) const
{
uint32 maxY = m_sourcePos.y + m_radius;
uint32 minY = m_sourcePos.y > m_radius ? m_sourcePos.y-m_radius : 0;
if (y > maxY || y < minY)
{
return false;
}
else
{
return true;
}
}
uint32 NodeMap::calIndex(const uint32 &x, const uint32 &y) const
{
uint32 dx = x - (m_sourcePos.x - m_radius);
uint32 dy = y - (m_sourcePos.y - m_radius);
return dx + dy * 2 * m_radius;
}
uint32 NodeMap::getSize() const
{
//現在m_radius是沒有變化的,所以size沒有變化
return (2*m_radius+1) * (2*m_radius+1);
}
(3)開啟列表和關閉列表
開啟列表(OpenList ):一個記錄下所有被考慮來尋找最短路徑的格子。需要考慮計算open裡的節點的周圍的點。 維持著f值的節點小根堆。
關閉列表(CloseList):一個記錄下不會再被考慮計算的格子。
/**
* \brief OpenList
*/
class OpenList {
public:
OpenList( NodeMap *pNodeMap );
inline bool add( const PathPoint &point );
inline bool checkAndUpdate( const PathPoint &point );
inline PathPoint popMinF();
inline void clear();
private:
NodeMap *m_pNodeMap;//節點地圖指標
BinaryHeap<PathPoint> m_binaryHeap;//二叉堆實現最小F值節點查詢
};
//----------------------------
// OpenList 實現
//----------------------------
OpenList::OpenList(NodeMap *pNodeMap) : m_pNodeMap(pNodeMap)
{
}
bool OpenList::add(const PathPoint &point) //插入二叉樹
{
PathPoint *pPoint = m_pNodeMap->checkAndGet(point.pos);//檢查該點是否在指定點的半徑範圍內(ai的尋路半徑),在的話就返回範圍點列表裡的對應的點
if (!pPoint)
{
return false;
}
if (PathPoint::NS_NONE != pPoint->state)
{//沒有加入過列表的
return false;
}
pPoint->pos = point.pos;
pPoint->father = point.father;
pPoint->g = point.g;
pPoint->h = point.h;
pPoint->state = PathPoint::NS_OPENLIST;
m_binaryHeap.putNodeIn(pPoint);//把該節點放到小根堆上
return true;
}
bool OpenList::checkAndUpdate(const PathPoint &point)
{
PathPoint *pPoint = m_pNodeMap->checkAndGet(point.pos);//檢查該點是否在起始節點的半徑範圍內(ai的尋路半徑)
if (!pPoint)
{
return false;
}
if (PathPoint::NS_OPENLIST != pPoint->state)
{//檢查該點是否在openlist裡面
return false;
}
if (pPoint->g + pPoint->h > point.g + point.h)
{ //更新該點到openlist,把g+h 值較小的就把g和h值重新賦值到openlist的該節點
pPoint->g = point.g;
pPoint->h = point.h;
pPoint->father = point.father;
m_binaryHeap.putNodeIn(pPoint);//小根堆的使用參考:http://blog.csdn.net/chenjiayi_yun/article/details/37654845
}
return true;
}
PathPoint OpenList::popMinF()
{
PathPoint *pPoint = NULL;
// 因為add會加入指向相同位置的指標到二叉堆裡面,
// 在pop掉一個後邏輯上就不應該在OpenList中了,
// 所以在二叉堆裡面指向沒有標記OpenList物件的指標需要全部忽略掉。
do {
pPoint = m_binaryHeap.popRootNodeOut();//彈出小根堆裡面的二叉樹頂點節點,(從二叉堆中的 獲取f值最小的點,彈出根節點,調整最小堆)
if (!pPoint)
{
return NullPathPoint;
}
}
while (PathPoint::NS_OPENLIST != pPoint->state);
// 出去的時候一定要標記
pPoint->state = PathPoint::NS_NONE;
return *pPoint;
}
void OpenList::clear()
{
m_binaryHeap.clear();
}
/**
* \brief CloseList
*/
class CloseList {
public:
CloseList( NodeMap *pNodeMap );
inline bool add( const PathPoint &point );
inline bool checkAndUpdate( const PathPoint &point );
inline void clear();
private:
NodeMap *m_pNodeMap;
};
//----------------------------
// CloseList 實現
//----------------------------
CloseList::CloseList(NodeMap *pNodeMap) : m_pNodeMap(pNodeMap)//節點地圖
{
}
bool CloseList::add(const PathPoint &point)
{
PathPoint *pPoint = m_pNodeMap->checkAndGet(point.pos);
if (!pPoint)
{
return false;
}
if (PathPoint::NS_NONE != pPoint->state)
{
return false;
}
pPoint->pos = point.pos;//設定該點的資料到節點容器的指定節點
pPoint->father = point.father;
pPoint->g = point.g;
pPoint->h = point.h;
pPoint->state = PathPoint::NS_CLOSELIST;
return true;
}
bool CloseList::checkAndUpdate(const PathPoint &point)
{
PathPoint *pPoint = m_pNodeMap->checkAndGet(point.pos);
if (!pPoint)
{
return false;
}
if (PathPoint::NS_CLOSELIST != pPoint->state)
{
return false;
}
if (pPoint->g + pPoint->h > point.g + point.h)
{//更新f值更小的節點,把g+h 值較小的就把g和h值重新賦值到closelist的該節點
pPoint->g = point.g;
pPoint->h = point.h;
pPoint->father = point.father;
}
return true;
}
void CloseList::clear()
{
}