A*算法的一个C#实现
最近正在读云风的《游戏之旅》,看着看着就读到了A*寻路算法,虽然以前没有接触过,但总觉得好奇。于是从网上找了一些资料便开始研究。
当然,主要参考的算法文档是“http://www.vckbase.com/document/viewdoc/?id=1422”不过这里并没有给出实际的源代码。而搜了一下A*算法的代码,大都是ActionScript的源码。毕竟用Flash做一个Demo会方便很多。不过既然都打开了VisualStudio,那么就用C#写一个吧。
A*算法最主要的是对路径的评分函数。而实际应用时,这个函数的设计会产生不同的结果。从上面的文档中我们可以很容易地了解到评分F的公式:
F = H + G
当然根据文中提到的简便方法,我们可以对H和G的计算写出下面的代码。
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值各是多少,这个点的坐标以及到达这个点的上一个点的坐标。
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
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*算法:
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]