A*寻路:源代码实现及使用

一、本篇目的

学习了A*寻路基础之后,发现源码下载不了了,自己实现一下,并做一些扩展记录以及测试。

本文所述完整源代码在这里下载

二、开发环境

  • VS2017 C#

三、简介

关于A*寻路,这篇文章说得很清楚:A* Pathfinding for Beginners

有人翻译成中文了,中文可以看这个:A*寻路入门

这篇文章说得很清楚,想调试源代码加深理解,但是文档中提供的源码怎么都下不来了,就萌生了自己编程实现的想法。

四、程序结构

程序结构大体如下图所示:

PathFinding +Terrain Terrain -MovePattern movePattern +FindPath() Terrain +Set() +SetXXX() MovePattern +MovablePositions() +NodeGValue() +NodeHValue() +NodeFValue() NormalMovePattern // 普通的移动样式 RiMovePattern // 马走日的移动样式

寻路类PathFinding需要针对不同的地形寻路,移动方式不同也会影响寻路结果。比如沙地比较难移动,那么可以设置地形的沙地位置基本移动力大一点,爬山和下山消耗移动力也会不一样的,所以地形在不同高度差的位置之间移动会影响寻路消耗的移动力,地形的这些性质在地形类Terrain来设置。

注意地形以左下角为原点,往右列号递增,往上行号递增。

移动时一般是移动到相邻的位置,但是有些项目(或者游戏,或者数学题目)是走别的形状的,比如马走日字。所以引入MovePattern类,并根据具体的移动样式不同,衍生出子类,比如源码考虑了普通移动样式类NormalMovePattern和马走日的移动样式类,要增加别的样式,继承MovePattern,实现自己的移动样式子类就可以了。

具体的实现代码注释很清晰了,看懂了简介中的文章,很容易能看懂源码和注释,本文不赘述。

源码AStartPathFinding项目,是寻路的库,编译后,结果是库文件AStarPathFinding.dll

后面描述如何使用这个库解决寻路的问题。

五、寻路库的使用

1、库添加到引用

新建一个c#项目,叫AStarPathFindingDemo,把AStarPathFinding.dll加入到引用中。如下图,添加引用,浏览选取这个库文件,保证这个库文件被勾选了,确定就行了。

添加引用

2、例子1

第1个先用简介中的图做例子:

例子1

文章答案给的是如下图:

行动力,斜线消耗14行动力来计算,是2步斜线,4步直线,共消耗68行动力。答案不是唯一的,比如从上方走也是一个答案,最终判定是消耗的行动力。

下面是使用的代码。

如图所示,地形是5行、7列的,先设置地形,代码如下:

/// <summary>
/// 测试5行,7列的如下图的寻路,左下角坐标是(0, 0)
/// X X X X X X X
/// X X X 1 X X X
/// X S X 1 X E X
/// X X X 1 X X X
/// X X X X X X X
/// 如上如,S为起点,E为终点,中间的1表示不可以立足也不能跃过(即墙)
/// </summary>
static private Terrain SetExam1Terrain()
{
    /* 先设置地形,没有高度差,高度全为0 */
    Terrain terrain = new Terrain();
    terrain.Set(7, 5);
    terrain.SetColumnMovableType(3, 1, 3, MovableType.NotStandonNotAcross);
    return terrain;
}

地形宽为7,高为5,不考虑每个位置的高度的话,高度全是0,不需要设置,所以使用terrain.Set(7, 5)来设置宽高,第4列的第2-4行是墙,既不可以立足,也不可以被跃过的地形,使用terrain.SetColumnMovableType(3, 1, 3, MovableType.NotStandonNotAcross)来设置,其中第一个参数3表示第4列(从0开始算,序号是3),第2给参数1表示第2行(从0开始算,序号是1),第3个参数3表示第4行,即设置第4列的行[1, 3]的可移动类型为MovableType.NotStandonNotAcross,即不可以立足,也不可以跃过。

接着就是寻路,代码如下:

static private void Exam1()
{
    /* 设置地形 */
    Terrain terrain = SetExam1Terrain();

    PathFinding pathFinding = new PathFinding();
    List<Node> bestPath = null;
    int action = 0;
    pathFinding.Terrain = terrain;
    if (pathFinding.FindPath(1, 2, 5, 2, out bestPath, out action))
    {
        /* 找到路径,打印 */
        ShowBestPath(bestPath, action);
    }
    else
    {
        /* 找不到路径 */
        Console.WriteLine("Not found path!");
    }
}

先设置好地形,然后传入给FindPath方法,这个方法前两个参数就是起始位置的坐标,即(1, 2),接着是终点的坐标(5, 2)。然后返回结果并打印出来。结果如下:

Found path:
(1, 2) -> (1, 3) -> (2, 4) -> (3, 4) -> (4, 4) -> (4, 3) -> (5, 2)
Action: 68

答案是6步,消耗行动力68。答案路径如下图;

和简介的文章答案不同,但是结果是对的,都是行动力68,2步斜线,4步直线

3、例子2

这个例子题目源于某小学生的课程中题目,如下图:

骑士走的日字,问骑士几步就能拿到宝物?

答案很明显是4步。假设马走日一步的行动力按照基本斜线来算14,那就是56的行动力。

程序如下:

设置地形:

/// <summary>
/// 测试日字步
/// 测试4行6列的,S点是骑士,E点是宝物,骑士最少几步拿到宝物,如下图
/// E X X X X X
/// X X X X X X
/// X X X X X X
/// X X X X X S
/// </summary>
static private Terrain SetExam2Terrain()
{
    /* 先设置地形,没有高度差,高度全为0 */
    Terrain terrain = new Terrain();
    terrain.Set(6, 4);
    return terrain;
}

地形就是6x4的,没什么特别。

寻路代码是:

static private void Exam2()
{
    /* 先设置地形,没有高度差,高度全为0 */
    Terrain terrain = SetExam2Terrain();

    PathFinding pathFinding = new PathFinding();
    List<Node> bestPath = null;
    int action = 0;
    pathFinding.Terrain = terrain;
    if (pathFinding.FindPath(5, 0, 0, 3, out bestPath, out action, MoveType.RiMove))
    {
        /* 找到路径,打印 */
        ShowBestPath(bestPath, action);
    }
    else
    {
        /* 找不到路径 */
        Console.WriteLine("Not found path!");
    }
}

和例子1一样,就是先设置地形,然后传入起点、终点坐标寻路,不同的是最后的一个参数传入了MoveType.RiMove,表示移动类型是日字步。

最后结果是:

Found path:
(5, 0) -> (3, 1) -> (2, 3) -> (1, 1) -> (0, 3)
Action: 56

路径画到图里和这个学生做题的答案有些不一样,但是很明显,结果也是对的。

最后来个比上面稍微复杂点的应用。

4、例子3

也是来自小学生的数学题,如下图:

骑士走的日字,骑士最少能用几步拿到两件宝物?

应该没有比8步更好的答案了。

地形和上面的有些不同,空白的位置是可以跃过去的,但是空白的位置是不可以立足的位置。比如图示的骑士起始的位置的左边几格都是空白,但是不影响骑士跃到标了1的位置,但是骑士却不可以跃到空白的位置去,那怕是日字的走法。

所以这个题目地形设置有些复杂,代码如下:

/// <summary>
/// 测试日字步
/// 测试9行10列的,S点是骑士,E点是宝物有2个,骑士最少几步拿到2个宝物,如下图:
/// 2表示不可以立足,但是可以跃过的位置
/// 2 2 2 2 2 2 2 2 X 2
/// 2 X X X 2 2 2 X 2 2
/// 2 X 2 X 2 2 2 2 X X
/// 2 X X E 2 X 2 2 X 2
/// 2 2 2 2 2 2 2 2 X 2
/// X X 2 2 X X X 2 X 2
/// 2 2 X 2 X 2 X 2 X 2
/// E 2 X 2 2 X X X X X
/// 2 2 2 2 2 2 2 2 S 2
/// </summary>
static private Terrain SetTest3Terrain()
{
    Terrain terrain = new Terrain();
    terrain.Set(10, 9);
    terrain.SetPointMovableType(0, 0, MovableType.NotStandonAcross);
    terrain.SetPointMovableType(0, 2, MovableType.NotStandonAcross);
    terrain.SetColumnMovableType(0, 4, 8, MovableType.NotStandonAcross);
    terrain.SetColumnMovableType(1, 0, 2, MovableType.NotStandonAcross);
    terrain.SetPointMovableType(1, 4, MovableType.NotStandonAcross);
    terrain.SetPointMovableType(1, 8, MovableType.NotStandonAcross);
    terrain.SetPointMovableType(2, 0, MovableType.NotStandonAcross);
    terrain.SetColumnMovableType(2, 3, 4, MovableType.NotStandonAcross);
    terrain.SetPointMovableType(2, 6, MovableType.NotStandonAcross);
    terrain.SetPointMovableType(2, 8, MovableType.NotStandonAcross);
    terrain.SetColumnMovableType(3, 0, 4, MovableType.NotStandonAcross);
    terrain.SetPointMovableType(3, 8, MovableType.NotStandonAcross);
    terrain.SetColumnMovableType(4, 0, 1, MovableType.NotStandonAcross);
    terrain.SetColumnMovableType(4, 4, 8, MovableType.NotStandonAcross);
    terrain.SetPointMovableType(5, 0, MovableType.NotStandonAcross);
    terrain.SetPointMovableType(5, 2, MovableType.NotStandonAcross);
    terrain.SetPointMovableType(5, 4, MovableType.NotStandonAcross);
    terrain.SetColumnMovableType(5, 6, 8, MovableType.NotStandonAcross);
    terrain.SetPointMovableType(6, 0, MovableType.NotStandonAcross);
    terrain.SetColumnMovableType(6, 4, 8, MovableType.NotStandonAcross);
    terrain.SetPointMovableType(7, 0, MovableType.NotStandonAcross);
    terrain.SetColumnMovableType(7, 2, 6, MovableType.NotStandonAcross);
    terrain.SetPointMovableType(7, 8, MovableType.NotStandonAcross);
    terrain.SetPointMovableType(8, 7, MovableType.NotStandonAcross);
    terrain.SetPointMovableType(9, 0, MovableType.NotStandonAcross);
    terrain.SetColumnMovableType(9, 2, 5, MovableType.NotStandonAcross);
    terrain.SetColumnMovableType(9, 7, 8, MovableType.NotStandonAcross);
    return terrain;
}

需要使用地形类的接口设置好很多空白地方(可以跃过,但是不可以立足的位置,类型是MovableType.NotStandonAcross

下面说寻路,寻路怎么做能找到最优的路径呢?

为了利于说明,给宝物编号,假设上面的宝物叫1号宝物,下面的宝物叫2号宝物。

寻路接口只支持一个起点,一个终点。那么做法是:

  • 假设先获取1号宝物,再获取2号宝物。

    即从起点到1号宝物寻路,再从1号宝物到达2号宝物寻路,把两次寻路的行动力求总和。

  • 再假设先获取2号宝物,再获取1号宝物。

    即“起点->2号宝物->1号宝物”,同样求出2次寻路的行动力总和。

  • 比较上面2种假设,行动力少的就是最优方案。

  • 注意,每种假设都寻路了两次,每次寻路都会造成地形力的评估值发生了改变,需要重新设置地形才可以进行第二次寻路,比如假设先取1号宝物,再取2号宝物的做法是:设置地形->寻路(起点,1号宝物)->重新设置地形->寻路(1号宝物, 2号宝物)->计算两次行动力总和。

代码如下:

static private void Exam3()
{
    /* 先设置地形,没有高度差,高度全为0 */
    Terrain terrain = SetTest3Terrain();
    /* 先拿上面的宝物,再拿下面的宝物,所用的行动力 */
    Console.WriteLine("假设先拿上面的宝物,再拿下面的宝物,寻路中...");
    PathFinding pathFinding = new PathFinding();
    List<Node> bestPath = null;
    int action1 = 0;
    pathFinding.Terrain = terrain;
    if (pathFinding.FindPath(8, 0, 3, 5, out bestPath, out action1, MoveType.RiMove))
    {
        /* 找到路径,打印 */
        Console.WriteLine("从起点到达上面宝物的寻路为:");
        ShowBestPath(bestPath, action1);
    }
    else
    {
        /* 找不到路径 */
        Console.WriteLine("Not found path!");
    }
    /* 接着再计算从上面宝物走到走到下面宝物的行动力 */
    /* 要先恢复地形,很重要!!!! */
    terrain = SetTest3Terrain();
    pathFinding = new PathFinding();
    bestPath = null;
    int action2 = 0;
    pathFinding.Terrain = terrain;
    if (pathFinding.FindPath(3, 5, 0, 1, out bestPath, out action2, MoveType.RiMove))
    {
        /* 找到路径,打印 */
        Console.WriteLine("从上面宝物到达下面宝物的寻路为:");
        ShowBestPath(bestPath, action2);
    }
    else
    {
        /* 找不到路径 */
        Console.WriteLine("Not found path!");
    }
    /* 总行动力 */
    int action_type1 = action1 + action2;
    Console.WriteLine("假设先拿上面的宝物,再拿下面的宝物,所用行动力是{0}", action_type1);

    /* 假设先取下面的宝物,再取上面的宝物 ,再做一遍*/
    Console.WriteLine();
    Console.WriteLine("假设先拿下面的宝物,再拿上面的宝物,寻路中...");
    terrain = SetTest3Terrain();
    /* 先拿上面的宝物,再拿下面的宝物,所用的行动力 */
    pathFinding = new PathFinding();
    bestPath = null;
    action1 = 0;
    pathFinding.Terrain = terrain;
    if (pathFinding.FindPath(8, 0, 0, 1, out bestPath, out action1, MoveType.RiMove))
    {
        /* 找到路径,打印 */
        Console.WriteLine("从起点到达下面宝物的寻路为:");
        ShowBestPath(bestPath, action1);
    }
    else
    {
        /* 找不到路径 */
        Console.WriteLine("Not found path!");
    }
    /* 接着再计算从上面宝物走到走到下面宝物的行动力 */
    /* 要先恢复地形 */
    terrain = SetTest3Terrain();
    pathFinding = new PathFinding();
    bestPath = null;
    action2 = 0;
    pathFinding.Terrain = terrain;
    if (pathFinding.FindPath(0, 1, 3, 5, out bestPath, out action2, MoveType.RiMove))
    {
        /* 找到路径,打印 */
        Console.WriteLine("从下面宝物到达上面宝物的寻路为:");
        ShowBestPath(bestPath, action2);
    }
    else
    {
        /* 找不到路径 */
        Console.WriteLine("Not found path!");
    }
    /* 总行动力 */
    int action_type2 = action1 + action2;
    Console.WriteLine("假设先拿下面的宝物,再拿上面的宝物,所用行动力是{0}", action_type2);
}

结果为:

假设先拿上面的宝物,再拿下面的宝物,寻路中...
从起点到达上面宝物的寻路为:

Found path:
(8, 0) -> (6, 1) -> (4, 2) -> (6, 3) -> (5, 5) -> (4, 3) -> (3, 5)
Action: 84
从上面宝物到达下面宝物的寻路为:

Found path:
(3, 5) -> (4, 3) -> (2, 2) -> (0, 1)
Action: 42
假设先拿上面的宝物,再拿下面的宝物,所用行动力是126

假设先拿下面的宝物,再拿上面的宝物,寻路中...
从起点到达下面宝物的寻路为:

Found path:
(8, 0) -> (6, 1) -> (4, 2) -> (2, 1) -> (1, 3) -> (0, 1)
Action: 70
从下面宝物到达上面宝物的寻路为:

Found path:
(0, 1) -> (2, 2) -> (4, 3) -> (3, 5)
Action: 42
假设先拿下面的宝物,再拿上面的宝物,所用行动力是112

最优的方案是和图示标的数字一样,先拿下面的宝物,再拿上面的宝物。

六、使用Excel画地形图

上面的例子,设置地形尤其麻烦,特别是不规则的图形,比如例子3,稍微不小心很容易就会出错。

excel上画则简单多了,地形每个节点就是excel的一个单元格。

比如例子1的图如下:

比如例子3的图如下:

例子3的图

excel绘制地形图,如例子所示,比较简单而且直观,只要能读取excel的图,转成Terrain对象就可以了。

读取Excel有开源库,可以用EPPlus,也可以用NPOI,经对比,我采用EPPlus

因为读取excel,转成Terrain,必须要做些约定,才可以按照约定转化,约定如下:

  • 地形全部按照长方形形状的,对于不规则的,可以按照最长边补齐成长方形,不存在的地形使用不可立足的节点填充

  • 所有地形的边框用实线,这样通过识别单元格的实线,来识别出来地形的长宽

  • 所有背景颜色要用RGB颜色,因为EPPlus识别RGB颜色比较容易。

    excel图颜色

    如上图,要使用标准色,如果标准色中没有的,就在其他颜色中配置RGB颜色,使用过一次之后,就会记录在最近使用的颜色中了。

    地形的ARGB颜色约定:

    起点:绿色 #FF00B050,起点的单元格写上文字"马"标识走日字型,普通的什么都不写

    终点:红色 #FFFF0000

    可立足地形:灰色 #FFE6E6E6

    不可立足可跃过地形:保留excel原来单元格的空白,即没有颜色

    不可立足不可跃过地形:深蓝色 #FF002060

  • 单元格用文字标注地形的基本行动力和高度。为了更容易绘制,更美观,如果高度是0,可以不标高度,如果基础行动力是普通的10,可以不标基础行动力。例子1的图,F5单元格的位置本来是可以不标的,因为基础行动力是10,高度是0,为了说明,标了一下,也不会有错。B: 10标识基础行动力是10,H : 0标识高度是0。(注意冒号必须使用英文字符,而且每一项要用换行隔开)

七、EPPlus读取excel地形图

关于EPPlus的了解,可以网上搜索。

我的VS2017,如果直接下载源码编译,会提示.net framework版本不对。

所以我是从NuGet中下载的。直接得到可用的各个版本的EPPlus.dll库。

和前述的添加AStarPathFinding.dll到引用一样的方法,把EPPlus.dll库添加到引用,程序增加一个读取Excel转换到Terrain的类,这个类提供方法,输入是excel文件名以及sheet序号,输出是Terrain对象。

颜色

不可立足不可跃过地形:深蓝色 #FF002060

  • 单元格用文字标注地形的基本行动力和高度。为了更容易绘制,更美观,如果高度是0,可以不标高度,如果基础行动力是普通的10,可以不标基础行动力。例子1的图,F5单元格的位置本来是可以不标的,因为基础行动力是10,高度是0,为了说明,标了一下,也不会有错。B: 10标识基础行动力是10,H : 0标识高度是0。(注意冒号必须使用英文字符,而且每一项要用换行隔开)

七、EPPlus读取excel地形图

关于EPPlus的了解,可以网上搜索。

我的VS2017,如果直接下载源码编译,会提示.net framework版本不对。

所以我是从NuGet中下载的。直接得到可用的各个版本的EPPlus.dll库。

和前述的添加AStarPathFinding.dll到引用一样的方法,把EPPlus.dll库添加到引用,程序增加一个读取Excel转换到Terrain的类,这个类提供方法,输入是excel文件名以及sheet序号,输出是Terrain对象。

最后使用例子4和例子5做测试,具体不再说明,参照源代码。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我会尝试回答你的问题。 首先,要使用Unity脚本实现A*寻路算法,需要在Unity中创建一个脚本,并将其添加到场景中的游戏对象上。 以下是实现A*寻路算法的Unity脚本示例代码: ```csharp using UnityEngine; using System.Collections; using System.Collections.Generic; public class AStarPathfinding : MonoBehaviour { public Transform seeker, target; //起点和终点 Grid grid; //寻路所需的网格 void Awake() { grid = GetComponent<Grid>(); } void Update() { FindPath(seeker.position, target.position); } void FindPath(Vector3 startPos, Vector3 targetPos) { Node startNode = grid.NodeFromWorldPoint(startPos); Node targetNode = grid.NodeFromWorldPoint(targetPos); List<Node> openSet = new List<Node>(); HashSet<Node> closedSet = new HashSet<Node>(); openSet.Add(startNode); while (openSet.Count > 0) { Node currentNode = openSet[0]; for (int i = 1; i < openSet.Count; i++) { if (openSet[i].fCost < currentNode.fCost || (openSet[i].fCost == currentNode.fCost && openSet[i].hCost < currentNode.hCost)) { currentNode = openSet[i]; } } openSet.Remove(currentNode); closedSet.Add(currentNode); if (currentNode == targetNode) { RetracePath(startNode, targetNode); return; } foreach (Node neighbour in grid.GetNeighbours(currentNode)) { if (!neighbour.walkable || closedSet.Contains(neighbour)) { continue; } int newMovementCostToNeighbour = currentNode.gCost + GetDistance(currentNode, neighbour); if (newMovementCostToNeighbour < neighbour.gCost || !openSet.Contains(neighbour)) { neighbour.gCost = newMovementCostToNeighbour; neighbour.hCost = GetDistance(neighbour, targetNode); neighbour.parent = currentNode; if (!openSet.Contains(neighbour)) { openSet.Add(neighbour); } } } } } void RetracePath(Node startNode, Node endNode) { List<Node> path = new List<Node>(); Node currentNode = endNode; while (currentNode != startNode) { path.Add(currentNode); currentNode = currentNode.parent; } path.Reverse(); grid.path = path; } int GetDistance(Node nodeA, Node nodeB) { int dstX = Mathf.Abs(nodeA.gridX - nodeB.gridX); int dstY = Mathf.Abs(nodeA.gridY - nodeB.gridY); if (dstX > dstY) { return 14 * dstY + 10 * (dstX - dstY); } return 14 * dstX + 10 * (dstY - dstX); } } ``` 该脚本中的A*寻路算法会在每次Update()函数调用时寻找从起点到终点的最短路径,并将其保存在网格的路径中。 实现A*寻路算法需要一个网格,该网格由一系列节点组成。每个节点包含了该节点在网格中的位置、该节点到起点的距离(gCost)、

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值