游戏服务器之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值最小节点。
(2)开启列表含f最小值的小根堆。小根堆参考:
http://blog.csdn.net/chenjiayi_yun/article/details/37654845
(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()
{
}