18.Finding Your Way Around: Pathfinding on a grid with obstacles
In the last post we saw about distances with horizontal and vertical movement, and how to move one object towards another using horizontal and vertical moves on an empty grid. But what if there are obstacles in the way? For that we need a pathfinding algorithm. This is not particularly mathematical, and it is a bit intricate, but it will come in handy for our next game.
在上一篇帖子里我们了解了关于水平和垂直运动的距离,以及如何使用水平和垂直移动来让一个物体在空格里朝着另一个物体移动。但是如果移动路径上有障碍物那又如何?为此我们需要一个路径寻找算法。这不是特殊的数学内容,而且有一点麻烦,但是在下一个游戏中将要使用它。
路标
How do you calculate all the shortest paths to a particular destination square on a grid? Your initial inclination might be to have, for each source square, a list of directions. So something like this (blue star is the source, red square is the destination, with each arrow showing which square to move to next):
在一个网格里你如何计算所有的到达某个特定目标方格的最短路径?你最初的想法可能是给每个源方格设置一个方向列表。于是就像这样(蓝星是源,红星是目标,每个箭头显示了下一步要向哪个方格移动):
But in fact, consider the next square on that route — its path will look like this:
但是事实上,考虑到路径上的下一个方格——它的路线将看起来像这样:
So actually, we don’t need a full list of directions for each source square, we just need one direction: to the next square. For a fixed destination, each square acts like a signpost, pointing the way to the next square. By following the signposts on each square, you can arrive at the destination. This means that for any given destination square, we need to form a complete grid of signposts, that will look something like this:
于是实际上,我们不需要给每个源方格设一个完全的方向列表,而仅需要一个方向:指向下一个方格。对于一个固定的目标,每个方格的作用像路标一样,指示通往下一个方格的方向。通过每个方格的路标引导,你便可以到达目的地。这意味着对于任意给定的目标方格,你需要形成一个完全的路标网格,如下图所示:
主要思想
So how do we build up this grid of signposts? You might think that we begin at the source square, but actually that doesn’t work so well: we’ve no idea which direction will be successful from the source, because we don’t know what walls might be in the way. Instead, we start at the destination, because from the squares exactly adjacent to the destination, we know which way to head:
于是怎样来创建这个路标网格呢?你可能会想到我们从源方格开始,但是实际上那样并不能很好地运行:我们并不知道从源方格开始往哪个方向移动会成功,因为我们不知道路径上有怎样的屏障。反之,我们从目标开始,因为从与目标完全相邻的方格开始,我们知道哪条道路去前进:
Then from the squares adjacent to those, we also know which way to head:
接着从与它们相邻的方格开始,我们也知道哪条道路去前进:
And so we spider outwards, searching all adjacent squares. At each square we see if the route we are currently tracing from the destination is shorter than the one which has been recorded. If the current route is shorter, continue, otherwise (if the old route is shorter) then give up. We will almost certainly reach situations where we have multiple ways in which we can head, for example the squares with the multiple smaller red arrows above. In these situations, we use the rule we saw inour last post to decide which way to head: the way that gives the shortest diagonal distance. In the picture above this is equal both ways, so we just make an arbitrary choice.
于是同样地我们十字交叉向外扩展,搜索所有相邻的方格。对于每个方格我们看看当前从目标追踪而来的路径是否比之前保存的要短。如果当前路径更短,则继续搜索,否则(如果之前的路径更短)便放弃。我们几乎确定地将会到达一些位置,那儿我们会面对多种不同的道路,比如上图中拥有几个红色小箭头的方格。在这些位置上,我们使用上一篇帖子里学习的规则来决定哪条道路去前进:拥有最短斜线距离的路径。在上图中两条路径是相等的,于是我们仅需要任选一条。
Show me the Code
展示代码
I’m not going to go into great depth on the code, but here it is if you want to see exactly what’s happening:
我不打算深入介绍代码,但是如果你想确切地知道发生了什么,你可以参考如下代码:
private void calculatePathFrom(byte[][] paths, int[][] dist, int fromX, int fromY, byte newDir, int newDist)
{
if (outOfBounds(fromX, fromY) || occupied[fromX][fromY])
return; //Outside world or blocked
byte prevDir = paths[fromX][fromY];
if (prevDir == STAY)
return; // At destination square again
int prevDist = dist[fromX][fromY];
if (prevDir != 0 && prevDist != 0 && prevDist < newDist)
return; //Already processed and already have shorter path
if (prevDir != 0 && prevDist > 1 && prevDist == newDist
&& (prevDir == newDir || distanceSq(toX - moveX(fromX, newDir), toY - moveY(fromY, newDir)) > distanceSq(toX - moveX(fromX, prevDir), toY - moveY(fromY, prevDir))))
return; //Already processed and already have shorter or equal path
paths[fromX][fromY] = newDir;
dist[fromX][fromY] = newDist;
calculatePathFrom(paths, dist, fromX - 1, fromY, RIGHT, newDist + 1);
calculatePathFrom(paths, dist, fromX + 1, fromY, LEFT, newDist + 1);
calculatePathFrom(paths, dist, fromX, fromY - 1, UP, newDist + 1);
calculatePathFrom(paths, dist, fromX, fromY + 1, DOWN, newDist + 1);
}
The code has four early exits:
该代码有四个早期地出口:
1. 我们希望寻找路径的方格被障碍物所占据,或者在网格之外。从这样的位置是不可能形成一条路径的。
2. 我们回溯到了目标方格(比如,在搜索相邻方格的路径时)——这是没有意义的,因为指向目的地的最短路径永远不会穿过目的地(只是停在那!)。
3. 我们已经找到了一条离该方格更短的路径,而最近找到的路径正好长于已经存在的路径。
4. 我们已经找到了一条离该方格相同长度的路径,然而之前的路径拥有更短的对角线距离(参照上一篇帖子),于是使用之前的路径。否则,如果路径相等而新路径有更短的对角线距离,我们继续搜索同时覆盖旧的路径。
Assuming none of the above are true, we replace the previous path with our new path, and spider outwards (last four lines) looking for other paths. You can seethe scenario in action — run the scenario and move your mouse over a square to see all the shortest paths to that square.
假设以上条件都不为真,我们便用新路径替换之前的路径,同时十字交叉向外扩展(最后四行代码)来寻找其他路径。你可以看看运行中的游戏剧本——执行游戏剧本并移动鼠标滑过一个方格去查看该方格的所有最短路径。