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

11
private 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

62
private PathNode __pn;63

64
private 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

79
private 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]


浙公网安备 33010602011771号