在游戏中常常会需要使用到寻路算法,常用的有深度寻路、广度寻路、A*寻路等。这些算法都集结了前人的智慧,作为程序员,我们的责任是把这些算法以代码的形式表示出来。本篇记录这两天遇到的A*寻路算法的简易实现。
基础的理论知识网上有很多,这里不再赘述,需要了解的概念有以下几个:
- F值:起点到终点的距离。
- G值:起点到当前点的距离(预估值,会变化)。
- H值:当前点到终点的距离(可以认为是一个常量,到一个确定坐标的点,其曼哈顿距离也就是确定的)。
公式:F = G + H, 直线走一格为10,斜线走一格为14(勾股定理取近似值)
- 曼哈顿距离:其实就是两个点间相差的横纵坐标之和(具体理解->直角三角形的两边和)。
- Openlist:存放可以走的点的集合,下一步要走的点在这个集合中找。简单说就是,从环绕“当前点”周围的八个点中去除不符和条件的点后,剩余的点,记住每走一步都需要检测这八个点,符合条件就添加到openlist中。
添加入openlist的条件:1.不在closelist 中 2.不是障碍点3.不在openlist(已经存在自然就不能重复添加啦)但是需要注意的是,如果这个点当前计算出的G值比原来的G值小,则原来的G值和F值需要替换掉,父节点也改为当前点,这里可能比较难理解,但是想一想,该点当前在openlist中,那它就有可能成为下一个“当前点”,那么如果它的G值(起点到当前点的距离)可以更小,那就必须改变,因为A*的目的就是要找最短路径。
- Closelist:这个表存放所有走过的点和障碍点(在检测八个点的时候加入)。
注意:1.openlist需要定时删除“点”,要怎么理解呢,找下一个目标点需要在openlist中找,它的条件就是F值最小,当找到了这个点后,用一个变量currNode记住它,然后将它从openlist中删除;走过的点就应该删除,否则可能再次选到。
- 7. prev:父节点;这一个概念可能网上别的A*资料没说到,但是它却十分重要,构成地图的点除了有f、g、h这三个属性外,还必须有这个prev属性,它的作用就是将终点和起点间的最短路径连接起来。首先,起点的prev自然就是null,而下一个点(我称其为A)的prev就是起点,再下一个点(B)的prev就是A,依此类推,直到终点,最短路径就是根据终点的prev依次往前推得到;在前边讲openlist时说过,往openlist中添加点时发现该点已经存在,则需要比较我们即将添加入openlist中的点和openlist中已经存在的点,它们两个的G值,如果发现即将添加进去的点G值比较小,则将openlist中的点G值赋值为小值,且改变它的父节点为当前点;(这个叙述可能会使一些网友摸不着头脑,想象一下,当一个点向正下方移动一步时,原点周围的八个点和当前点周围的八个点是不是重合了6个,所有就会出现往openlist中添加点的时候发现它已经存在;还有为什么要将障碍点添加到closelist中呢,原因就是不能让障碍点被判断两次,完全是浪费资源)
说了一大堆,可能用代码来表达简洁多了,使用c语言完成实现
//头文件
#ifndef ASTAR_H
#define ASTAR _H
#define COL_X 9 //9列7行的地图
#define ROW_Y 7
//a星节点
struct AStarNode
{
unsigned int f_; //f值,起点到终点
unsigned int g_;//g值,起点到当前点(预估值)
unsigned int h_;//h值,当前点到终点
int x; //地图坐标
int y;
};
//枚举了八个方向,作用是区别偏移量,方便理解使用枚举
//也可以用下边三个数组快速计算偏移量(顺时针),xy下标一一对应,可以减少冗余代码
//AROUND_X = {0, 1, 1, 1, 0, -1, -1, -1}; //顺时针,由正北开始
//AROUND_Y = {-1, -1, 0, 1, 1, 1, 0, -1};
//AROUND_G = {10, 14, 10, 14, 10, 14, 10, 14}; //环绕的八点,g值
enum dir
{
dir_b, //北
dir_d,//东
dir_n,//南
dir_x,//西
dir_xb,//西北
dir_db,//东北
dir_dn,//东南
dir_xn,//西南
};
#endif
//cpp文件
#include <vector>
#include <math.h>
#include <stdlib.h>
#include <windows.h>
#include “aStar.h”
using namespace std;
//对于openlist来说,每走一步都要删除上一个走的节点,使用vector不是一个好方法,需要改进可以使用map容器,(设置键值的一种方法可以根据xy坐标生成字符串)
vector<AStarNode*> AStar_Closelist;
vector<AStarNode*> AStar_Openlist;
AStarNode* begin_Node; //开始节点
AStarNode* end_Node;//结束节点
AStarNode* current_Node;//当前节点
int aStar_Node[ROW_Y][COL_X] = {
{1, 1, 1, 1, 1, 1, 1, 1, 1,},
{1, 0, 0, 0, 1, 0, 1, 1, 1, },
{1, 0, 1, 0, 0, 0, 1, 1, 1, },
{1, 0, 1, 0, 1, 0, 1, 0, 1, },
{1, 0, 0, 0, 1, 0, 1, 0, 1,},
{1, 0, 1, 0, 1, 0, 0, 0, 1, },
{1, 1, 1, 1, 1, 1, 1, 1, 1,}
};
//设置地图起点和终点
void setBeginAndEnd(int b_y, int b_x, int e_y, int e_x);
//找A星路径,参数isAngle表示是否支持走斜线,默认支持
void findRoute(AStarNode* current_node, bool isAngle = true);
//判断节点是否在openlist上
bool isOpenList(AStarNode* node, AStarNode* current_node);
//删除openlist上的点
void removeOpenListNode(AStarNode* node);
//判断节点是否在closelist上
bool isCloseList(AStarNode* node);
//main函数
int main(void)
{
setBeginAndEnd(3, 1, 3, 7);
//当前点初始化为开始点
current_Node = begin_Node;
//将当前点放入closelist
AStar_Closelist.push_back(current_Node);
//寻找路径
findRoute(current_Node);
system(“pause”);
return 0;
}
//设置地图起点和终点
void setBeginAndEnd(int b_y, int b_x, int e_y, int e_x)
{
begin_Node = new AStarNode;
memset(begin_Node, 0, sizeof(AStarNode));
begin_Node->x = b_x;
begin_Node->y = b_y;
begin_Node->dir = dir_xb; //初始化方向为西北
begin_Node->prev_ = NULL; //起点的父节点为null
//终点
end_Node = new AStarNode;
memset(end_Node, 0, sizeof(AStarNode));
end _Node->x = e_x;
end _Node->y = e_y;
}
//判断节点是否在openlist上
bool isOpenList(AStarNode* node, AStarNode* current_node)
{
vector<AStarNode*>::iterator it = AStar_Openlist.begin();
for(it; it != AStar_Openlist.end(); ++it)
{
//在openlist中发现该点已经存在
if((*it)->x == node->x &&(*it)->y == node->y)
{
//如果该点的g_比较大,则改变它的g_
if((*it)->g_ > node->g_) {
(*it)->g_ = node->g_;
(*it)->f_ = (*it)->g_ + (*it)->h_;
(*it)->prev_ = current_node; //重新指定父节点
}
return true;
}
}
return false;
}
//删除openlist上的点
void removeOpenListNode(AStarNode* node)
{
vector<AStarNode*>::iterator it = AStar_Openlist.begin();
for(it; it != AStar_Openlist.end(); ++it)
{
if((*it)->x == node->x && (*it)->y == node->y)
{
AStar_Openlist.erase(it);
break;
}
}
}
//判断节点是否在closelist上
bool isCloseList(AStarNode* node)
{
vector<AStarNode*>::iterator it = AStar_Closelist.begin();
for(it; it != AStar_Closelist.end(); ++it)
{
if((*it)->x == node->x &&(*it)->y == node->y)
{
return true;
}
}
return false;
}
void findRoute(AStarNode* current_node, bool isAngle = true)
{
AStarNode* currNode = current_node;
bool flag = true;
while(flag)
{
AStarNode* minP = new AStarNode; //储存最优点(下一步要走的点)
memset(minP, 0, sizeof(AStarNode)); //初始化节点
if(AStar_Openlist.empty())
{
minP->f_ = 10000; //如果openlist中没有点,设置一个大值暂时占位,这个情况出现于当前点为起点时
}else{
minP = AStar_Openlist[0]; //将最优点先赋值为openlist的第一个元素
}
//试探方向
for(int i = 0; i < (isAngle ? 8 : 4); ++i)
{
AStarNode* node = new AStarNode;
memset(node, 0, sizeof(AStarNode));
/*node->x = currNode->x + Around_X[i];
node->y = currNode->y + Around_Y[i];
node->g_ = currNode->g_ + Around_G[i];*/
//上述注释的部分可以替换下边这个switch语句
//不过isAngle的功能就需要做一下修改
//短短的三行语句,功能相同,但是却减少了大量冗余代码
switch(i)
{
case dir_xb:
node->x = (currNode->x)-1;
node->y = (currNode->y)-1;
node->g_= currNode->g_ + 14;
break;
case dir_b:
node->x = (currNode->x);
node->y = (currNode->y)-1;
node->g_= currNode->g_ + 10;
break;
case dir_db:
node->x = (currNode->x)+1;
node->y = (currNode->y)-1;
node->g_= currNode->g_ + 14;
break;
case dir_d:
node->x = (currNode->x)+1;
node->y = (currNode->y);
node->g_= currNode->g_ + 10;
break;
case dir_dn:
node->x = (currNode->x)+1;
node->y = (currNode->y)+1;
node->g_= currNode->g_ + 14;
break;
case dir_n:
node->x = (currNode->x);
node->y = (currNode->y)+1;
node->g_= currNode->g_ + 10;
break;
case dir_xn:
node->x = (currNode->x)-1;
node->y = (currNode->y)+1;
node->g_= currNode->g_ + 14;
break;
case dir_x:
node->x = (currNode->x)-1;
node->y = (currNode->y);
node->g_= currNode->g_ + 10;
break;
}
//在closelist中
if(isCloseList(node))
continue;
//障碍点,放入closelist
if(aStar_Node[node->y][node->x] == 1){
AStar_Closelist.push_back(node);
continue;
}
//在openlist中
if(isOpenList(node, currNode))
continue;
//添加到openlist前的赋值操作
//曼哈顿距离计算
node->h_ = (abs(end_Node->x – node->x) +abs(end_Node->y – node->y))*10;
node->f_ = node->g_ + node->h_; //起点到终点距离
node->prev_ = currNode; //指定父节点
//添加
AStar_Openlist.push_back(node);
}
//遍历完八个点后,开始找目标点(下一个要走的点)
//取openlist这f值最小的
int count = AStar_Openlist.size();
//openlist中没有点就说明,地图所有的点都已经走过
if(count == 0){
cout<<”没有正确路径”<<endl;
return;
}
for(int i = 0; i < count; i++)
{
if(minP->f_ > AStar_Openlist[i]->f_)
{
minP = AStar_Openlist[i];
}
}
//判断新的当前点是否等于终点
if(minP->x == end_Node->x && minP->Y == end_Node->y){
cout<<”找到正确路径”<<endl;
AStarNode* pNode = minP;
//从尾到头打印最短路径
while(pNode)
{
cout<<”x:”<<pNode->x<<”y:”<<pNode->y<<endl;
Sleep(500);//休眠0.5秒
pNode = pNode->prev_; //依靠父节点依次向前推
}
return;
}
//删除openlist中的当前点
removeOpenlistNode(minP);
//当前点添加入closelist
AStar_Closelist.push_back(minP);
currNode = minP; //下一轮找点开始
}
}
//代码是手打上去的,可能存在一些小错误,刚开始了解A*的时候也是看了许多资料,后来发现只有通过自己亲自实现才真正明白,代码不重要,重要的是编程思想
1代表障碍物,0表示通路;防止出现越界的情况,所以地图外圈都是障碍物
1 |
1 |
1 |
1 |
1 |
1 |
1 |
1 |
1 |
1 |
0 |
0 |
0 |
1 |
0 |
1 |
1 |
1 |
1 |
0 |
1 |
0 |
0 |
0 |
1 |
1 |
1 |
1 |
0 |
1 |
0 |
1 |
0 |
1 |
0 |
1 |
1 |
0 |
0 |
0 |
1 |
0 |
1 |
0 |
1 |
1 |
0 |
1 |
0 |
1 |
0 |
0 |
0 |
1 |
1 |
1 |
1 |
1 |
1 |
1 |
1 |
1 |
1 |
游戏地图
以上就是关于A*算法的代码实现,存在许多不足,可继续优化,但基本上实现了该算法。
结论:
- A*路径是通过最后一个点的父节点依次向前找出来的;该路径存在于closelist中,需要知道closelist中还存在着许多点,所有父节点的作用尤为重要。
- 每走一步之前,都要将选中的点从openlist中删除。
- 添加入openlist的条件是 1.不在closelist中,以及2.不是障碍点,3.openlist中不存在
码字不易
转载请说明文章的来源、作者和原文的链接。