寻路算法之A Star

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_37245458/article/details/96854381

A*算法是最好优先算法的一种,是解决最短路径的有效方法。只要理解A*的工作原理,算法就不难实现。

1. A* 的工作原理

    什么是寻路?寻路就是从有限的点中找出一条从起点到终点的路径。但是,寻路的过程中我们不知道下一个节点是什么,有可能下一个节点是一个墙,也可能是一个陷阱等等不能经过的点。所以我们需要不断的试探,直到我们从起点正确的走到了终点,这个试探的过程就结束了,如果四周都是不能经过的点封闭的,这个试探过程也要结束。

    我们说了,A*是找出最短路径的算法,那么我们就需要在寻路的过程中考虑继续下一步的消耗,每次都是向消耗最少的点走,直到走到终点。所以A* = 试探+权重。

    如图,左边的蓝色矩形表示起点,右边的蓝色矩形表示终点,紫色矩形表示墙,蓝色圆点表示试探路线上的路点,绿色圆点表示可选路点,红色圆点表示最优路线上的路点。我们从图中可以看出根据算法的设置,寻路会有一定的试探过程,但是保证每一步最优路线都是正确的,虽然可能有其他的走法(根据算法的设置不同,产生的最优路线有可能不同)。

    我们要做的关键是找出每个点的权重,然后尽可能的绕过墙,找出最佳路线。那么这些点的权重是什么?

    首先每个点到终点都有一个距离,根据曼哈顿算法,我们可以知道不能走斜线地情况下这两个点的距离是多少,我们假设这个值是H。然后每个点周围有8个点(4条边+4个顶点),这个点到周围的点又产生了一个距离,我们假设这段距离为G。现在这个点的权重F就变成了G+H,而这个权重是我们对点到终点的估计值,这个估计值如果接近点到终点的实际值,得到的路线越准确并且效率更高。

2. A*寻路的过程

    首先我们准备一个关闭列表,表示我们不再检测的点,一开始这个列表中只有墙。

    然后准备一个队列,描述进行下一步的点,并把起点放入队列中。

    1. 从队列中取出权重最小的点(选择点),并找出它周围的8个点。对于刚开始,我们权重最小的点就是起点。

    2. 对它周围的8个点计算出它们的权重F=G+H,让这8个点指向选择点,并放入到队列中。

    3. 把取出的点放入进关闭列表,这个已经取出的点就不在检测了。

    4. 继续回到第1步重复,直到队列为空时或者走到终点后结束。

    PS:如果遇到了墙怎么办?按照上面的4步已经足够我们找出一个最简单的最优路径了。

    如图,但是我们发现,在坐标(2,1)的位置找到的路径直接穿过了两个墙,如果这个墙是一个直线方向上的我们这样做还说的过去,但是现在这样子貌似会直接让我们直接撞在墙上。所以遇到墙时,需要有一个特殊的处理,让我们绕过这个墙。而这一个操作应该在我们找出周围的8个点之前进行,那就是找出当前选择点的上下左右四个方向是否有墙,因为斜方向上就算有墙也可以直接通过所以没有必要找斜方向的墙。找到这四个方向上的墙后,我们需要对这些墙周围的点根据选择点的斜方向做一次清理,因为这些墙上下左右方向上不会存在选择点斜方向产生的点。这个操作决定了寻路的路径是否会在遇到障碍时斜着穿过去。

3.A* Unity源码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PointStruct {
    public PointStruct(int x, int y) {
        this._x = x;
        this._y = y;
    }

    private int _x;
    private int _y;

    public int x {
        get { return _x; }
    }

    public int y {
        get { return _y; }
    }

    private int _weight;

    public int Weight {
        get { return _weight; }
    }

    public void CalculateWeight() {
        this._weight = this.G + this.H;
    }

    public int G = 0;
    public int H = 0;
    
    public int CalculateConsumeH(int x, int y) {
        return Mathf.Abs(this._x - x) + Mathf.Abs(this._y - y);
    }
    
    public PointStruct parentPoint;
    public int tag;         // 0, 1表示点为墙壁, 2表示点在开放列表, 3表示点在关闭列表
}

首先需要准备这样一个结构,它保存了一个点的信息。parentPoint实现了每个点的链式存储。tag则表示这个点的状态。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AStarAlgorithm : MonoBehaviour {
    public Transform aStarShownTransform;
    
    public int SquareCount = 7;             // 绘制 n*n 的方格
    public Color SquareColor = Color.black;

    private List<PointStruct> openList = new List<PointStruct>();
    private List<PointStruct> closeList = new List<PointStruct>();

    [System.Serializable]
    public struct Point {
        public int x;
        public int y;
    }
    public Point[] wallPoints;
    public Color WallColor;

    public Point startPoint;
    public Color StartPointColor;

    public Point endPoint;
    public Color EndPointColor;

    public PointStruct startPointStruct;
    public PointStruct[] points;

    public Color BestPointColor;
    public Color BestLocationColor;

    public Color ForecastLocationColor;

    private void Start() {
        if (aStarShownTransform == null) {
            throw new Exception("Transform is NULL.");
        }
    }

    private void DrawSquare(Vector3 center, float edge) {
        Vector3 topLeft = new Vector3(center.x - edge / 2, center.y - edge / 2);
        Vector3 topRight = new Vector3(center.x + edge / 2, center.y - edge / 2);
        Vector3 bottomLeft = new Vector3(center.x - edge / 2, center.y + edge / 2);
        Vector3 bottomRight = new Vector3(center.x + edge / 2, center.y + edge / 2);

        Gizmos.DrawLine(topLeft, topRight);
        Gizmos.DrawLine(topLeft, bottomLeft);
        Gizmos.DrawLine(topRight, bottomRight);
        Gizmos.DrawLine(bottomLeft, bottomRight);
    }

    // 绘制Map
    private void DrawMap() {
        Gizmos.color = SquareColor;

        int row = SquareCount;
        int col = SquareCount;
        int index = 0;
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                points[index] = new PointStruct(i, j);
                points[index].tag = 0;
                DrawSquare(new Vector3(i, j), 1);
                index++;
            }
        }
    }

    // 绘制障碍点
    private void DrawWall() {
        if (wallPoints == null) {
            Debug.LogWarning("wall is null");
            return;
        }

        Gizmos.color = WallColor;
        for (int i = 0; i < wallPoints.Length; i++) {
            foreach (var v in points) {
                if (v.x == wallPoints[i].x && v.y == wallPoints[i].y) {
                    v.tag = 1;
                    closeList.Add(v);
                    Gizmos.DrawCube(new Vector3(wallPoints[i].x, wallPoints[i].y), new Vector3(1, 1, 0));
                }
            }
        }
    }

    // 绘制起点和终点
    private void DrawStart() {
        Gizmos.color = StartPointColor;
        Gizmos.DrawCube(new Vector3(startPoint.x, startPoint.y), new Vector3(1, 1, 0));

        foreach (var v in points) {
            if (v.x == startPoint.x && v.y == startPoint.y) {
                v.tag = 2;
                startPointStruct = v;
                startPointStruct.parentPoint = null;
                openList.Add(v);
                break;
            }
        }
    }

    private void DrawEnd() {
        Gizmos.color = EndPointColor;
        Gizmos.DrawCube(new Vector3(endPoint.x, endPoint.y), new Vector3(1, 1, 0));
    }

    // 绘制预测路径
    private void DrawForecastLocation() {
        // 所有试探的部分
        foreach (var v in closeList) {
            if (v.x == startPoint.x && v.y == startPoint.y) continue;
            if (v.tag == 1) continue;
            Gizmos.color = ForecastLocationColor;
            Gizmos.DrawSphere(new Vector3(v.x, v.y), 0.2f);
        }
    }

    // 绘制最优路径
    private void DrawBestPoint(PointStruct best) {
        Gizmos.color = BestPointColor;
        Gizmos.DrawSphere(new Vector3(best.x, best.y), 0.2f);
        Gizmos.color = Color.cyan;
        Gizmos.DrawLine(new Vector3(best.x, best.y), new Vector3(best.parentPoint.x, best.parentPoint.y));
    }

    private void DrawBestLocation(PointStruct best) {
        while (best.parentPoint != null) {
            if (best.x != endPoint.x || best.y != endPoint.y) {
                Gizmos.color = BestLocationColor;
                Gizmos.DrawSphere(new Vector3(best.x, best.y), 0.2f);
            } else {
                Gizmos.color = BestLocationColor;
                Gizmos.DrawSphere(new Vector3(best.x, best.y), 0.2f);
            }
            best = best.parentPoint;
        }

        if (best.parentPoint == null) {
            Gizmos.color = BestLocationColor;
            Gizmos.DrawSphere(new Vector3(best.x, best.y), 0.2f);
        }
    }

    // 检测周围的墙
    private List<PointStruct> DetectWalls(PointStruct p) {
        List<PointStruct> psArray = new List<PointStruct>();
        for (int i = -1; i < 2; i += 2) {
            int x = p.x + i;
            PointStruct temp1 = new PointStruct(x, p.y);
            int y = p.y + i;
            PointStruct temp2 = new PointStruct(p.x, y);
            
            foreach (var v in points) {
                if (v.x == temp1.x && v.y == temp1.y) temp1 = v;
                else if (v.y == temp2.y && v.x == temp2.x) temp2 = v;
            }

            if (temp1.tag == 1) psArray.Add(temp1);
            if (temp2.tag == 1) psArray.Add(temp2);
        }
        return psArray;
    }

    // 打印开放列表
    private void PrintOpenList() {
        foreach (var v in openList) {
            Debug.Log($"{{{v.x}:{v.y}}}, Weight={v.Weight} G={v.G} H={v.H}");
        }
    }

    // 计算最优点
    private PointStruct CalculateBestPoint(PointStruct p, List<PointStruct> detectWalls) {
        PointStruct best = p;
        foreach (var v in points) {
            int deltaX = Mathf.Abs(v.x - p.x);
            int deltaY = Mathf.Abs(v.y - p.y);
            float sqrt = Mathf.Sqrt(deltaX * deltaX + deltaY * deltaY);

            if ((deltaX == 1 || deltaY == 1) && (int)sqrt == 1 && v.tag == 0) {
                if (openList.Contains(v)) continue;
                if (closeList.Contains(v)) continue;

                bool enable = true;
                if (detectWalls.Count > 0) {
                    foreach (var wall in detectWalls) {
                        if ((wall.x == v.x && Mathf.Abs(wall.y - v.y) == 1)
                            || (wall.y == v.y && Mathf.Abs(wall.x - v.x) == 1)) {
                            enable = false;
                        }
                    }
                }
                if (!enable) continue;

                v.G = (int)(sqrt * 10);
                v.H = v.CalculateConsumeH(endPoint.x, endPoint.y);
                v.CalculateWeight();
                v.parentPoint = p;
                v.tag = 2;
                openList.Add(v);

                DrawBestPoint(v);
                if (v.x == endPoint.x && v.y == endPoint.y) { best = v; break; }
            } else if (v.tag == 2) {
                int sumG1 = p.parentPoint.G + p.G + v.G;
                int sumG2 = p.parentPoint.G + v.G;
                if (sumG1 > sumG2) {
                    continue;
                }
            }
        }
        return best;
    }

    private void SearchAllEnabledPoints(PointStruct p) {
        if (openList.Contains(p)) {
            openList.Remove(p);
            p.tag = 3;
            closeList.Add(p);
            DrawForecastLocation();
        }

        // 斜穿过墙
        // List<PointStruct> detectWalls = new List<PointStruct>();
        List<PointStruct> detectWalls = DetectWalls(p);
        PointStruct best = CalculateBestPoint(p, detectWalls);

        // 最优路径
        if (best.x == endPoint.x && best.y == endPoint.y) {
            DrawBestLocation(best);
            return;
        }

        // 递归回调
        if (openList.Count > 0) BFS(openList[0]);
    }

    private void BFS(PointStruct p) {
        PointStruct temp = p;
        for (int i = 0; i < openList.Count; i++) {
            if (temp.Weight > openList[i].Weight) {
                temp = openList[i];
            } else if (temp.Weight == 0) {
                temp = openList[i];
            }
        }
        
        SearchAllEnabledPoints(temp);
    }

    private void OnDrawGizmos() {
        if (aStarShownTransform == null) return;

        // restart per draw gizmos
        Matrix4x4 defaultMatrix = Gizmos.matrix;
        Gizmos.matrix = aStarShownTransform.localToWorldMatrix;

        Color defaultColor = Gizmos.color;
        

        openList = new List<PointStruct>();
        closeList = new List<PointStruct>();
        points = new PointStruct[SquareCount * SquareCount];
        
        DrawMap();
        DrawWall();
        DrawStart();
        DrawEnd();

        BFS(openList[0]);
        
        DrawMap();

        Gizmos.color = defaultColor;
        Gizmos.matrix = defaultMatrix;
    }
}

这里我用List来实现存储找到的点,是为了找出最小权重的点时方便。

猜你喜欢

转载自blog.csdn.net/qq_37245458/article/details/96854381