美文网首页C++实现的
寻路算法A-Star(A*)的C++实现

寻路算法A-Star(A*)的C++实现

作者: crossous | 来源:发表于2019-06-20 15:50 被阅读13次

    前言

      在游戏中,一个单位从一个地点移动到另一个地点总是复杂的,需要考虑的因素有很多,不过今天只实现最简单的寻路算法。
      地图的组织往往是一个图的数据结构,例如地形贴图,单通道的值会直接储存地形的高度信息。
      我们希望通过寻路算法(Pathfinding Algorithm),找到两点之间的最短路径。
      作为一个地图,两点之间的路径必然是有很多的,最笨的方法是将所有路径都检测出来,取其长度最小的路径。
      更好的算法还有迪杰斯特拉(Dijkstra)最短路径算法,以及BFS(Breadth First Search)广度优先算法,都能找到最短路径。
      A-Star算法采用了一种“启发式”的方法,会比这些寻路算法更快。

    基本原理

      首先,这个算法包含一张基础的地图,包含以下的元素:
    • 一张二维网格组成的地图
    • 绿色的起始点
    • 红色的终结点
    • 灰色的障碍
        我们需要找到一个从绿色起始点到达红色终结点的路径。寻路算法根据需求不同,走法并不统一,例如行走方向是8方向还是4方向(“米”字还是“十”字),会不会像象棋那样被“别住”马腿(例如2行4列的方格是否能直接斜走到1行5列的方格中)。
      在此声明:文章默认实现8方向,无阻塞的寻路。如果有需求,也很容易改变。
        在A*算法中,我们不会记录路径;实现方法是:每个节点(方格)都会记住走到自己方格的上一个方格是谁;假如我们的算法走到终点,就代表我们成功了,我们只需要不断找上一个方格,就能还原出这一个最短路径。

      每一个方格还有其他属性。首先,是当前节点的行列坐标。其次,是三个变量,我们命名为F、G、H
      其中G的含义为:从原点出发,沿着路径走到当前节点所需要的“代价”。
      从一个格子到另一个格子的“花销”往往是不同的,比如我们做了一个2D RPG游戏,同样是一格子的距离,假如路面有“泥泞”属性,就会导致时间花销变大;假如地面有“坑坑洼洼”属性,就会导致耐久变低等等,这些“花销”可以通过一些评价方法(会直接规定)统一为一种“花销”指标,而这个指标可看做路径的权值。
      我们的实现不用那么复杂,先简单定义一种开销:长度开销,从一个格子水平或垂直到达另一个格子花销为10,而斜向到达花销为14(近似√(10+10)=14.14)
      H的含义为:从当前节点出发,大致到达终点的“开销”。
      这个变量H就是体现A*算法“启发式”的地方,这个变量H的“开销”并非是精准的,我们可以用一些合理的方法来测算它,最简单的方法是比较直接距离,例如用沟谷定理直接算欧式距离,不过这样会出小数,而我们的“开销”暂时定义为整形,因此采用“曼哈顿距离”(也称街道距离,就是算两者横纵坐标绝对值之和)。
      F是用来评估路径的变量,能帮助我们最快接近最短路径,最简单的方式:F=G+H,这样一来,F的含义就是:从起始点出发走到当前的花费,加上预计到达终点的花费,也就是预计的总花费。或许F变量可以采用一些加权平均的计算方式,有兴趣可以试试。
      现在举个例子,坐标(4, 3)(非数组方式,而是坐标系方式,3行4列或x=4,y=3),起始点右侧的格子。从起始点只需要一步,因此G=10,曼哈顿距离到达终点H=30,因此F=40
      根据上方的定义,我们写出了格子的结构体:

    struct Node {
        int x;
        int y;
    
        int G;
        int H;
        int F;
        Node* prev = nullptr;//路径的前一个格子
    
        bool visiable;//可访问的
    };
    

      接下来再用到两个列表,openlistcloselistopenlist代表已知但未探索过的节点,closelist代表探索完成的节点。
      可以想象为:我们站在一个格子中,我们的视线能看透周围8个格子,每看到新的格子,便加入到openlist中,每从openlist中探索(走过)完一个格子,便将其从openlist中去除,并加入到closelist中。
      如下图,走过的格子在closelist中(淡蓝色),未走过,但已知的格子在openlist中(绿色):

    算法步骤

    伪代码:

    起点G=0,H=曼哈顿距离,F=G+H
    其他格子暂时将F和G设为无限大
    起点加入openlist
    loop(openlist不为空){
        找到openlist中F值最小的的格子,将其设为当前处理的格子
        if 这个格子就是end格子:
            return true
    
        计算当前格子周围8个格子(如果是超出边界,或者是障碍物,或者已经探索过(处于closelist中),则不予处理,):
            if (当前格子的G+当前格子到达周围格子的开销(10或14)) < 周围格子本身的G,则更新周围格子的G和F(因为路径更短,H不会变)
            if 这个周围格子不在openlist,也不再closelist中,则加入openlist
    
        将当前格子取出openlist并加入closelist中
    }
    return false(能找到的都找遍了,说明找不到路径)
    

    思路大致清晰,接下来是代码的编写

    代码实现

      由于涉及可视化比较麻烦,因此采用打印到控制台输出。
      在这个程序中,我希望将地图信息以字符数组的方式传入,程序应该告诉我们是否能找到路径,并将找到的最短路径打印出来。

    //以这种方式
    int main() {
    //地图可视化参照上方图片
    //O代表可行路径
    //X代表障碍物
    //S代表起点
    //E代表终点
        std::vector<std::vector<char>> map = {
            {'O', 'O','O','O','O','O','O','O'},
            {'O', 'O','O','O','X','O','O','O'},
            {'O', 'O','S','O','X','O','E','O'},
            {'O', 'O','O','O','X','O','O','O'},
            {'O', 'O','O','O','O','O','O','O'},
            {'O', 'O','O','O','O','O','O','O'}
        };
    
        A_Star problem(map);
    
        if (problem.solve()) {
            problem.print_map();
        }
    }
    

      我们需要实现的就是A_Star类,针对它的功能,我们对其做如下声明:

    class A_Star {
    public:
        A_Star(const std::vector<std::vector<char>>& map);
        ~A_Star();
        bool solve();//解决方案
        void print_path();//追溯路径,打印在控制台上
        void print_map();//直接打印寻找的地图
    
        Node* m_map = nullptr;//地图用一维数组数组存储
        int m_map_height;//高和宽
        int m_map_weight;
        Node* m_start = nullptr;//开始和结束格子
        Node* m_end = nullptr;
    }
    

      对于如果有拷贝需求,拷贝构造函数还是很需要的,按需写即可。
      对于构造方法,希望它能将字符串数组转化为格子结构,并初始化属性。

    A_Star::A_Star(const std::vector<std::vector<char>>& map) {
        m_map_height = map.size();
        m_map_weight = map[0].size();
    
        if (m_map_height == 0 || m_map_weight == 0) {
            throw std::exception("map format error");
        }
    
        m_map = new Node[m_map_height * m_map_weight];
        for (int i = 0; i < m_map_height; ++i) {
            for (int j = 0; j < m_map_weight; ++j) {
                Node& curr_node = m_map[i*m_map_weight + j];
    
                curr_node.x = j;
                curr_node.y = i;
    
                curr_node.G = std::numeric_limits<int>::max();//先全部设置为无限大
                curr_node.F = std::numeric_limits<int>::max();
    
                switch (map[i][j]) {
                case 'O':
                    curr_node.visiable = true;
                    break;
                case 'X':
                    curr_node.visiable = false;
                    break;
                case 'S':
                    curr_node.visiable = true;
                    m_start = &curr_node;
                    break;
                case 'E':
                    curr_node.visiable = true;
                    m_end = &curr_node;
                    break;
                default:
                    throw std::exception("illegal identifier exist");
                }
            }
        }
    
        if (m_start == nullptr) {
            throw std::exception("start point is not exist");
        }
        if (m_end == nullptr) {
            throw std::exception("end point is not exist");
        }
    
        for (int i = 0; i < m_map_height; ++i) {
            for (int j = 0; j < m_map_weight; ++j) {
                Node& curr_node = m_map[i*m_map_weight + j];
                curr_node.H = (std::abs(curr_node.x - m_end->x) + std::abs(curr_node.y - m_end->y)) * 10;//计算曼哈顿距离
            }
        }
    }
    

      析构方法很简单,我们只动态申请了地图,释放掉就好了。

    A_Star::~A_Star() {
        delete[] m_map;
    }
    

      对于solve方法,我希望能根据初始化的数据来找到一条最短路径,如果找到就返回true,否则返回false,并将寻找的路径信息存在Node结构中。
      代码相对较长,但处理周围8个节点中有很多重复代码

    bool A_Star::solve() {
    
        std::vector<Node*> openlist;
        std::vector<Node*> closelist;
    
        openlist.push_back(m_start);//开始节点加入openlist
        m_start->G = 0;//开始节点的G置零并计算F
        m_start->F = m_start->H;
    
        while (openlist.size() != 0) {//如果openlist不为空
    
            Node* curr_node = nullptr;//找到F值最小的节点作为当前处理节点
            for (std::vector<Node*>::iterator node = openlist.begin(); node != openlist.end(); node++) {
                if (curr_node == nullptr) {
                    curr_node = *node;
                    continue;
                }
                else if ((*node)->F <= curr_node->F) {
                    curr_node = *node;
                }
            }
    
    
            if (curr_node == m_end) {//如果这个节点刚好是终点,直接返回true退出
                return true;
            }
    
            bool has_prev_x = curr_node->x - 1 >= 0;
            bool has_next_x = curr_node->x + 1 < m_map_weight;
            bool has_prev_y = curr_node->y - 1 >= 0;
            bool has_next_y = curr_node->y + 1 < m_map_height;
    
            if (has_prev_x) {//存在左侧
                Node* left = &m_map[curr_node->y * m_map_weight + curr_node->x - 1];
                if (left->visiable && std::find(closelist.begin(), closelist.end(), left) == closelist.end()) {//如果此路径可用并且不在closelist中
                    if (curr_node->G + 10 < left->G) {//自身G走10到达left节点比left原来的G小,则给left_up换条路径,并重新计算G和F
                        left->prev = curr_node;
                        left->G = curr_node->G + 10;
                        left->F = left->G + left->H;
                        if (std::find(openlist.begin(), openlist.end(), left) == openlist.end()) {//如果不在openlist中,加入openlist
                            openlist.push_back(left);
                        }
                    }
                }
                if (has_prev_y) {//存在左上
                    Node* left_up = &m_map[(curr_node->y - 1)*m_map_weight + curr_node->x - 1];
                    if (left_up->visiable && std::find(closelist.begin(), closelist.end(), left_up) == closelist.end()) {
                        if (curr_node->G + 14 < left_up->G) {//自身G走14到达left_up节点比left_up原来的G小,则换条路径
                            left_up->prev = curr_node;
                            left_up->G = curr_node->G + 14;
                            left_up->F = left_up->G + left_up->H;
                            if (std::find(openlist.begin(), openlist.end(), left_up) == openlist.end()) {//如果左侧节点不再openlist中,则添加进去
                                openlist.push_back(left_up);
                            }
                        }
                    }
                }
                if (has_next_y) {//存在左下
                    Node* left_down = &m_map[(curr_node->y + 1)*m_map_weight + curr_node->x - 1];
                    if (left_down->visiable && std::find(closelist.begin(), closelist.end(), left_down) == closelist.end()) {
                        if (curr_node->G + 14 < left_down->G) {//自身G走14到达left_down节点比left_down原来的G小,则换条路径
                            left_down->prev = curr_node;
                            left_down->G = curr_node->G + 14;
                            left_down->F = left_down->G + left_down->H;
                            if (std::find(openlist.begin(), openlist.end(), left_down) == openlist.end()) {
                                openlist.push_back(left_down);
                            }
                        }
                    }
                }
            }
            if (has_next_x) {//存在右侧
                Node* right = &m_map[curr_node->y * m_map_weight + curr_node->x + 1];
                if (right->visiable && std::find(closelist.begin(), closelist.end(), right) == closelist.end()) {
                    if (curr_node->G + 10 < right->G) {
                        right->prev = curr_node;
                        right->G = curr_node->G + 10;
                        right->F = right->G + right->H;
                        if (std::find(openlist.begin(), openlist.end(), right) == openlist.end()) {
                            openlist.push_back(right);
                        }
                    }
                }
                if (has_prev_y) {//存在右上
                    Node* right_up = &m_map[(curr_node->y - 1)*m_map_weight + curr_node->x + 1];
                    if (right_up->visiable && std::find(closelist.begin(), closelist.end(), right_up) == closelist.end()) {
                        if (curr_node->G + 14 < right_up->G) {
                            right_up->prev = curr_node;
                            right_up->G = curr_node->G + 14;
                            right_up->F = right_up->G + right_up->H;
                            if (std::find(openlist.begin(), openlist.end(), right_up) == openlist.end()) {
                                openlist.push_back(right_up);
                            }
                        }
                    }
                }
                if (has_next_y) {//存在右下
                    Node* right_down = &m_map[(curr_node->y + 1)*m_map_weight + curr_node->x + 1];
                    if (right_down->visiable && std::find(closelist.begin(), closelist.end(), right_down) == closelist.end()) {
                        if (curr_node->G + 14 < right_down->G) {
                            right_down->prev = curr_node;
                            right_down->G = curr_node->G + 14;
                            right_down->F = right_down->G + right_down->H;
                            if (std::find(openlist.begin(), openlist.end(), right_down) == openlist.end()) {
                                openlist.push_back(right_down);
                            }
                        }
                    }
                }
            }
            if (has_prev_y) {//存在上侧
                Node* up = &m_map[(curr_node->y - 1)*m_map_weight + curr_node->x];
                if (up->visiable && std::find(closelist.begin(), closelist.end(), up) == closelist.end()) {
                    if (curr_node->G + 10 < up->G) {
                        up->prev = curr_node;
                        up->G = curr_node->G + 10;
                        up->F = up->G + up->H;
                        if (std::find(openlist.begin(), openlist.end(), up) == openlist.end()) {
                            openlist.push_back(up);
                        }
                    }
                }
            }
            if (has_next_y) {//存在下侧
                Node* down = &m_map[(curr_node->y + 1)*m_map_weight + curr_node->x];
                if (down->visiable && std::find(closelist.begin(), closelist.end(), down) == closelist.end()) {
                    if (curr_node->G + 10 < down->G) {
                        down->prev = curr_node;
                        down->G = curr_node->G + 10;
                        down->F = down->G + down->H;
                        if (std::find(openlist.begin(), openlist.end(), down) == openlist.end()) {
                            openlist.push_back(down);
                        }
                    }
                }
            }
            openlist.erase(std::find(openlist.begin(), openlist.end(), curr_node));//把当前节点从openlist中拿出来
    
            closelist.push_back(curr_node);//放到closelist中
        }
    
        return false;//没找到路径,直接返回
    }
    

      剩下的代码就是用来可视化的:

    void A_Star::print_path() {//从终点往回追溯路径并打印
        Node* curr_node = m_end;
        while (curr_node != nullptr) {
            std::cout << "(" << curr_node->x << ", " << curr_node->y << ")" << std::endl;
            curr_node = curr_node->prev;
        }
    }
    
    void A_Star::print_map() {
        char* map = new char[m_map_height * m_map_weight];//还原输入地图的标记点
        for (int i = 0; i < m_map_height; ++i) {
            for (int j = 0; j < m_map_weight; ++j) {
                if (m_map[i*m_map_weight + j].visiable)
                    map[i*m_map_weight + j] = 'O';
                else
                    map[i*m_map_weight + j] = 'X';
            }
        }
    
        map[m_start->y*m_map_weight + m_start->x] = 'S';
        map[m_end->y*m_map_weight + m_end->x] = 'E';
    
        Node* curr_node = m_end->prev;//将除了起点和终点的路径标为'*'
        while (curr_node->prev != nullptr) {
            map[curr_node->y * m_map_weight + curr_node->x] = '*';
            curr_node = curr_node->prev;
        }
    
        for (int i = 0; i < m_map_height; ++i) {//打印输出到控制台
            for (int j = 0; j < m_map_weight; ++j) {
                std::cout << map[i*m_map_weight + j] << " ";
            }
            std::cout << std::endl;
        }
    
        delete[] map;
    }
    

    测试

      按照上面一开始放出的代码,输出:

    O O O O * O O O
    O O O * X * O O
    O O S O X O E O
    O O O O X O O O
    O O O O O O O O
    O O O O O O O O
    

      和图中基本一致,还可以试试别的:

    std::vector<std::vector<char>> map = {
        {'O', 'X','X','X','X','O','O','O'},
        {'O', 'X','O','O','X','O','O','O'},
        {'O', 'O','S','O','X','O','E','X'},
        {'O', 'O','O','O','X','X','X','O'},
        {'O', 'O','O','X','O','X','O','O'},
        {'O', 'O','O','O','O','O','O','O'}
    };
    
    outout:
    O X X X X O O O
    O X O O X O O O
    O O S O X O E X
    O O O * X X X *
    O O O X * X * O
    O O O O O * O O
    

      这个略有不同,我们代码实现的是斜向完全无阻塞,可视化网站实现的是单边遮挡无阻塞,双边遮挡阻塞,因此略有不同,不过研究明白算法,更改还是很容易的。

    脑洞

      最基础的A*寻路算法就这样实现了,可以看出算法的灵活性,在很多地方都可以加以定制。
      例如,我们需要移动的角色有“闪烁技能”,能瞬移一面墙的距离,那么我们的已知范围就可以从周围8格扩大到24格,同时为了防止人物肆意妄为的“闪烁”,可以针对H变量动手,使得闪烁的代价增大,致使F变量增大,在openlist寻找最小F的过程中,就不会优先选择闪烁。
      除此之外,前面提到的不同路径的不同花销也是可以做一些小动作,使得AI能根据地形选择不同的移动方式。
      期待早日能应用到实践当中!

    引用

    A*算法理解及代码实现 思路参考自这里,python代码以及pygame可视化
    PathFinding.js一个JS库的可视化演示,里面还包括很多其他寻路算法的展示

    相关文章

      网友评论

        本文标题:寻路算法A-Star(A*)的C++实现

        本文链接:https://www.haomeiwen.com/subject/lzidqctx.html