A*算法的一个C#实现

最近正在读云风的《游戏之旅》,看着看着就读到了A*寻路算法,虽然以前没有接触过,但总觉得好奇。于是从网上找了一些资料便开始研究。

当然,主要参考的算法文档是“http://www.vckbase.com/document/viewdoc/?id=1422”不过这里并没有给出实际的源代码。而搜了一下A*算法的代码,大都是ActionScript的源码。毕竟用Flash做一个Demo会方便很多。不过既然都打开了VisualStudio,那么就用C#写一个吧。

A*算法最主要的是对路径的评分函数。而实际应用时,这个函数的设计会产生不同的结果。从上面的文档中我们可以很容易地了解到评分F的公式:

         F = H + G

当然根据文中提到的简便方法,我们可以对HG的计算写出下面的代码。

1private int G(int parent)
2{
3    int d = 10;
4    return d + parent;
5}

6private int H(int x, int y, Point end)
7{
8    return (Math.Abs(x - end.X) + Math.Abs(y - end.Y)) * 10;
9}

为了进行寻路的计算,我们还需要一个类来保存针对地图上某些点遍历信息的记录,比如这个点的F、G、H值各是多少,这个点的坐标以及到达这个点的上一个点的坐标。

 1class PathNode : IComparable<PathNode>
 2{
 3    public int G;
 4    public int H;
 5    public int F {
 6        get{
 7            return G + H;
 8        }

 9    }

10
11    public PathNode Parent;
12    public Point Position;
13
14    public PathNode(Point pos)
15    {
16        this.Position = pos;
17        this.Parent = null;
18        this.G = 0;
19        this.H = 0;
20    }

21
22    public override string ToString()
23    {
24        return Position.ToString();
25    }

26
27    IComparable Members
33}

PathNode这个类实现了IComparable接口,目的是为了对PathNode列表进行排序。还记得上面提到的文章中的一句话吗“寻找开启列表中F值最低的格子。我们称它为当前格。”没错,这就是为这个条件做的准备。对于寻找F值最低的“格子”,把开启列表一排序就OK了。
在实现实际的算法时,还需要准备3个容器对象:

private List<PathNode> unLockList = new List<PathNode>();
private Dictionary<string, PathNode> lockList = new Dictionary<string, PathNode>();
private List<PathNode> path = new List<PathNode>();

前两个是算法中提到的“开启列表”和“关闭列表”,最后一个是找到的最终路径。
最后来实现A*算法:

 1public List<PathNode> FindPath()
 2{
 3    unLockList.Clear();
 4    lockList.Clear();
 5    path.Clear();
 6    doFindPath();
 7    path.Reverse();
 8    return path;
 9}

10
11private void doFindPath()
12{
13    PathNode start = new PathNode(Start);
14    PathNode cur = start;
15    while (true)
16    {
17        if(!lockList.ContainsKey(cur.ToString()))
18            lockList.Add(cur.ToString(), cur);
19        for (int i = 0; i < delta.Length; i++)
20        {
21            Point newp = new Point(cur.Position.X + delta[i][0],
22                cur.Position.Y + delta[i][1]);
23            if (!canWalkOnIt(newp))
24                continue;
25            if (lockList.ContainsKey(newp.ToString()))
26                continue;
27            if (isPointInUnlockList(newp))
28            {
29                PathNode ulnode = __pn;
30                int newg = G(cur.G);
31                if (newg < ulnode.G)
32                {
33                    ulnode.Parent = cur;
34                    ulnode.G = newg;
35                }

36                continue;
37            }

38            PathNode newpn = new PathNode(newp);
39            newpn.G = G(cur.G);
40            newpn.H = H(newp.X, newp.Y, End);
41            newpn.Parent = cur;
42            unLockList.Add(newpn);
43        }

44        if (unLockList.Count == 0)
45            break;
46        unLockList.Sort();
47        cur = unLockList[0];
48        unLockList.Remove(cur);
49        
50        if (cur.Position.Equals(End))
51        {
52            while (cur != null)
53            {
54                path.Add(cur);
55                cur = cur.Parent;
56            }

57            break;
58        }

59    }

60}

61
62private PathNode __pn;
63
64private bool isPointInUnlockList(Point src)
65{
66    __pn = null;
67    foreach (PathNode item in unLockList)
68    {
69        if (item.Position.Equals(src))
70        {
71            __pn = item;
72            return true;
73        }

74
75    }

76    return false;
77}

78
79private bool canWalkOnIt(Point node)
80{
81    if (node.X < 0 || node.Y < 0)
82        return false;
83    if (node.X > Width - 1 || node.Y > Height - 1)
84        return false;
85    return GetNodeValue(node.X, node.Y) >= 0;
86}

没写什么注释,但思路就是上文中的“A*方法总结”。在此就不重新粘贴一遍了。在此需要多啰嗦两句的是,关闭列表用了一个Dictionary,其实关闭列表的目的就是查找下一个点是否在关闭列表当中,用Dictionary的ContainsKey方法比较容易,毕竟在Hashtable中找个Key总比在List中找个元素要快。
为了简化算法,这里只是遍历了当前点的上下左右4个相邻点。上文中介绍的是遍历8个点的情况,不过这也不是很复杂,只不过麻烦点在于G这个方法,需要判断一下是不是斜向走的。另外对4个相邻点的遍历,方法来源于之前看的一段AS代码,它用了一个偏移量数组来保存8个偏移量。而这里只是保存了4个偏移量。在实际的算法中,循环一下偏移量数组就很方便了(之前见过一个代码并没有用这个方法,而是复制了8短类似的函数调用代码,逻辑上就不如这个看的清晰)。delta数组如下:
private int[][] delta = new int[][]{
    new int[]{0,1},
    new int[]{0,-1},
    new int[]{1,0},
    new int[]{-1,0}
};

另一个需要注意的地方是如果4个偏移后的新点包含在开启列表中,那么应该是对开启列表中对应的PathNode的G值进行更新。如果是重新new一个新的PathNode,然后再加入开启列表,那么算法就会出现问题,有可能会陷入无限循环。
对于寻路的结果获取无非就是对PathNode链表中每个PathNode进行遍历,然后放到一个List中再Reverse一下。对于地图来说,这里用的是一个int数组,元素小于0的时候代表不能通过。而A*算法计算出的结果可能并不是最优的结果,不过其效率还是比较高的,原因在于有了评分函数的帮助可以遍历更少的节点。
最后,还是贴上整个Demo项目的文件吧,结构和代码看起来可能并不优雅。[AStarPathSearch.rar]


posted on 2009-07-22 15:01  blacktear  阅读(2846)  评论(6编辑  收藏  举报