萌新的数据结构与算法3 A*算法

简介

Astar是一种深度优先启发式寻路算法,广泛运用在游戏领域。提起Astar不得不提一下迪杰特斯拉算法,它是一种广度优先启发式寻路算法,俗称阉割版的Astar。因为大部分情况下Astar的效率都比迪杰特斯拉高,我们只需要稍作了解就可以。

Astar的原理,有一篇博客写的很详细,萌新推荐一下https://www.cnblogs.com/zhoug2020/p/3468167.html

代码

1.先定义一个地图上的节点

public class Node
{
    public int x { get; }
    public int y { get; }
    public int g;
    public int h;
    public int f;
    public bool walkable;//该节点是否可移动
    public Node parent;
    public Node(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
    /// <summary>
    /// 清空节点上的消耗计算数据
    /// </summary>
    public void Clear()
    {
        g = 0;
        h = 0;
        f = 0;
        parent = null;
    }
}

2.获取一个节点周围节点的方法

这里是4方向战棋作为例子,如果8方向的话,4个顶点消耗应该高一些。
4方向战棋4个方向消耗都是1。

/// <summary>
/// 获取周围节点
/// </summary>
/// <param name="neighbourNode"></param>
/// <param name="startPoint"></param>
public void GetNeighbour(List<Node> neighbourNode, Node startPoint)
{
    if (startPoint.x - 1 >= 0)
    {
        neighbourNode.Add(nodes[startPoint.x - 1, startPoint.y]);
    }
    if (startPoint.x + 1 < length)
    {
        neighbourNode.Add(nodes[startPoint.x + 1, startPoint.y]);
    }
    if (startPoint.y - 1 >= 0)
    {
        neighbourNode.Add(nodes[startPoint.x, startPoint.y - 1]);
    }
    if (startPoint.y + 1 < width)
    {
        neighbourNode.Add(nodes[startPoint.x, startPoint.y + 1]);
    }
}

3.预估h值 曼哈顿算法

/// <summary>
/// 预估两个点间的消耗 曼哈顿算法
/// </summary>
/// <param name="startPoint"></param>
/// <param name="endPoint"></param>
/// <returns></returns>
public int GetH(Node startPoint, Node endPoint)
{
    int x = startPoint.x >= endPoint.x ? startPoint.x - endPoint.x : endPoint.x - startPoint.x;
    int y = startPoint.y >= endPoint.y ? startPoint.y - endPoint.y : endPoint.y - startPoint.y;
    return x + y;
}

4.每次从开放列表取新节点计算之前 排序

因为数据少,所以用冒泡排序会更快一些

/// <summary>
/// 按F值冒泡排序
/// </summary>
/// <param name="list"></param>
public void SortListByF(List<Node> list)
{
    if (list == null || list.Count == 0)
    {
        return;
    }
    for (int i = 0; i < list.Count - 1; i++)
    {
        for (int j = 0; j < list.Count - 1 - i; j++)
        {
            if (list[j].f > list[j + 1].f)
            {
                Node temp = list[j];
                list[j] = list[j + 1];
                list[j + 1] = temp;
            }
        }
    }
}

5.核心的寻路算法

/// <summary>
/// 寻路
/// </summary>
/// <param name="startPoint"></param>
/// <param name="endPoint"></param>
/// <returns></returns>
public List<Node> FindPath(Vector3Int startPoint, Vector3Int endPoint)
{
    List<Node> openList = new List<Node>();//开放列表
    List<Node> closeList = new List<Node>();//关闭列表
    List<Node> neighbourNodes = new List<Node>();//周围4个节点
    Node startNode = nodes[startPoint.x, startPoint.y];//起始点
    Node endNode = nodes[endPoint.x, endPoint.y];//目标点
    if (startNode == endNode)
    {
        return GetPath(startNode);
    }
    openList.Add(startNode);
    while (openList.Count > 0)
    {
        SortListByF(openList);//重新排序
        startNode = openList[0];//获取当前最小F值的点
        if (startNode == endNode) {
            Debug.Log("get path");
            return GetPath(startNode);
        }
        neighbourNodes.Clear();//清空
        GetNeighbour(neighbourNodes, startNode); //获取周边4个节点
        foreach (Node n in neighbourNodes)
        {
            //已经在关闭列表 本次不进行处理
            if (closeList.Contains(n))
            {
                continue;
            }
            //如果该点不可移动
            if (!n.walkable)
            {
                closeList.Add(n);
                continue;
            }
            //消耗计算
            int g = startNode.g + 1;
            int h = GetH(n, endNode);
            int f = g + h;
            //检测是否已经探索过
            if (openList.Contains(n))
            {
                //存在更优路线
                if (n.g > g)
                {
                    n.g = g;
                    n.f = g + n.h;
                    n.parent = startNode;
                }
            }
            else
            {
                n.g = g;
                n.h = h;
                n.f = f;
                n.parent = startNode;
                openList.Add(n);
            }
        }
        openList.Remove(startNode);
        closeList.Add(startNode);//将该点加入关闭列表
    }
    Debug.Log("no path");
    return null;
}

6.储存路径

寻路方法结束时,如果存在路径的话,路径保存在节点node的链表里,我们还需要一个方法取出路径。
另外,取完路径一定记得清空节点缓存的fgh值,不然下次调用数据发生错乱可能会死机233

/// <summary>
/// 得到路径
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public List<Node> GetPath(Node path)
{
    List<Node> pathList = new List<Node>();
    if (path != null)
    {
        pathList.Add(path);
    }
    if (path.parent != null)
    {
        while (path.parent != null)
        {
            pathList.Add(path.parent);
            path = path.parent;
        }
    }
    pathList.Reverse();//逆向排序 这里用栈去储存路径好了,栈结构刚好逆向输出,用list反而麻烦了,这个优化就留给读者好了
    NodesClear(pathList);//清空计算过程中缓存的数据
    return pathList;
}
/// <summary>
/// 清理节点上的数据
/// </summary>
/// <param name="paths"></param>
public void NodesClear(List<Node> paths)
{
    foreach (var n in paths)
    {
        n.Clear();
    }
}

7.A*的拓展

战棋游戏中,角色的移动范围搜索就是Astar的一个变种。
有大佬和我讨论说,如果角色移动力是3,就创建一个7*7的范围,然后去除掉曼哈顿值大于3的节点,就是移动范围。这样时间复杂度O(n)就能搞定。
实际上这么考虑不全面,角色有可能被围在一个四面是墙的空间,即便墙外的曼哈顿值小于等于3也没有办法走过去。
所以,移动范围还是考虑用Astar去搜索,攻击范围是穿透型的,直接用曼哈顿值计算就可以。
下面是代码示例

/// <summary>
/// 寻找移动范围
/// </summary>
/// <param name="startPoint"></param>
/// <param name="movePower"></param>
/// <returns></returns>
public List<Node> FindMove(Vector3Int startPoint, int movePower)
{
    List<Node> openList = new List<Node>();//开放列表
    List<Node> closeList = new List<Node>();//关闭列表
    List<Node> neighbourNodes = new List<Node>();//周围4个节点
    List<Node> moveRange = new List<Node>();
    Node startNode = nodes[startPoint.x, startPoint.y];//初始节点
    openList.Add(startNode);
    moveRange.Add(startNode);
    do
    {
        startNode = openList[0];//取g值最小的节点作为本次探索节点
        neighbourNodes.Clear();//清空
        GetNeighbour(neighbourNodes, startNode); //获取周边4个节点
        foreach (Node n in neighbourNodes)
        {
            //已经在障碍物列表 本次不进行处理
            if (closeList.Contains(n))
            {
                continue;
            }
            SrpgTile tile = terrain.GetTile<SrpgTile>(new Vector3Int(n.x, n.y, 0));
            //如果该点不可移动
            if (!tile.walkable || n.unit != null || n.obj != null)
            {
                closeList.Add(n);
                continue;
            }
            int g = startNode.g + 1;
            //检测是否已经探索过
            if (openList.Contains(n))
            {
                if (n.g > g)
                {
                    n.g = g;
                    n.parent = startNode;
                }
                continue;
            }
            n.g = g;
            n.parent = startNode;
            if (n.g <= movePower)
            {
                if (!moveRange.Contains(n))
                {
                    moveRange.Add(n);
                }
            }
            if (n.g < movePower)
            {
                openList.Add(n);
            }
        }
        openList.Remove(startNode);//探索完毕 从开放列表中移除本次探索点
        closeList.Add(startNode);//将该点加入关闭列表
    } while (openList.Count > 0);
    NodesClear(moveRange);
    return moveRange;
}

8.优化

Astar寻路 起点与终点距离太远的时候,Astar的计算量会非常大,可以考虑用四叉树把地图分割一下,结合Astar寻路
四叉树的介绍链接https://blog.csdn.net/jxw167/article/details/81869949

9.效果

完美收工
a*寻路

猜你喜欢

转载自blog.csdn.net/qq_29799917/article/details/89737934