E D R S I H C RSS
ID
Password
Join
War is good for business. Invest your son. -- Seymour Chwast. 베트남 반전 포스터에서


Contents

1 머리말
1.1 Find a path or Take a step?
2 Introduction
2.1 The A* Algorithm
2.2 The Heuristic
2.3 Speed or Accuracy?
2.3.1 Computer speed
2.3.2 Number of units
2.3.3 Implementation ideas
3 Implementation Notes
3.1 Sketch
3.2 Source code
3.3 Performance tips
3.3.1 Bad heuristic
3.3.2 Slow priority queue
3.3.2.1 Heaps
3.3.2.2 Splay Trees
3.3.2.3 HOT Queues
3.4 Variations of A*
3.4.1 Beam search
3.4.2 Iterative deepening
3.4.3 Dynamic weighting
3.4.4 Bandwidth search
3.4.5 Bidirectional search
3.4.6 Hierarchical A*
3.4.7 Dynamic A*
4 Dealing with moving obstacles
4.1 Recalculating paths
4.2 Path splicing
4.3 Watching for map changes
4.4 Dealing with clashes
4.5 Predicting obstacle movement
4.6 Waiting for movement
4.7 Coordinated movement
5 Speed of precalculated paths vs. step-taking
5.1 Predictable behavior vs. potential speed
5.1.1 Load spikes
5.1.2 Waypoints
5.2 Path recalculation
5.2.1 Idle-time recalculation
5.3 Redundancy in path-finding
5.3.1 Networked games
5.3.2 Multiple units
5.4 Summary
6 Space used by precalculated paths
6.1 Locations vs. directions
6.2 Path compression
6.2.1 Location storage
6.2.2 Direction storage
6.3 Beacons
6.4 Limited path length
6.5 Summary
7 Walking traps in steppers
7.1 Predetermined places to avoid
7.2 Learning to avoid traps
8 AI Techniques
8.1 Neural Networks
8.2 Genetic Algorithms
9 References
10 Map representations
10.1 Flat
10.2 Hierarchal
10.3 Non-grid path-finding
10.3.1 Polygonal maps
10.3.2 Road maps
11 Long and short term goals
11.1 Unit Movement
11.2 Behavior flags or stacks
12 Heuristics
12.1 Manhattan distance
12.2 Diagonal distance
12.3 Straight line distance
12.4 Guessing path quality
12.5 Breaking ties
12.6 Searching for an area
13 Movement costs for path-finders
13.1 Altitude
13.1.1 Moving uphill
13.1.2 Moving up- or downhill
13.2 Terrain
13.2.1 Forests, mountains, and hills
13.2.2 Roads
13.2.3 Walls or other barriers
13.2.4 Sloped Land
13.3 Enemies and friendly units
13.4 Marked beacons
13.5 Fuel Consumption
14 User experience with shortest paths
14.1 Dumb Movement
14.2 Smart Movement
14.3 Multithreading
14.4 Multiple Units
15 Applications
15.1 Exploration
15.2 Spying
15.3 Road Building
15.4 City Building
16 소스 코드
16.1 path.h
16.2 path.cpp

1 머리말 #

In the first few months of 1996, I read about many approaches to unit movement and path calculation. At the time path-finding did not seem to be widely used in games, but as CPU cycles have increased and as game players have demanded better movement of units, path-finding algorithms have become more popular. I eventually decided to implement path-finding for my game with the A* algorithm. My game has a grid/tile map (as opposed to a continuous coordinate map), and I wanted to tell military units to walk from one spot to another. How could I find a good path? I want my units to take roads when possible, and avoid mountains and hills. (This is similar to the path-finding goals in the once-popular game, "Civilization".) I have been writing up some of my thoughts and findings in these pages.

1.1 Find a path or Take a step? #


When trying to write movement for game units, the biggest choice is probably between the path-finding and step-taking approaches. Path-Finders look at the map and find a path for your unit to take. After that, your unit takes each step prescribed by the path-finder. Step-takers look only for the next step and leave the remainder of the path to the goal for later. There are advantages and disadvantages to each approach, as you might expect. When two things are in widespread use, you should expect that neither is always better than the other.

2 Introduction #

I will be focusing on the A* Algorithm. A* is probably your best choice for path-finding, since it can be significantly faster than Dijkstra's algorithm or Best-First Search (BFS). It was developed in 1968 to combine heuristic methods (which use information about the problem to be solved to make decisions) and formal methods (which don't use problem-specific information, but can be formally analyzed). The overall structure is a graph searching algorithm. Unlike most graph searching algorithms, A* utilizes a heuristic function that estimates how close any spot on the map is to the goal. By using the heuristic, it can guide its search to look in the best direction first. If that fails, it can look at other paths. I won't go into the details of the algorithm; instead I'll first describe the behavior of A*, then concentrate on my analysis of using this algorithm for games. I recommend looking up A* in an AI textbook to learn how the algorithm works in detail.

2.1 The A* Algorithm #

Most path-finding algorithms are written for arbitrary graphs rather than grid-based games. We'd like to find something that can take advantage of the grid nature of the map. There are some things we consider common sense, but that algorithms don't understand. For example, we know something about directions: we know that in general, as two things get farther apart, it will take a longer to move from one to the other; and we know that there aren't any secret wormholes that let you teleport from one spot on the map to another. (Well, I assume there aren't any; if there are, it becomes very hard to find a good path because you don't know where to look first.)

Note: For most of this document, we will assume that A* is being used on a grid in a tile-based game. However, A* was designed for use with graphs, and it can be used in different ways in games -- see the section on polygonal obstacles for an example.

A* is like other graph-searching algorithms in that it can potentially search a huge area of the map. It is also like step-taking algorithms in that it starts out going straight for the goal. (This is the primary difference between A* and Dijkstra: A* has a heuristic function to guide its search towards the goal; Dijkstra has no such information, and searches in all directions.) Unlike the step-takers, however, A* can "backtrack" if going straight for the goal doesn't take you there. It does this as it's going by keeping track of possible grid spots that might lead to a good path. For example, if you were heading towards a town and came to a fork in the road, and decided to go left instead of right, you'd probably remember that if you didn't find the town, you could come back and try the other path. A* does the same thing.

So far, this sounds very much like Best-First Search. The main difference is that BFS will keep looking down one path as long as it thinks it's getting closer to the goal, until it gets to a dead-end. A* will go down several paths at once if they all look like they're getting closer to the goal. A* looks at the path that has the lowest "cost" from beginning to end, whereas BFS looks at the path that has the lowest cost from the current point to the end. BFS ignores the cost of the path taken so far, so it is possible for it to go off on a wild goose chase on some long expensive path that seems to be good. A* would look at other paths as soon as that path got expensive. In the next section we will look at the heuristic function, which is a guess of the cost from the current point to the end.

If you find the goal by going straight, then you don't really need to search the things in the backtrack list. That's why A* is pretty fast most of the time. It puts things on the list but it can throw them away if it finds the goal.

2.2 The Heuristic #

The heuristic function h(p) tells A* an estimate of the cost from the current position p to the goal. To find the best path, A* uses both the heuristic and g(p), the cost of the best path from the start to p. The sum of these two values is called the estimated cost, f(p).

It's important to choose a good heuristic function. A bad heuristic can really slow down A* or make it produce bad paths. If you want A* to give you "perfect" paths, the heuristic function should be an underestimate of the actual cost of getting from one spot to the goal. Be careful that you don't underestimate too much however. A low heuristic won't give A* much information, and it'll keep telling A* that there are better paths available, so it will take longer to give you a path.

An interesting note: If your heuristic is exact, then A* will never stray from the best path. Of course, it's hard to get an exact heuristic, but it's nice to know that A* will act perfectly when you give it perfect information. (Also note that in the sometimes common case where you have no obstacles, the heuristic can be perfect.) Another thing that may be interesting is that as your heuristic gets closer to being perfect, A* has to search less and less. This gives me confidence that it's using the heuristic well -- if it wasn't, then a perfect heuristic probably would lead to an inefficient search. If there aren't any obstacles or nasty terrain squares in the way, then the heuristic (which is usually the number of steps to the goal) is perfect, and A* will head straight towards the goal and not look anywhere else on the map. The way to think about this is that the increase in the f value is related to how much is searched. When the heuristic is perfect, f never changes. When the heuristic underestimates too much, f increases quickly. The better the heuristic, the less f increases, and the less of the map is searched.


Note: Technically, the A* algorithm should be called simply A if the heuristic is an underestimate. However, I will continue to call it A* because the implementation is the same.


You may not want "perfect" paths. You might want to get "reasonable" paths quickly instead of perfect paths take some time to find, especially if you are writing a real-time game. For this, there are several things you can do to speed up A*. First, you don't have to have an underestimating heuristic. If your heuristic function overestimates some of the time, it can find the goal quicker. Alternatively, you can adjust your "cost" function to more closely match your heuristic.

For example, if your game has two types of terrain, Flat and Mountain, and the movement costs are 1 for flat land and 3 for mountains, A* is going to search three times as far along flat land as it does along mountainous land. This is because it's possible that there is a path along flat terrain that goes around the mountains. You can speed up A*'s search by using 1.5 as the heuristic distance between two map spaces. A* will then compare 3 to 1.5, and it won't look as bad as comparing 3 to 1. It is not as dissatisfied with mountainous terrain, so it won't spend as much time trying to find a way around it. You can also speed up A*'s search by decreasing the amount it searches for paths around mountains: just tell A* that the movement cost on mountains is 2 instead of 3. Now it will search only twice as far along the flat terrain as along mountainous terrain.

A*'s ability to vary its behavior based on the heuristic and cost functions can be very useful in a game. The tradeoff between speed and accuracy can be exploited to make your game faster.

2.3 Speed or Accuracy? #

For most games, you don't really need the best path between two points. You just need something that's close. What you need may depend on what's going on in the game, or how fast the computer is.

2.3.1 Computer speed #

If your game is running on a slow computer, you can adjust your A* heuristic to approximate more. This will speed up A*, so your game won't slow to a crawl on those older computers. Conversely, if it is running on a fast computer, you can adjust the heuristic to give better paths. You have more CPU time, so why not use it to make the game run better? This seems to be a good situation -- players who have powerful computers can get more out of the game; yet you don't leave everyone else with an unplayable game.

2.3.2 Number of units #

As the number of military units on the map increases, it's going to use up more CPU time. You can just adjust the heuristics to give more approximate paths as the unit count goes up. This will keep your game from slowing down too much. Also, it's realistic. A general with a large army doesn't have as much time to spend planning each unit's movement as a general with a small army. The result? The large army's general thinks less per unit -- the equivalent of A* searching less of the map. Human players aren't going to be able to think as well when controlling a large force, so a computer that thinks less per unit might be a more realistic opponent.

2.3.3 Implementation ideas #

As the game is running, you can keep track of how much time is being spent on A* searches. If the time exceeds some threshold, you can adjust the heuristic function towards speed and away from accuracy. If it's lower than some threshold, you can make the heuristic more accurate. A self-adjusting heuristic takes care of both computer speed and number of units, and you don't have to spend time "tuning" it yourself.

3 Implementation Notes #


3.1 Sketch #

I won't describe the implementation in detail here, but you can look in an AI book or at another web page that has a description. See the references section for a list of books. Essentially, there is a set of nodes (map locations and a cost) called OPEN and another set called CLOSED. Each time through the main loop, you pick out the best element from OPEN (where "best" means "the one with the lowest cost"), and you look at its neighbors. You then put any unvisited neighbors into the OPEN set. The cost of a node is the sum of the current cost of walking from the start to that node and the heuristic estimate of the cost from that node to the goal.

3.2 Source code #

My own C++ A* code is available: "path.cpp" and "path.h". There is also an older (slower, but easier to understand) version of my code, as well as many other implementations of A*, on [http]Steve Woodcock's Games AI Page.

3.3 Performance tips #

A lot of people don't know why their A* implementation is so slow, and ask on rec.games.programmer or comp.ai.games. The most common reasons are:

3.3.1 Bad heuristic #

For example, on a grid map, you should have a heuristic that reflects the actual movement costs along normal directions and diagonals. If your game doesn't allow diagonal movement, you probably want abs(dx)+abs(dy). If it does, you probably want max(abs(dx),abs(dy)).

This is counterintuitive -- it makes the most sense to have a "true" distance heuristic. A*, however, is comparing the g values (actual movement costs) to h values (heuristic). If these two don't match, or if they aren't at the same scale, A* may give bad paths or may search too much.

The problem with this heuristic is that it gives you path that look bad, even if they aren't really bad! To remedy this, add a small (around 1% of the heuristic) penalty when the node isn't near the straight-line path from the start to the goal. Also see the Heuristics section of this document for more information.

3.3.2 Slow priority queue #

What's the first thing you'll think of using for these two sets? If you're like me, you probably thought "array". You may have thought "linked list", too. The problem is that finding the best node out of an array or list is slow. Lists and arrays are very general. Usually, more specific data structures can outperform general data structures. For A*, we do not need the flexibility of a list (fast insertion and deletion anywhere), nor do we need the flexibility of an array (fast access to any element). Instead, we want two operations: insert and delete best. (Actually, we need a third; see the note below.)

3.3.2.1 Heaps #

For these operations, what you really should have is a priority queue. These can be implemented in a variety of ways, including balanced binary trees, skip lists, and splay trees. My favorite is with heaps. Heaps are easy to implement, and they are stored in an array, which is pretty efficient. I use STL's efficient implementation of heaps for my code. I get speed and I don't have to code up and test a heap data structure. An example set of numbers: if you have 1,000 nodes in your OPEN set, then inserting into and removing from a sorted list takes an average of 500 comparisons and 2 moves; for an unsorted list it takes 1000 comparisons and 2 moves; for a sorted array it takes 500 comparisons and 500 moves; for a heap it takes 10 comparisons and 10 moves. That's approximately a factor of 100 faster. However, these are worst case values. More realistically, with 1000 nodes in the set, you should expect heaps to be 25% faster than linked lists, and for 1500 nodes in the set, you should expect heaps to be 50% faster. The more nodes you have, the faster the heap will be relative to an array or list.

It is important to keep in mind that we are not merely looking for asymptotice ("big O") behavior. We also want to look for a low constant. To see why, consider an algorithm that is O(log N) and another that is O(N), where N is the number of elements in the heap. It may be that on your machine, an implementation of the first algorithm takes 10,000 * log(N) seconds, while an implementation of the second one takes 2 * N seconds. For N = 256, the first would take 80,000 seconds and the second would take 512 seconds. The "faster" algorithm takes more time in this case, and would only start to be faster when N > 200,000.

You cannot merely compare two algorithms. You should also compare the implementations of those algorithms. You also have to know what size your data might be. In the above example, the first implementation is faster for N > 200,000, but if in your game, N stays under 30,000, then the second implementation would have been better.

A friend of mine (who does research in data structures for shortest-path algorithms) says that heaps are good unless you have more than 10,000 elements in your heap. Unless your game's map is extremely large, you probably don't need a more complicated data structure (like [http]multi-level buckets). You should probably [http]stay away from Fibonacci heaps, which have good asymptotic performance but have slow implementations unless N is huge.

Important Note for anyone using heaps: Unlike Dijkstra's Algorithm, A* requires that you determine whether the new node you are examining is already in the OPEN set, and if it is, whether it is better than the one in the OPEN set, and if it is better, then the one in the OPEN set must be adjusted. The problem is that when using heaps, it is difficult to find a particular node in the heap -- it requires a search through the entire heap, which is slow. To avoid this expensive search, my A* code has a "Marking" array to help it answer the two questions above. To avoid the first question (whether something is in the OPEN set), the Marking array keeps track of which map locations are in the OPEN and CLOSED sets. To avoid the second question (whether the new node is better than the old one), the Marking array keeps track of the g values at each node. Only if both questions are answered "yes" does my code invoke Find (which I call find_in_open). Experiments suggest that for most searches in my game, one of the two questions will be answered "no" most of the time, so almost all the calls to Find are eliminated when using the Marking array.

3.3.2.2 Splay Trees #

Heaps are a tree-based structure with expected O(log N) time operations. However, the problem is that with A*, the common behavior is that you have a low cost node that is removed (causing O(log N) behavior, since values have to move up from the very bottom of the tree) followed by low cost nodes that are added (cuasing O(log N) behavior, since these values are added at the bottom and bubble up to the very top). The expected case behavior of heaps here is equivalent to the worst case behavior. We may be able to do better if we find a data structure where expected case is better, even if the worst case is no better.

Splay trees are a self adjusting tree structure. Any access to a node in the tree tends to bring that node up to the top. The result is a "caching" effect, where rarely used nodes go to the bottom and don't slow down operations. It doesn't matter how big your splay tree is, because your operations are only as slow as your "cache size". In A*, the low cost nodes are used a lot, and the high cost nodes aren't used for a long time, so those high cost nodes can move to the bottom of the tree.

3.3.2.3 HOT Queues #

There's another good data structure that may be better than heaps. Often, you can restrict the range of values that would be in the priority queue. Given a restricted range, there are often better algorithms. For example, sorting can be done on arbitrary values in O(N log N) time, but when there is a fixed range it can be done in O(N) time.

We can use [http]HOT Queues (Heap On Top queues) to take advantage of a particular property of A* and Dijkstra's algorithms: when we are removing nodes with value f, the nodes we insert have value f + delta where delta <= C. (Note however that delta >= 0 only if your heuristic is admissible, and this technique is not as useful if you use an inadmissible heuristic.) The constant C is the maximum change in cost from one point to an adjacent point. We can therefore keep "buckets" that store some subset of the values (just like we do in the O(N) sorting algorithms), and in the most important bucket we can use a heap. For example, if there are ten buckets, then the heap only contains (on average) one tenth of the values, so it runs faster than using a heap for all of them. In addition, we do not organize the other nine tenths of the elements unless they are actually needed.

In A*, we often put nodes into OPEN that we never actually need. HOT Queues are a big win because the elements that are not needed are inserted in O(1) time. Only elements that are needed get heapified (which is not too expensive). The only operation that is more than O(1) is node deletion from the heap, which is O(log N) but remember that the queue is small, so it's not as bad as you'd expect.

In addition, if C is small, we do not even need a heap for the smallest bucket, so insertion and deletion are both O(1) time! (Compare this to heaps, which have insertions and deletions take O(log N) time.) One person reported that HOT queues are as fast as heaps for at most 800 nodes in the OPEN set, and are 20% faster when there are at most 1500 nodes. I would expect that HOT queues get faster as the number of nodes increases.

3.4 Variations of A* #


3.4.1 Beam search #

In the main A* loop, the OPEN set stores all the nodes that may need to be searched to find a path. The Beam Search is a variation of A* that places a limit on the size of the OPEN set. If the set becomes too large, the node with the worst chances of giving a good path is dropped. One drawback is that you have to keep your set sorted to do this; a priority queue implementation may not work well. (HOT queues would work, though.) On the other hand, sorting it is not much slower than a priority queue, and it may help so much to be able to drop nodes that it pays for the extra overhead of sorting. (Note that you don't do a full sort every time you add a node to the list; you only need to place the new node in the right place, which can be done in logarithmic time. Inserting into a priority queue is also logarithmic. It's just that priority queues can have a lower constant factor associated with the costs.)

3.4.2 Iterative deepening #

Iterative Deepening is used in many AI algorithms to start with an approximate answer, then make it more accurate. The name comes from game tree searches, where you look some number of moves ahead (for example, in Chess). You can try to deepen the tree by looking ahead more moves. Once your answer doesn't change or improve much, you assume that you have a pretty good answer, and it won't improve when you try to make it more accurate again. In ID-A*, the "depth" is a cutoff for f values. When the f value is too large, the node won't even be considered (i.e., it won't be added to the OPEN set). The first time through you process very few nodes. Each subsequent pass, you increase the number of nodes you visit. If you find that the path improves, then you continue to increase the cutoff; otherwise, you can stop. For more details, read these [http]lecture nodes on ID-A*.

I personally don't see much need for ID-A* in games. ID algorithms tend to increase computation time while reducing memory requirements. In games, however, the "nodes" are very small -- they are simply coordinates. I don't see a big win from not storing those nodes. As for processing time, although lists, arrays, and heaps are affected by a large number of nodes added to the OPEN set, HOT queues can be used to avoid using any CPU time for handling nodes with high f values.

3.4.3 Dynamic weighting #

With dynamic weighting, you assume that at the beginning of your search, it's more important to get (anywhere) quickly; at the end of the search, it's more important to get to the goal.
f(p) = g(p) + w(p) * h(p)
Here, there is a weight (w >= 1) associated with the heuristic. As you get closer to the goal, you decrease the weight; this decreases the importance of the heuristic, and increases the relative importance of the actual cost of the path.

3.4.4 Bandwidth search #

There are two properties about Bandwidth Search that some people may find useful. This variation assumes that h is an overestimate, but that it doesn't overestimate by more than some number e. If this is the case in your search, then the path you get will have a cost that doesn't exceed the best path's cost by more than e. Once again, the better you make your heuristic, the better your solution will be.

Another property you get is that if you can drop some nodes in the OPEN set. Whenever h+d is greater then the true cost of the path (for some d), you can drop any node that has an f value that's at least e+d higher than the f value of the best node in OPEN. This is a strange property. You have a "band" of good values for f; everything outside this band can be dropped, because there is a guarantee that it will not be on the best path.

Curiously, you can use different heuristics for the two properties, and things still work out. You can use one heuristic to guarantee that your path isn't too bad, and another one to determine what to drop in the OPEN set.

3.4.5 Bidirectional search #

Instead of searching from the start to the finish, you can start two searches in parallel -- one from start to finish, and one from finish to start. When they meet, you should have a good path.

This sounds like a good idea, but I don't think it'll give you very much. The idea behind bidirectional searches is that searching results in a `tree' that fans out over the map. A big tree is much worse than two small trees, so it's better to have two small search trees. With A*, however, my experiments suggest that you don't get a tree. You get a path that has nearby map areas explored, but it doesn't fan out like Dijkstra's algorithm. In fact, that's what makes A* so fast -- no matter how long your path is, it doesn't search like crazy, unless the path really is crazy. It tends to search only small areas of the map. Bidirectional search may be more useful if your map is complex.

The front-to-front variation links the two searches together. Instead of choosing the best forward-search node-g(start,x) + h(x,goal)-or the best backward-search node-g(y,goal) + h(start,y)-this algorithm chooses a pair of nodes with the best g(start,x) + h(x,y) + g(y,goal).

The retargeting approach abandons simultaneous searches in the forward and backward directions. Instead, it performs a forward search for a short time, chooses the best forward candidate, and then performs a backward search---not to the starting point, but to that candidate. After a while, it chooses a best backward candidate and performs a forward search from the best forward candidate to the best backward candidate. This process continues until the two candidates are the same point.

3.4.6 Hierarchical A* #

Path-Finding algorithms tend to be worse than linear: if you double the distance needed to travel, it takes more than twice as long to find the path. You can think of path-finding as searching some area like a circle --- when the circle's diameter doubles, it has four times the area.

One way to reduce the problem is to have multiple levels of searching. For example, to get from your home to a location in another city, you would find a path from your chair to your car, from the car to the street, from the street to a freeway, from the freeway to the edge of the city, from there to the other city, then to a street, to a parking lot, and finally to the door of the destination building. There are several levels of searching here:

  • At the street level, you are concerned with walking from one location to a nearby location, but you do not go out on the street.
  • At the city level, you go from one street to another until you find the freeway. You do not worry about going into buildings or parking lots, nor do you worry about going on freeways.
  • At the state level, you go from one city to another on the freeway. You do not worry about streets within cities until you get to your destination city.

Dividing the problem into levels allows you to ignore most of your choices. When moving from city to city, it is quite tedious to consider every street in every city along the way. Instead, you ignore them all, and only consider freeways. The problem becomes small and manageable, and solving it becomes fast.

You can use multiple levels with graph-searching algorithms such as A*. One paper on this topic is [http]Hierarchical A*: Searching Abstraction Hierarchies Efficiently. You do not need to use the same algorithm at each level. See [http]the section about splines for an example of using a path-finding algorithm at one level (from tile to tile) and a simple spline drawing algorithm at the lower level (from pixel to pixel).

A similar approach is to use varying resolution. First, plot a path with low resolution. As you get closer to a point, refine the path with a higher resolution. This approach can be used with path splicing to avoid moving obstacles.

3.4.7 Dynamic A* #

My impression of Dynamic A* (D*) was that it was good for robots but not necessarily for games. D* is intended for use when you don't have complete information. If you don't have all the information, A* can make mistakes; D*'s contribution is that it can correct those mistakes without taking much time. A* assumes you have all the information at the beginning, and then gives you a path to follow. D*'s does not require complete information at the beginning -- instead, you give it what you know, and it gives you a path. When you learn more, D* will make improvements to the path. D* solves a different problem than A*. D* never does worse than A*. If you have all the information at the beginning, they produce the same path. If you do not have all the information, they initially produce the same path but D* improves it, so its path is better than the one A* produces. However, D* requires a lot of space -- essentially you run A* and keep around its internal information (OPEN/CLOSED sets, path tree, g values), and then when the map changes, D* will tell you if you need to adjust your path to take into account the map changes. For a game with more than one unit, you usually don't want to keep all that information around, so D* can't really be used. It was designed for robotics, where there is just one robot -- you don't need to reuse the memory for some other robot's path. If your game has only one or a small number of units, you may want to investigate D*.

4 Dealing with moving obstacles #

A path-finding algorithm will compute a path around stationary obstacles, but what if the obstacles move? By the time a unit reaches a particular point, an obstacle may no longer be there, or a new obstacle may be there. One way to deal with this problem is to abandon path-finding and instead use movement algorithms that do not look far ahead; this approach will be discussed in a later section. This section will cover modifications to the path finding approach that could be used to handle moving obstacles.

4.1 Recalculating paths #

In most cases, as time passes we can expect an increase in the difference between the world and the world at the time the path was found. At some point in time, it may be worth recalculating the remaining path with up-to-date information. Listed below are some criteria that could be used for determining when a recalculation is needed:

  • Every N steps: this guarantees that the information used to calculate the path is not more than N steps old.
  • Whenever extra CPU time is available: this allows dynamic adjustment of path quality; as more units are deployed, or if the game is running on a slower computer, CPU usage per unit can be decreased.
  • Whenever the unit changes direction: if the shortest path in the world is a straight line, then a change of direction indicates an obstacle, so it is possible that the obstacle has moved; this is not as useful if you have varying terrain, which could also make your unit change direction.

The main drawback of path recalculation is that a lot of path information is thrown away. For example, if the path is 100 steps and it is recalculated every 10 steps, the total number of path steps is 100+90+80+70+60+50+40+30+20+10 = 550. For a path of M steps, approximately M^2 path steps are computed over time. Therefore path recalculation is not a good idea if you expect to have many long paths. It would be better to reuse the path information instead of throwing it away.

4.2 Path splicing #

When a path needs to be recalculated, it is often to go around nearby obstacles, not for distant obstacles. If distant obstacles are held constant, we could assume that the distant path will not change much. Therefore only the nearby portion of the path will change. Instead of recalculating the entire path, we can recalculate the first M steps of the path:

  1. Let p[1]..p[N] be the remaineder of the path (N steps)
  2. Compute a new path from p[1] to p[M]
  3. Splice this new path into the old path by removing p[1]..p[M] and inserting the new path in its place

http://theory.stanford.edu/~amitp/GameProgramming/mtn_path.png

Since p[1] and p[M] are fewer than M steps apart, it's unlikely that the new path will be long. Unfortunately, situations can arise in which the new path is long and not very good. The accompanying figure shows such a situation. The original path is 1-2-3-4; brown areas are obstacles. If we reach 2 and discover that the path from 2 to 3 has been blocked, path splicing would replace 2-3 with the path 2-3-5 and splice it in, resulting in the unit moving along path 1-2-5-3-4. We can see this is not a good path; 1-2-5-4 would be better.

Bad paths can often be detected by looking at the length of the new path. If it is significantly longer than M, it could be bad. A simple solution is to add a limit (maximum path length) to the path finding algorithm. If a short path isn't found, the algorithm returns an error code; in this case, use path recalculation instead of path splicing to get a path such as 1-2-5-4.

Implementation Note: Store the path in reverse order: it is easy to remove the beginning of the path and splice in a new path with a different length; because both operations occur at the end of the array. Essentially you treat the array as a stack where the top element is the next move to make.

For cases not involving these situations, for a path with N steps, path splicing will compute 2N to 3N path steps, depending on how often a new path is spliced in. This is a fairly low cost for the ability to resond to changes in the world. Surprisingly, the cost is independent of M, the number of steps for splicing. Instead of affecting CPU time, M controls a tradeoff between responsiveness and path quality. If M is high, the unit's movement will not respond quickly to changes in the map. If M is too low, the paths being spliced out may be too short to allow the replacement path to go around the obstacle cleanly; more suboptimal paths (such as 1-2-5-3-4) will be found. Try different values of M and different criteria for splicing (such as every 3/4 M steps) to see what's right for your map.

Path splicing is significantly faster than path recalculation, but it does not respond well to major changes in the path. It is possible to detect many of these situations and use path recalculation instead. It also has a few variables that can be adjusted, such as M and the choice of when to find a new path, so it can be adjusted (even at run-time) for different conditions.

4.3 Watching for map changes #

An alternative to recalculating all or part of the path at certain intervals is to have changes to the map trigger a recalculation. The map can be divided into regions, and every unit can express an interest in certain regions. (All the regions that contain part of the path could be of interest, or only nearby regions that contain part of the path.) Whenever an obstacle enters or leaves a region, that region is marked changed, and all units that have an interest in that region are notified, so that paths can be recalculated to take into account the change in obstacles.

Many variations of this technique are possible. For example, instead of immediately notifying the units, only notify them at regular intervals. Many changes can be grouped into one notification, so that excessive path recalculations are not needed. Another example is for the unit to poll the regions instead of the regions notifying the unit.

Watching for map changes allows units to avoid recalculation whenever the obstacles on the map do not change, so consider it if you have many regions that do not change often.

4.4 Dealing with clashes #

In general, moving obstacles can include friendly unit movement, enemy unit movement, and other changes to the map (such as the addition or removal of a wall). For the specific case of friendly unit movement, some additional techniques may be useful.

4.5 Predicting obstacle movement #

If obstacle movement can be predicted, it is possible to take into account future position of obstacles for path-finding. An algorithm such as A* has a cost function that determines how difficult it is to pass a point on the map. A* can be modified to keep track of the time required to reach a point (determined by current path length), and this time can be passed in to the cost function. The cost function can then take time into account, and use the predicted obstacle position at that time to determine whether the map space is impassable. This modification is not perfect, however, as it will not take into account the possibility of waiting at a point for the obstacle to move out of the way, and A* is not designed to differentiate between paths along the same route, but at different points in time.

4.6 Waiting for movement #

An easily implemented technique is based on the assumption that the other obstacle will move first. When an obstacle is in the way, just wait some amount of time for it to move. If it still hasn't moved after that period of time, recalculate a path around it or to the destination. If the obstacle is detected ahead of time, your unit can simply walk slower to give the other unit more time to get out of the way.

It is possible that two units will bump into each other, and each will wait for the other to proceed. In this case, a priority scheme can be used: assign each unit a unique number, and then make the lower numbered unit wait for the higher numbered unit. This will force one of the units to proceed if both are waiting. When obstacles are detected ahead of time, the lower numbered unit should slow down before reaching the expected point of collision.

4.7 Coordinated movement #

The technique described above does not work when units are trying to move through a narrow corridor. If one unit stands still while the other tries to go around, the corridor can't be used by both units. One unit will block it while the other one will take a long path around.

It should be possible for the second unit to communicate with the first one, and ask it to back up. Once the corridor is clear, the second unit can pass through, and then the first unit can go through. This may be complicated to implement unless you can identify the corridors beforehand. For randomly generated maps, it could be very difficult to determine where the corridor is and how far the first unit needs to back up.

5 Speed of precalculated paths vs. step-taking #

The general impression of path-finding algorithms appears to be that they are significantly slower than step-taking approaches. Although calculating a long path is significantly more time consuming than taking a step, in a comparison we must instead look at a series of steps.

5.1 Predictable behavior vs. potential speed #

Step-taking usually involves a constant amount of time per step taken, whereas path-finding involves an unknown amount of time per step taken, because it may be difficult to predict how many map spaces are going to be searched. It is a good idea to try your path-finder on some sample maps to get an idea of how much of the map is being searched. However, even if you tune the average case to be good (fast), the worst case scenario in a path-finder can be worse than a step-taker.

A step-taker's CPU usage is fairly easy to predict. This can be an advantage in many games, especially when timing is important. Although easy to predict, the time taken may be somewhat large, especially if sophisticated analysis is needed to determine what step to take. A step-taker usually recalculates information at each step: Unless the analysis is simple, the step-taker will have to analyze the neighboring area to determine where to move; at the next step, the analyzed area will overlap the area analyzed previously. A path-finder on the other hand can perform calculations once and use the results to analyze many steps. If the required analysis is complex, a path-finder may use less time overall than a step-taker.

5.1.1 Load spikes #

One situation to take into account is in a game that allows the player to select multiple units and select a destination for all of them. With path-finding, there will be many paths to compute at the same time, so you will get a load spike, a short period of very high load (CPU usage). Handling movement step-by-step wouldn't produce a load spike, because the load is distributed over a long period of time.

One way to avoid the load spike is to delay path-finding. Units can be programmed to follow either their instincts (simple movement) or their brains (a precalculated path). Units will follow their instincts unless their brains tell them otherwise. (This approach is used in nature and also in insect-like robots at MIT labs.) Instead of calculating all the paths at once, limit the game to finding one path every one, two, or three game cycles. Then let the units start walking according to instinct (which could simply be moving in a straight line towards the goal), and come back later to find paths for them. This approach allows you to even out the cost of path-finding so that it doesn't occur all at once.

5.1.2 Waypoints #

Step-takers take a similar amount of time no matter how long the path is. Path-finders however take more than twice as much time to find a path that is twice as long. Remember that path-finding is searching an area, and areas for a circle of diameter N grow in proportion to N^2. Also, the data structures used in A* (such as a heap) grow at rate N log N rather than simply N.

One way to reduce the growth is two split a long path into two shorter paths. You may ask the user to mark waypoints on the path: instead of simply clicking on a destination, ask the user to click on two or three points along the way to the destination. You now have three or four smaller paths to compute, and you save some time. The user also has some control over the overall path---for example, your path-finder may have found a path to the west of some mountains, but for safety's sake, the user wants to stay to the east of the mountains (near friendly guard towers).

The main change in unit movement code will be that instead of a single destination, you will have a list of destinations. Find a path to the first destination. Once you get there, remove it from the list and find a path to the next destination.

5.2 Path recalculation #

Unfortunately, a path-finder cannot reuse the results of analysis indefinitely. After some time, the results may be invalidated by updates to the map. The step-taker can respond immediately to the changes, but a calculated path is not going to reflect the most recent map. For a path-finder, the path may have to be verified or recalculated periodically; this can be a disadvantage if paths are long or if path updates are frequent. For example, in a game that allows walls and other obstacles to be blown up, a path may not be valid for long. Also, if the path calculation took into account enemy positions, those would not be valid for long. On the other hand, if your path-finder does not take into account faraway enemy positions, and if the obstacles on the map are fairly static, the path is likely to be good for a longer period of time. The more often information used for movement changes, the more of an advantage step-taking has over path-finding.

5.2.1 Idle-time recalculation #

To smooth out the varying CPU load that may occur with path-finding, the approach used to smooth out load spikes can be used with path recalculation. Paths can be calculated when the CPU load is low, and recalculation can be delayed if the CPU load is high. This puts idle cycles to good use, and avoids slowing down the game when there's a lot going on.

5.3 Redundancy in path-finding #

Sometimes, the path found by a path-finder can be used more than once. This reduces the cost of path-finding, since you can amortize one search over several uses.

5.3.1 Networked games #

Path-Finders produce a lot of information at the beginning, while step-takers produce a small amount of information at each step. For networked games, the path determined by the path-finder can be transmitted to the other machines on the network, so that the path need be calculated only once, rather than once per machine.

5.3.2 Multiple units #

  • See Also: Putting paths together is called path splicing in another section of these notes.
If many units are in or near the same location, and have the same destination (or destinations close to each other), it is very likely that the path found for one will be useful for other units. One idea is to find a path P from the center of the units to the center of the destinations. Then, use most of that path for all the units, but replace the first 10 steps and the last 10 steps by paths that are found for each individual unit. Unit i will receive a path from its starting location to P10, followed by the shared path P10..len(P)-10, followed by a path from Plen(P)-10 to the destination.

The paths found for each unit are short (approximately 10 steps on average), and the long path is shared. Most of the path is found only once and shared among all the units. However, the user may not be impressed if he sees all the units moving on the same path. To improve the appearance of the system, make the units follow slightly different paths. One way to do this is to alter the paths themselves, by choosing adjacent locations.

Another approach is to make the units aware of each other (perhaps by picking a "leader" unit randomly, or by picking the one with the best sense of what's going on), and only find a path for the leader. Then use a [http]flocking algorithm to make them move in a group.

5.4 Summary #

Try your path-finder on sample maps to see how many spaces are searched in relation to how many steps are taken. Some algorithms, such as A* using a very high heuristic, lead to a constant amount of searching per step in common cases. Other algorithms, such as Dijkstra's, use a lot of searching per step. The results will depend on your algorithm, your heuristic, and your map characteristics, so run some experiments.

  1. If there is a lot of work that has to be done per step taken, a path-finder's CPU usage may be difficult to predict. A more predictable, although sometimes slower, step-taker may be a better choice. If there is a small amount of work per step, then a path-finder's cost can be predicted, and directly compared to a step-taker.
  2. Also, ask yourself how often map changes will affect a path. If such changes are common, path-finding will be more costly, and step-taking may be better. If such changes are rare, path-finding may be inexpensive relative to step-taking because analysis can be performed once and reused for the entire path.
  3. If varying CPU load is a problem, consider load balancing by allowing units to continue moving with less information, until CPU load decreases, and a path can be found for the unit.
  4. Re-using the path found by the path-finder decreases the cost of path-finding, so look for ways to save and reuse the paths.

6 Space used by precalculated paths #

Sometimes, it's not the time needed to calculate a path, but the space used by paths for hundreds of units that is the limiting factor. Step-takers typically use very little space, while path-finders require space for the algorithm to run, plus space to store a path.

6.1 Locations vs. directions #

A path can be either locations or directions. Locations take more space, but have the advantage that it is easy to determine an arbitrary location or direction in the path without traversing the path. When storing directions, only the direction can be determined easily; the location can only be determined by going through the entire path, following the directions. In a typical grid map, locations may be stored with two 16-bit integers, making each step take 32 bits. There are far fewer directions, however, so they can take less space. If the unit can move in only four directions, each step takes only 2 bits; if the unit can move in six or eight directions, each step takes 3 bits. Either of these is a significant savings over storing locations in the path. Hannu Kankaanpaa suggests that you can further reduce the space requirement by storing the relative direction ("turn right 60 degrees") instead of the absolute direction ("go north"). Some relative directions may not make sense for some types of units. For example, if your unit is moving north, it's unlikely that the next step is to go south. In a six directional game, you have only five meaningful directions. On some maps, perhaps only three of those directions (straight, left 60 degrees, right 60 degrees) make sense, but on other maps, turning right 120 degrees may be a valid move (for example, when going up a steep mountain path with switchbacks).

6.2 Path compression #

Once a path can be found, it can be compressed in some way. A general purpose compression algorithm could be used, but will not be discussed here. A compression algorithm specific to paths could be used to shorten either location-based paths or direction-based paths. Before deciding, look at typical paths in your game to decide which kind of compression will work best. In addition, consider ease of implementation (and debugging), the size of the code, and whether it really matters. If you have a limit of 300 units and only 50 are walking at any one time, and paths are short (100 steps), the total memory requirement might only be <50k anyway, and not worth worrying about compression.

6.2.1 Location storage #

In maps where obstacles rather than terrain are the main influence in determining paths, there may be many straight-line segments in the path. If this is the case, then a path need contain only the endpoints (sometimes called waypoints) of those line segments. Movement consists of examining the next point on the path and moving in a straight line towards it.

6.2.2 Direction storage #

When directions are stored, it may be the case that a particular direction is followed many times in a row. You can take advantage of that common pattern to store the path in less space.

One way to store the path is to store both a direction and a number which indicates how many times the unit should move in that direction. Unlike the optimization for location storage, this optimization can make things worse if a direction is not taken many times in a row. Also, for many straight lines where location compression is useful, direction compression is not, since the line may not be aligned with one of the walking directions. With relative directions, you can eliminate "keep going forward" as a possible direction. Hannu points out that with an eight direction map, you can eliminate forwards, backwards, and the 135 degree left and right turns (assuming your map allows it), and you can then store each direction with only two bits.

Another way to store the path is to use variable length encoding. The idea is to use a single bit (0) for the most common step: go straight. Use a 1 to mark a turn, and follow the 1 by some number of bits to represent the turn. In a four directional map, you can turn only left or right, so you might use 10 for left and 11 for right.

Variable length encoding is more general and may compress better than run length encoding for mixed paths, but not as well for long straight paths. The sequence (north, straight six steps, turn left, straight three steps, turn right, straight five steps, turn left, straight two steps) is represented as (NORTH, 6), (WEST, 3), (NORTH, 5), (WEST, 2) with run length encoding. If each direction is two bits and each distance is eight bits, this path requires 40 bits to store. With variable length encoding, you use one bit for each step and two bits for each turn -- NORTH 0 0 0 0 0 0 10 0 0 0 11 0 0 0 0 0 10 0 0 -- a total of 24 bits. If the initial direction and each turn imply one step, you can save one bit per turn, resulting in 20 bits to store the path. However, longer paths can take more space with variable length encoding. The sequence (north, straight two hundred steps) is (NORTH, 200) with run length encoding, a total of 10 bits. The same sequence with variable length encoding is NORTH 0 0 ..., a total of 202 bits.

6.3 Beacons #

A beacon is a spot marked as good for paths. For example, ancient trade routes often would pass particular points, which often became trading towns. Given knowledge of those beacons, a path-finder can use known paths between beacons and only calculate a path to the first beacon on the journey and from the last beacon on the journey. This approach is commonly used by people travelling: a person determines the path from the starting point to a train station or airport, and then takes a predetermined path to another train station or airport, then finally determines a path to the destination. The path-finder searches a path from beacon to beacon, rather than one step at a time. These large-step paths involve a large number of steps but a small amount of storage, since only the location of the next beacon is stored in the path. Beacons work well on hand drawn maps but may be difficult to determine on computer generated maps.

6.4 Limited path length #

Given that map conditions or orders may change, it may not make sense to store a long path, since at some point the remainder of the path may not be of any use. Each unit can store a fixed number of steps at the beginning of the path, and then use path recalculation when the path has almost been traversed. This approach allows for control of the amount of data used per unit.

6.5 Summary #

Paths can potentially take up a lot of space in a game, especially when the paths are long and there are many units present. Path compression, waypoints, and beacons reduce the space requirements by storing many steps in a small amount of data. Waypoints rely on straight-line segments being common so that we have to store only the endpoints, while beacons rely on there being well-known paths calculated beforehand between specially marked places on the map. If paths still take up too much space, the path length can be limited, resulting in the classic time-space tradeoff: to save space, information can be forgotten and recalculated later.

7 Walking traps in steppers #

Path finders typically scan a large area to determine a path for the unit to follow. Step taking algorithms typically scan a small area instead to determine a few steps for the unit to take. Step takers can respond better to changes in the map, but this diagram shows one of the problems that can occur:


The unit is initially at the bottom of the map and wants to get to the top. There is nothing in the area it scans (shown in green) to indicate that the unit should not move up, so it continues on its way. Near the top, it detects an obstacle and changes direction. It then finds its way around the "U"-shaped obstacle, following the red path. In contrast, a path-finder would have found a shorter path (blue), never sending the unit into the concave shaped obstacle. How can we avoid these areas in a step taker?

7.1 Predetermined places to avoid #

If maps are created by a human, simply avoid making concave obstacles. Alternatively, you could mark each concave area in the map. Then, the unit can treat interiors of concave areas as obstacles (except those that contain the unit's destination).


In this figure, the shaded area is treated as an obstacle because the unit does not want to travel into its interior. The unit can go around instead of going inside and then coming out.

This technique is also useful for computer generated maps, as long as they do not change often as the game runs. You can write an algorithm to find all concave obstacles, and then mark their interiors as possible obstacles.

7.2 Learning to avoid traps #

Instead of marking areas of the map as being obstacles, it should be possible for units to try any path, but then learn to avoid areas that caused them to turn around. This technique may be more useful for maps that change often, as the units can learn to avoid an area, but be willing to try it again occasionally to see if things have changed.

8 AI Techniques #

Path-finding is often associated with AI, because the A* algorithm and many other path-finding algorithms were developed by AI researchers. I've often heard talk of using neural networks or genetic algorithms in path-finding, and I'll describe why I don't see this as being terribly promising.

8.1 Neural Networks #

Neural networks are structures that can be "trained" to recognize patterns in inputs. I believe they are a good match for problems in which pattern recognition code is difficult to write (one knows the patterns but not how to recognize them) or in which the patterns are dynamic (one does not know the patterns ahead of time). Path-finding, in my opnion, is neither of these: we do know how to solve the problem, and we have a general algorithm that can handle all sorts of patterns (mountains, rivers, valleys, etc.) without special code. We want to do the same thing in all the cases: find a good path. If on the other hand, the problem was to do different things in each case (for example, meditate if you're on a mountain, swim if you're in a river, or run if you're in a valley), then you need to determine which type of landscape you're in, and for that it may make sense to use neural networks. (If you're using a static map, however, I would recommend that you simply mark the areas of each type.) For path-finding itself, however, I don't think we need a different technique for each type of landscape, so I don't consider it important to recognize the patterns in the landscape.

8.2 Genetic Algorithms #

Genetic Algorithms allow you to explore a space of parameters to find solutions that score well according to a "fitness function". I believe they are a good match for problems in which it's not clear how to find a solution. They're also a good technique to use if you have little information available about what kinds of parameters are good. Genetic Algorithms are essentially a blind search over an area, with some feedback that tells you if you're getting closer. However, if you know something about the parameter space, you can do better than a blind search: you know which direction to travel in, and it's likely you'll get there faster if you know where to go. For path-finding, we know algorithms that can help us find a good path; why look for random paths and evaluate them? Genetic Programming takes Genetic Algorithms a step further, and treats programs as the parameters. For example, you may be breeding path-finding algorithms, and your fitness function would rate each algorithm based on how well it does. Again, I believe Genetic Programming works well if you don't know a good program to solve your problem, but humans tend to do better than random trial and error. [http]John Koza at Stanford has shown me genetic programs that do a good job for some problems, but don't handle all the cases; also they're not the simplest or fastest program that can do the job. If you're a student of evolution, you know that nature has not produced the best solutions; it just finds those that are "good enough". Human reasoning can often come up with better solutions than evolution's blind search. Nature didn't come up with the wheel, with fire, or a cure for Polio. On the other hand, Genetic Algorithms and Genetic Programming can come up with solutions that humans just hadn't thought of, especially in problems that defy logic. Still, path-finding has been extensively studied and we have good algorithms; I think Genetic Algorithms are better suited for problems that we haven't found good solutions for.

9 References #

10 Map representations #

10.1 Flat #

A flat map has but one level in its representation. It's rare for games to have only one level -- often there is a "tile" level and then a "sub-tile" level in which objects can move within a tile. However, it's common for path-finding to occur on only one of the levels.

10.2 Hierarchal #

A hierarchal map has many levels in its representation. A heterogenous hierarchy typically has a fixed number of levels, each with different characteristics. Ultima VI, for example, has a "world" map, on which are cities and dungeons. You can enter a city or dungeon and be in a second map level. In addition, there are "layers" of worlds on top of one another, making for a three-level hierarchy. A homogeneous hierarchy has an arbitrary number of levels, each with the same characteristics. Quad trees and oct trees can be considered to be homogeneous hierarchies.

In a hierarchal map, path-finding may occur on several levels. For example, if a 1024x1024 world was divided into 64x64 "zones", it may be reasonable to find a path from the player's location to the edge of the zone, then from zone to zone until reaching the desired zone, then from the edge of that zone to the desired location. At the coarser levels, long paths can be found more easily because the path-finder does not consider all of the details. When the player actually walks across each zone, the path-finder can be invoked again to find a short path through that zone. By keeping the problem size small, the path-finder can run quicker.

10.3 Non-grid path-finding #

Through most of this document I've assumed that A* was being used on a grid of some sort, where the "nodes" given to A* were grid locations and the "edges" were directions you could travel from a grid location. However, A* was designed to work with arbitrary graphs, not only grids.

10.3.1 Polygonal maps #

One form of graph that you may want to use if your game does not have terrain, and has polygonal obstacles, is based on "navigation points".


In this diagram, I've marked the corners of every obstacle with a blue dot. In addition, the start and end points are blue dots. These navigation points are the nodes to feed to A*. In addition, A* needs to know which points are connected. To determine this, look at each pair and decide whether the straight line between the two points is unobstructed. In this diagram, these edges are the light blue lines. The third piece of information A* needs is distances between the points. Depending on how your units move, that can be manhattan distance, diagonal grid distance, or straight line distance.

A* will then consider paths from navigation point to navigation point. The dotted green line is one such path. This is much faster than looking for paths from grid point to grid point, as you have only 2 + 4*(#obstacles) navigation points, instead of xsize * ysize grid locations. When there are no obstacles in the way, A* will do very well -- the start point and end point will be connected by an edge, and A* will find that path immediately, without expanding any other navigation points. Even when there are obstacles to consider, A* will jump from corner to corner until it finds the best path, which will still be much faster than looking for a path from a grid location to another.

10.3.2 Road maps #

If your units can only move on roads, you may want to consider giving A* the road and intersection information. Each intersection will be a node in the graph, and each road will be an edge. A* will find paths from intersection to intersection, which is typically much faster than exploring all possible directions, one grid space at a time. To save time, build the node/edge graph ahead of time instead of when you run A*.

For some applications, your units may not start and end on intersections. To handle this case, each time you run A*, you will need to modify the node/edge graph. Add the starting and ending points as new nodes, and add edges between these points and their nearest intersections. After path-finding, remove these extra nodes and edges from the graph so that the graph is ready to be used for the next invocation of A*.


In this diagram, the intersections are blue circles. In addition, the start and end points are blue circles. These are the nodes in the graph for A*. The edges are the roads between the nodes, and these edges should be given the driving distance along each road (shown in black text).

In the "roads as edges" framework, you can incorporate one-way roads as unidirectional edges in the graph.

If you want to assign costs to turning, you can extend the framework a bit: instead of nodes being locations, consider nodes to be a <location, direction> pair (a point in state space), where the direction indicates what direction you were facing when you arrived at that location. Replace edges from X to Y with edges from <X, dir> to <Y, dir> to represent a straight drive, and from <X, dir1> to <X, dir2> to represent a "turn". Each edge represents either a straight drive or a turn, but not both. You can then assign costs to the edges representing turns.

If you also need to take into account turn limitations, such as "only right turns", you can use a variation of this framework in which the two types of edges are always combined. Each edge represents an optional turn followed by a straight drive. In this framework, you can represent restrictions like "you can only turn right": include an edge from <X, north> to <Y, north> for driving straight, and an edge from <X, north> to <Z, east> for the right turn followed by a drive, but don't include <X, north> to anything west, because that would mean a left turn, and don't include anything south, because that would mean a U-turn.

In this framework, you can model a large city downtown, in which you have one-way streets, turn restrictions at certain intersections (often prohibiting U-turns and sometimes prohibiting left turns), and turn costs (to model slowing down and waiting for pedestrians before you turn right). Compared to grid maps, A* can find paths in road graphs environment fairly quickly, because there are few choices to make at each graph node, and there are relatively few nodes in the map.

11 Long and short term goals #

I've concentrated on the task of finding paths from one place to another. However, a lower level question is: once I have a path, how do I move along it? The most obvious answer is moving in a straight line from one location to the next. However, you might also want to move in a curve, or have multiple levels of movement. You may want to treat the locations as a low-priority goal from which you deviate. A higher level question is: where do you want to go? Unless you first answer the higher level question, path-finding is not very useful. Certainly, one form of goal setting is asking the user to click on the destination. However, you may have automated tasks as well---exploring, spying, attacking, and building are common ones.

11.1 Unit Movement #

I've concentrated on path-finding, which reduces the problem of moving from one location to another with the many smaller problems of moving from one space to an adjacent space.

You could move in a straight line from one location to the next but there are alternatives. Consider the four movement paths on this diagram:


The red path is the standard approach: move from the center of one square to the center of the next. The green path is somewhat better: move in straight lines between the edges between the tiles, instead of the center of the tiles. You might also try moving in straight lines between the corners of tiles. The blue paths use splines, with dark blue being low order splines and light blue being a higher order spline (which takes longer to compute).

Lines between corners and edges of tiles will be the shortest solution. However, the splines can make your units seem less mechanical and more "alive". Gamedev has an article about splines for movement.

If your units cannot turn easily, you may want to take that into account when plotting a movement path. Craig Reynolds has a great page about steering that has a paper about steering and Java applets demonstrating various behaviors. If you have more than one unit moving along a path, you may also want to investigate flocking. Craig recommends that instead of treating paths as a list of places your unit must visit, you treat paths as "a guideline, from which you deviate reactively as conditions require."

11.2 Behavior flags or stacks #

Your units may have more than one goal. For example, you may have a general goal like "spying" but also a more immediate goal like "go to the enemy headquarters". In addition, there may be temporary goals like "avoid that patrol guard". Here are some ideas for goals:

  • Stop: Stay in the current location
  • Stay: Stay in one area
  • Flee: Move to a safe area
  • Retreat: Move to a safe area, while fighting off enemy units
  • Explore: Find and learn about areas for which little information is known
  • Wander: Move around aimlessly
  • Search: Look for a particular object
  • Spy: Go near an object or unit to learn more about it, without being seen
  • Patrol: Repeatedly walk through an area to make sure no enemy units go through it
  • Defend: Stay near some object or unit to keep enemy units away
  • Guard: Stay near the entrance to some area to keep enemy units out
  • Attack: Move to some object or unit to capture or destroy it
  • Surround: With other units, try to surround an enemy unit or object
  • Shun: Move away from some object or unit
  • Avoid: Stay away from any other units
  • Follow: Stay near some unit as it moves around
  • Group: Seek and form groups of units
  • Work: Perform some task like mining, farming, or collecting

For each unit you can have a flag indicating which behavior it is to perform. To have multiple levels, keep a behavior stack. The top of the stack will be the most immediate goal and the bottom of the stack will be the overall goal. When you need to do something new but later want to go back to what you were doing, push a new behavior on the stack. If you instead need to do something new but don't want to go back to the old behavior, clear the stack. Once you are done with some goal, pop it from the stack and start performing the next behavior on the stack.

12 Heuristics #

When using a heuristic-based algorithm like A*, you may wonder what kind of heuristic you should use. In this section I will cover mostly ideas for grid maps. If you use A* on a polygonal map, you may want different heuristics. The heuristic should be a continuous function; jumps in the heuristic from one map space to its neighbor may cause A* to behave strangely.

A* does best (i.e., finds an optimal path in the least time) when the distance is close to the minimal path cost from the current node to the goal. However, we may want some modifications to the heuristic to give us better behavior, depending on what we really need. For example, we may not need the optimal path, or perhaps we want different characteristics for the beginning of the path and the end of the path.

12.1 Manhattan distance #

The standard heuristic is the Manhattan distance. Look at your cost function and see what the least cost is for moving from one space to another. In my game, it is 10. Therefore, the heuristic in my game should be 10 times the Manhattan distance:
h(A) = 10 * (abs(A.x-goal.x) + abs(A.y-goal.y))
You should use the scale that matches your cost function.

12.2 Diagonal distance #

If on your map you allow diagonal movement, then you need a different heuristic. The Manhattan distance for (4 east, 4 north) will be 8. However, you could simply move (4 northeast) instead, so the heuristic should be 4. This function handles diagonals:
h(A) = max(abs(A.x-goal.x), abs(A.y-goal.y))

12.3 Straight line distance #

If your units can move at any angle (instead of grid directions), then you should probably use a straight line distance:
h(A) = sqrt((A.x-goal.x)^2 + (A.y-goal.y)^2)

12.4 Guessing path quality #

Instead of assuming the path is the best possible, you could check for obvious obstacles, and increase your path cost if you find some. To do this, construct a polygonal estimate of the map (make polygons for impassable areas, but ignore passable terrain, no matter how difficult it is to traverse). Then when you are looking at the distance from the current position to the goal, you can scan for simple obstacles and draw a line around them. The length of that line can be your heuristic.

12.5 Breaking ties #

One thing that can lead to poor performance is ties in the heuristic. When several paths have the same f value, they are all explored, even though we only need to explore one of them. To solve this problem, we can add a small tie-breaker to the heuristic. This tie breaker also can give us nicer looking paths:
double dx1 = currentX - goalX;
double dy1 = currentY - goalY;
double dx2 = startX - goalX;
double dy2 = startY - goalY;
cross = dx1*dy2 - dx2*dy1;
if( cross<0 ) cross = -cross;
... add cross*0.001 to the heuristic ...
This code computes the vector cross-product between the start to goal vector and the current point to goal vector. When these vectors don't line up, the cross product will be larger. The result is that this code will give some slight preference to a path that lies along the straight line path from the start to the goal. It can significantly improve the speed of path-finding when there are few obstacles around.

To see a demonstration of the improvement from this fudge factor, see James Macgill's A* applet. Use "Clear" to clear the map, and choose two points on opposite corners of the map. When you use the "Classic A*" method, you will see the effect of ties. When you use the "Fudge" method, you will see the effect of the above cross product added to the heuristic.

12.6 Searching for an area #

If you want to search for any spot near some goal, instead of one particular space, you could construct a heuristic h'(x) that is the minimum of h1(x), h2(x), h3(x), ... where h1, h2, h3 are heuristics to each of the nearby spots. However, a faster way is to just let A* search for the center of the goal area. Once you pull any nearby space from the OPEN set, you can stop and construct a path.

13 Movement costs for path-finders #

When using a path-finding algorithm, you may want to treat map spaces as something other than clear and blocked. Often there is more information available, such as the difficulty of moving through that area. For example, swamps and mountains may be more difficult to pass than grasslands and desert. With some algorithms, like A*, you can put this encode this information into the cost function. Listed below are some ideas for movement costs that might be useful.

13.1 Altitude #

High altitudes (such as mountains) can have a higher movement cost than low altitudes. With this cost function, your units will try to stay in the lowlands whenever possible. For example, if the source and destination are both at high altitudes, the unit might move downhill, travel for a while, and then move back uphill.

13.1.1 Moving uphill #

  • Note: You can find an example of using hills in the movement cost in my A* code, in the UnitMovement class.

Instead of high altitudes having a high cost, moving uphill can have a high cost. This avoids the odd situation described above. With this cost function, units try to avoid moving uphill. Faced with the same situation, the unit will try to avoid moving back uphill at the end; it can do this by staying at a high altitude throughout its travels. A cost function such as this one may be good for units such as soldiers, which can move downhill easily but have a hard time going uphill.

13.1.2 Moving up- or downhill #

Some units, such as tanks, have a hard time moving uphill or downhill. You can assign a high cost to moving downhill, and an even higher cost to moving uphill. The units will try to avoid changing altitudes.

13.2 Terrain #

You may want different types of terrain to have different movement costs.

13.2.1 Forests, mountains, and hills #

Instead of relying on altitude, you may want to use terrain types. (For example, Civilization does this.) Each terrain type can have a movement cost associated with it. This movement table might apply to all units, or different movement tables could be associated with each unit type. For example, soldiers might have no trouble moving through forests, but tanks might have a very hard time. A fancier method is to assign movement costs to changing terrain. Going from grassland into mountains could be more expensive than going from hills to mountains, which could be more expensive than going from mountains to mountains.

13.2.2 Roads #

In many games, the primary purpose of roads is to make movement possible or easier. After choosing a movement cost function, you can add a road modifier to it. One possibility is to divide the cost by some constant (such as two); another is to assign a fixed cost to movement along a road.

  • Note: You can find an example of using roads in the movement cost in my A* code, in the UnitMovement class.

I strongly advise that you do not make road movement free (zero-cost). This confuses path-finding algorithms such as A*, because it introduces the possibility that the shortest path from one point to another is along a winding road that seems to lead nowhere. The algorithm has to search a very wide area to make sure that no such roads exist. Note that in the game Civilization, railroads had zero-cost movement, but when using the "Auto Goto" function, railroads had a non-zero cost. This is evidence that a path-finding algorithm was being used.

13.2.3 Walls or other barriers #

Instead of checking both movement costs and for obstacles in your path-finding algorithm, you can just use movement costs. Just assign a very high movement cost to any obstacle. When expanding nodes (in the A* algorithm), check if the cost is too high; if it is, then throw the node out.

13.2.4 Sloped Land #

  • Note: You can find an example of using land slopes in the movement cost in my A* code, in the FlatCanalPath class.

Instead of using movement up and down hills, you might want to make movement on any hill expensive. To do this, compute the overall slope of the terrain (by looking at the maximum difference between the current tail and its neighbors), and use that as part of the movement cost. Land that is very steep will have a high cost and land that is shallow will have a low cost. This approach differs from the movement uphill/downhill cost in that it looks for land that is steep, while the previous approach looked for units that move in a steep direction. In particular, if you're on a hill and can move left or right without going up or down, the uphill/downhill approach will consider it a low cost, while this approach will consider it a high cost (because the land is steep even if you aren't going up or down).

Sloped land costs may not make sense for unit movement, but you can use path-finding for more than finding paths for units. I use it for finding paths for roads, canals, bridges, and so on. If you want to build these items on flat land, you can take land slope into account when finding a path for a road or canal. See the section on applications for more ideas.

13.3 Enemies and friendly units #

Another modifier can help you avoid enemy units. Using influence maps, you can keep track of areas that are near enemy or friendly units. The influence map has a positive value for friendly units and a negative value for enemy units. By increasing the movement cost whenever you are in negative territory, you can influence your units to stay away from the enemy.

Even more complicated (and perhaps not possible with influence maps) is to look at visibility: is your unit visible by an enemy unit? Is your unit detectable in some other way? Is it possible for that enemy unit to fire on you?

13.4 Marked beacons #

If your map is designed and not automatically generated, you can add extra information to it. For example, you can mark certain positions as beacons, and pre-calculate good paths between beacons. The paths between beacons can have a lower movement cost than other paths; that way, the path finding algorithm could gravitate towards one of these predetermined paths. You can imagine a plateau (normal movement costs) with a canyon (paths between beacons); if the path-finding algorithm falls into the canyon, it's likely to stay in there. This could cut the time it takes to find a path (but find less optimal paths); however, I have not tried this technique so I am not sure it works.

Another way to think about this technique is that you need to make decisions about where to move next. With beacons, you do not need to make very many decisions---you simply decide which beacon to visit next, instead of where to turn at each step.

Good choices for beacons include lighthouses, cities, mountain passes, and bridges.

13.5 Fuel Consumption #

In addition to looking at the time it takes to go somewhere, you may want to consider the fuel it takes. The fuel consumption may be given more weight when the unit's fuel level is lower.

  • Note: To keep the state space small, you need to round off the fuel value to a coarse measurement unit. Unfortunately, this makes the search less effective.

To track the fuel usage through the map, you need to use state space, as described in . The state would be the pair <location, fuel>. However, state space can become very large, so it may be worth looking at alternatives to using ordinary A*.

One alternative is [ftp]A* with Bounded Costs (ABC). With ABC, you can assign a bound ("20 gallons") to a cost ("fuel").

14 User experience with shortest paths #

What's most important in the game is the user. You want the user to have fun! You don't want him (or her) to feel like the computer is cheating, or that the game units aren't behaving properly.

14.1 Dumb Movement #

If the path-finding doesn't work well, the user will end up moving the units manually. Avoid this! In Civilization, the rules for the game allowed for zero-cost movement along railroads. However, the pathfinder had a non-zero movement cost. The result was that users avoided using the pathfinder, and instead moved units manually on the railroads. In Command and Conquer, units could get stuck in "U" shaped traps (see the section on walking traps) so users would have to guide the units manually. A dumb pathfinder will annoy your users and make them move units themselves, so make your pathfinder decent!

14.2 Smart Movement #

Making units too smart is almost as bad as making units too dumb. If the player has to deal with fog of war but the pathfinder has access to the entire map, the units will mysteriously know where to go even though the user does not. That's a clear sign to the user that something odd is going on. On the other hand, it gives better paths. A compromise is to scale up the movement costs on unexplored areas. For example, if your normal movement costs are 1 for grass, 3 for forest, and 7 for mountains, set them differently on unexplored areas: 5 for grass, 6 for forest, 7 for mountains. The unit will take mountains vs. grass into account, but not too much; it'll just be a subtle hint. Raising costs for moving through unexplored areas will also tend to make the unit stay in explored territory as much as possible. You might want to do the opposite for "scout" units: they should prefer unexplored areas.

Try to keep your units balanced between too dumb and too smart. The goal should be to make it match what the user might have done to move the unit around.

14.3 Multithreading #

You can use multithreading to improve the user experience. When a unit needs a path, allow it to start moving in a straight line towards the goal, and add a request to a pathfinding queue. In another (low priority) thread, pull requests off the queue and find paths. Your units will start moving immediately, so the user won't be left wondering if something is wrong, and you won't have a high CPU load (which will slow down the rest of the game) while the path is being calculated.

14.4 Multiple Units #

If your game allows multiple units in a group to move together, try to make the movement look interesting. You can find a single path for them all to follow, and then have them all follow the path individually, but this will lead to either a line of units or units trying to pass each other. Instead, vary the paths a little so that they can walk in parallel. Alternatively, pick one "leader" unit to move along the path and have the other units use a separately programmed "follow" behavior. This following could be as simple as moving towards the leader but stay some distance away, or it could be as involved as flocking.

15 Applications #

In addition to finding a path for a unit to move along, path-finding can be used for several other purposes.

15.1 Exploration #

If part of your cost function penalizes paths that are on known territory, paths are more likely to go through unexplored territory. These paths are good for scout units.

15.2 Spying #

If part of the cost function penalizes paths near the enemy's watchtowers and other units, your unit will tend to stay in hiding. Note however that to work well, you may have to update the path periodically to take into account enemy unit movements.

15.3 Road Building #

Historically, roads have been built along paths that are often used. As the paths are used more and more often, vegetation is removed and replaced with dirt, and later with stone or other material. One application of path-finding is to find roads. Given places that people want to go (cities, lakes, springs, sources of minerals, and so on), find paths randomly between these important locations. After finding hundreds or perhaps thousands of paths, determine which spaces on the map most often occur on paths. Turn those spaces into roads. Repeat the experiment, with the path-finder preferring roads, and you will find more roads to build. This technique can work for multiple types of roads as well (highways, roads, dirt paths): the most commonly used spaces would become highways and less commonly used spaces would become roads or dirt paths.

15.4 City Building #

Cities often form around natural resources such as farmland or sources of mineral wealth. As people from these cities trade with each other, they need trading routes. Use path-finding to find their trading routes, and then mark a day's worth of travel on these routes. After a caravan travels for a day, it will need a place to stop: a perfect place for a city! Cities that lie along more than one travel route are great places for trading villages, which eventually grow into cities.

A combination of the road building and city building may be useful for producing realistic maps, either for scenarios or for randomized maps.

16 소스 코드 #

16.1 path.h #

#ifndef Path_h
#define Path_h

#include "Map.h"    // Specific to my game
#include <vector.h> // From STL
#include <algo.h>   // From STL

#define ALTITUDE_SCALE (NUM_TERRAIN_TILES/16)
#define MAXIMUM_PATH_LENGTH 100000

// Statistics about the path are kept in this structure
struct PathStats
{
    int nodes_searched;
    int nodes_added;
    int nodes_removed;
    int nodes_visited;
    int nodes_left;
    int path_length;
    int path_cost;
    PathStats()
        :nodes_searched(0), nodes_added(0), nodes_removed(0),
         nodes_visited(0), nodes_left(0),
         path_length(0), path_cost(0)
    {}
};

const int PathCutoff = 15;        // default cutoff
PathStats FindUnitPath( Map& map, HexCoord A, HexCoord B,
                        vector<HexCoord>& path, Unit* unit,
                        int cutoff = PathCutoff );
PathStats FindBuildPath( Map& map, HexCoord A, HexCoord B,
                    vector<HexCoord>& path );
PathStats FindCanalPath( Map& map, HexCoord A, HexCoord B,
                         vector<HexCoord>& path );

int hex_distance( HexCoord a, HexCoord b );
int movement_cost( Map& m, HexCoord a, HexCoord b, Unit* unit );

#endif

16.2 path.cpp #

//////////////////////////////////////////////////////////////////////
// Amit's Path-finding (A*) code.
//
// Copyright (C) 1999 Amit J. Patel
//
// Permission to use, copy, modify, distribute and sell this software
// and its documentation for any purpose is hereby granted without fee,
// provided that the above copyright notice appear in all copies and
// that both that copyright notice and this permission notice appear
// in supporting documentation.  Amit J. Patel makes no
// representations about the suitability of this software for any
// purpose.  It is provided "as is" without express or implied warranty.
//
//
// This code is not self-contained.  It compiles in the context of
// my game (SimBlob) and will need modification to work in another
// program.  I am providing it as a base to work from, and not as
// a finished library.
//
// The main items of interest in my code are:
//
// 1. I'm using a hexagonal grid instead of a square grid.  Since A*
//    on a square grid works better with the "Manhattan" distance than with
//    straight-line distance, I wrote a "Manhattan" distance on a hexagonal
//    grid.  I also added in a penalty for paths that are not straight
//    lines.  This makes lines turn out straight in the simplest case (no
//    obstacles) without using a straight-line distance function (which can
//    make the path finder much slower).
//
//    To see the distance function, search for UnitMovement and look at
//    its 'dist' function.
//
// 2. The cost function is adjustable at run-time, allowing for a
//    sort of "slider" that varies from "Fast Path Finder" to "Good Path
//    Quality".  (My notes on A* have some ways in which you can use this.)
//
// 3. I'm using a data structure called a "heap" instead of an array
//    or linked list for my OPEN set.  Using lists or arrays, an
//    insert/delete combination takes time O(N); with heaps, an
//    insert/delete combination takes time O(log N).  When N (the number of
//    elements in OPEN) is large, this can be a big win.  However, heaps
//    by themselves are not good for one particular operation in A*.
//    The code here avoids that operation most of the time by using
//    a "Marking" array.  For more information about how this helps
//    avoid a potentially expensive operation, see my Implementation
//    Nodes in my notes on A*.
//
//  Thanks to Rob Rodrigues dos santos Jr for pointing out some
//  editing bugs in the version of the code I put up on the web.
//////////////////////////////////////////////////////////////////////

#include "Path.h"

// The mark array marks directions on the map.  The direction points
// to the spot that is the previous spot along the path.  By starting
// at the end, we can trace our way back to the start, and have a path.
// It also stores 'f' values for each space on the map.  These are used
// to determine whether something is in OPEN or not.  It stores 'g'
// values to determine whether costs need to be propagated down.
struct Marking
{
    HexDirection direction:4;    // !DirNone means OPEN || CLOSED
    int f:14;                    // >= 0 means OPEN
    int g:14;                    // >= 0 means OPEN || CLOSED
    Marking(): direction(DirNone), f(-1), g(-1) {}
};
static MapArray<Marking>& mark = *(new MapArray<Marking>(Marking()));

// Path_div is used to modify the heuristic.  The lower the number,
// the higher the heuristic value.  This gives us worse paths, but
// it finds them faster.  This is a variable instead of a constant
// so that I can adjust this dynamically, depending on how much CPU
// time I have.  The more CPU time there is, the better paths I should
// search for.
int path_div = 6;

struct Node
{
    HexCoord h;        // location on the map, in hex coordinates
    int gval;        // g in A* represents how far we've already gone
    int hval;        // h in A* represents an estimate of how far is left

    Node(): h(0,0), gval(0), hval(0) {}
    ~Node() {}
};

bool operator < ( const Node& a, const Node& b )
{
    // To compare two nodes, we compare the `f' value, which is the
    // sum of the g and h values.
    return (a.gval+a.hval) < (b.gval+b.hval);
}

bool operator == ( const Node& a, const Node& b )
{
    // Two nodes are equal if their components are equal
    return (a.h == b.h) && (a.gval == b.gval) && (a.hval == b.hval);
}

inline HexDirection ReverseDirection( HexDirection d )
{
    // With hexagons, I'm numbering the directions 0 = N, 1 = NE,
    // and so on (clockwise).  To flip the direction, I can just
    // add 3, mod 6.
    return HexDirection( ( 3+int(d) ) % 6 );
}

// greater<Node> is an STL thing to create a 'comparison' object out of
// the greater-than operator, and call it comp.
typedef vector<Node> Container;
greater<Node> comp;

// I'm using a priority queue implemented as a heap.  STL has some nice
// heap manipulation functions.  (Look at the source to `priority_queue'
// for details.)  I didn't use priority_queue because later on, I need
// to traverse the entire data structure to update certain elements; the
// abstraction layer on priority_queue wouldn't let me do that.
inline void get_first( Container& v, Node& n )
{
    n = v.front();
    pop_heap( v.begin(), v.end(), comp );
    v.pop_back();
}

// Here's the class that implements A*.  I take a map, two points
// (A and B), and then output the path in the `path' vector, when
// find_path is called.
template <class Heuristic>
struct AStar
{
    PathStats stats;
    Heuristic& heuristic;
    // Remember which nodes are in the OPEN set
    Container open;
    // Remember which nodes we visited, so that we can clear the mark array
    // at the end.  This is the 'CLOSED' set plus the 'OPEN' set.
    Container visited;
    Map& map;
    HexCoord A, B;

    AStar( Heuristic& h, Map& m, HexCoord a, HexCoord b )
        : heuristic(h), map(m), A(a), B(b) {}
    ~AStar();

    // Main function:
    void find_path( vector<HexCoord>& path );

    // Helpers:
    void propagate_down( Node H );
    Container::iterator find_in_open( HexCoord h );
    inline bool in_open( const HexCoord& h )
    {
        return mark.data[h.m][h.n].f != -1;
    }
};

template<class Heuristic>
AStar<Heuristic>::~AStar()
{
    // Erase the mark array, for all items in open or visited
    for( Container::iterator o = open.begin(); o != open.end(); ++o )
    {
        HexCoord h = (*o).h;
        mark.data[h.m][h.n].direction = DirNone;
        mark.data[h.m][h.n].f = -1;
        mark.data[h.m][h.n].g = -1;
    }
    for( Container::iterator v = visited.begin(); v != visited.end(); ++v )
    {
        HexCoord h = (*v).h;
        mark.data[h.m][h.n].direction = DirNone;
        mark.data[h.m][h.n].g = -1;
        assert( !in_open( h ) );
    }
}

template <class Heuristic>
Container::iterator AStar<Heuristic>::find_in_open( HexCoord hn )
{
    // Only search for this node if we know it's in the OPEN set
    if( Map::valid(hn) && in_open(hn) )
    {
        for( Container::iterator i = open.begin(); i != open.end(); ++i )
        {
            stats.nodes_searched++;
            if( (*i).h == hn )
                return i;
        }
    }
    return open.end();
}

// This is the 'propagate down' stage of the algorithm, which I'm not
// sure I did right.
template <class Heuristic>
void AStar<Heuristic>::propagate_down( Node H )
{
    // Keep track of the nodes that we still have to consider
    Container examine;
    examine.push_back(H);
    while( !examine.empty() )
    {
        // Remove one node from the list
        Node N = examine.back();
        examine.pop_back();

        // Examine its neighbors
        for( int dir = 0; dir < 6; ++dir )
        {
            HexDirection d = HexDirection(dir);
            HexCoord hn = Neighbor( N.h, d );
            if( in_open(hn) )
            {
                // This node is in OPEN
                int new_g = N.gval + heuristic.kost( map, N.h, d, hn );

                // Compare this `g' to the stored `g' in the array
                if( new_g < mark.data[hn.m][hn.n].g )
                {
                    Container::iterator i = find_in_open( hn );
                    assert( i != open.end() );
                    assert( mark.data[hn.m][hn.n].g == (*i).gval );

                    // Push this thing UP in the heap (only up allowed!)
                    (*i).gval = new_g;
                    push_heap( open.begin(), i+1, comp );

                    // Set its direction to the parent node
                    mark.data[hn.m][hn.n].g = new_g;
                    mark.data[hn.m][hn.n].f = new_g + (*i).hval;
                    mark.data[hn.m][hn.n].direction = ReverseDirection(d);

                    // Now reset its parent
                    examine.push_back( (*i) );
                }
                else
                {
                    // The new node is no better, so stop here
                }
            }
            else
            {
                // Either it's in closed, or it's not visited yet
            }
        }
    }
}

template <class Heuristic>
void AStar<Heuristic>::find_path( vector<HexCoord>& path )
{
    Node N;
    {
        // insert the original node
        N.h = A;
        N.gval = 0;
        N.hval = heuristic.dist(map,A,B);
        open.push_back(N);
        mark.data[A.m][A.n].f = N.gval+N.hval;
        mark.data[A.m][A.n].g = N.gval;
        stats.nodes_added++;
    }

    // * Things in OPEN are in the open container (which is a heap),
    //   and also their mark[...].f value is nonnegative.
    // * Things in CLOSED are in the visited container (which is unordered),
    //   and also their mark[...].direction value is not DirNone.

    // While there are still nodes to visit, visit them!
    while( !open.empty() )
    {
        get_first( open, N );
        mark.data[N.h.m][N.h.n].f = -1;
        visited.push_back( N );
        stats.nodes_removed++;

        // If we're at the goal, then exit
        if( N.h == B )
            break;

        // If we've looked too long, then exit
        if( stats.nodes_removed >= heuristic.abort_path )
        {
            // Select a good element of OPEN
            for( Container::iterator i = open.begin(); i != open.end(); ++i )
            {
                if( (*i).hval*2 + (*i).gval < N.hval*2 + N.gval )
                    N = *i;
            }

            B = N.h;
            break;
        }

        // Every other column gets a different order of searching dirs
        // (Alternatively, you could pick one at random).  I don't want
        // to be too biased by my choice of order in which I look at the
        // neighboring grid spots.
        int directions1[6] = {0,1,2,3,4,5};
        int directions2[6] = {5,4,3,2,1,0};
        int *directions;
        if( (N.h.m+N.h.n) % 2 == 0 )
            directions = directions1;
        else
            directions = directions2;

        // Look at your neighbors.
        for( int dci = 0; dci < 6; ++dci )
        {
            HexDirection d = HexDirection(directions[dci]);
            HexCoord hn = Neighbor( N.h, d );
            // If it's off the end of the map, then don't keep scanning
            if( !map.valid(hn) )
                continue;

            int k = heuristic.kost(map, N.h, d, hn);
            Node N2;
            N2.h = hn;
            N2.gval = N.gval + k;
            N2.hval = heuristic.dist( map, hn, B );
            // If this spot (hn) hasn't been visited, its mark is DirNone
            if( mark.data[hn.m][hn.n].direction == DirNone )
            {
                // The space is not marked
                mark.data[hn.m][hn.n].direction = ReverseDirection(d);
                mark.data[hn.m][hn.n].f = N2.gval+N2.hval;
                mark.data[hn.m][hn.n].g = N2.gval;
                open.push_back( N2 );
                push_heap( open.begin(), open.end(), comp );
                stats.nodes_added++;
            }
            else
            {
                // We know it's in OPEN or CLOSED...
                if( in_open(hn) )
                {
                    // It's in OPEN, so figure out whether g is better
                    if( N2.gval < mark.data[hn.m][hn.n].g )
                    {
                        // Search for hn in open
                        Container::iterator find1 = find_in_open( hn );
                        assert( find1 != open.end() );

                        // Replace *find1's gval with N2.gval in the list&map
                        mark.data[hn.m][hn.n].direction = ReverseDirection(d);
                        mark.data[hn.m][hn.n].g = N2.gval;
                        mark.data[hn.m][hn.n].f = N2.gval+N2.hval;
                        (*find1).gval = N2.gval;
                        // This is allowed but it's not obvious why:
                        push_heap( open.begin(), find1+1, comp );
                        // (ask Amit if you're curious about it)

                        // This next step is not needed for most games
                        propagate_down( *find1 );
                    }
                }
            }
        }
    }

    if( N.h == B && N.gval < MAXIMUM_PATH_LENGTH )
    {
        stats.path_cost = N.gval;
        // We have found a path, so let's copy it into `path'
        HexCoord h = B;
        while( h != A )
        {
            HexDirection dir = mark.data[h.m][h.n].direction;
            path.push_back( h );
            h = Neighbor( h, dir );
            stats.path_length++;
        }
        path.push_back( A );
        // path now contains the hexes in which the unit must travel ..
        // backwards (like a stack)
    }
    else
    {
        // No path
    }

    stats.nodes_left = open.size();
    stats.nodes_visited = visited.size();
}

////////////////////////////////////////////////////////////////////////
// Specific instantiations of A* for different purposes

// UnitMovement is for moving units (soldiers, builders, firefighters)
struct UnitMovement
{
    HexCoord source;
    Unit* unit;
    int abort_path;

    inline static int dist( const HexCoord& a, const HexCoord& b )
    {
        // The **Manhattan** distance is what should be used in A*'s heuristic
        // distance estimate, *not* the straight-line distance.  This is because
        // A* wants to know the estimated distance for its paths, which involve
        // steps along the grid.  (Of course, if you assign 1.4 to the cost of
        // a diagonal, then you should use a distance function close to the
        // real distance.)

        // Here I compute a "Manhattan" distance for hexagons.  Nifty, eh?
        int a1 = 2*a.m;
        int a2 =  2*a.n+a.m%2 - a.m;
        int a3 = -2*a.n-a.m%2 - a.m; // == -a1-a2
        int b1 = 2*b.m;
        int b2 =  2*b.n+b.m%2 - b.m;
        int b3 = -2*b.n-b.m%2 - b.m; // == -b1-b2

        // One step on the map is 10 in this function
        return 5*max(abs(a1-b1), max(abs(a2-b2), abs(a3-b3)));
    }

    inline int dist( Map& m, const HexCoord& a, const HexCoord& b )
    {
        double dx1 = a.x() - b.x();
        double dy1 = a.y() - b.y();
        double dx2 = source.x() - b.x();
        double dy2 = source.y() - b.y();
        double cross = dx1*dy2-dx2*dy1;
        if( cross < 0 ) cross = -cross;

        return dist( a, b ) + int(cross/20000);
    }

    inline int kost( Map& m, const HexCoord& a,
                     HexDirection d, const HexCoord& b, int pd = -1 )
    {
        // This is the cost of moving one step.  To get completely accurate
        // paths, this must be greater than or equal to the change in the
        // distance function when you take a step.

        if( pd == -1 ) pd = path_div;

        // Check for neighboring moving obstacles
        int occ = m.occupied_[b];
        if( ( occ != -1 && m.units[occ] != unit ) &&
            ( !m.units[occ]->moving() || ( source == a && d != DirNone ) ) )
                return MAXIMUM_PATH_LENGTH;

        // Roads are faster (twice as fast), and cancel altitude effects
        Terrain t1 = m.terrain(a);
        Terrain t2 = m.terrain(b);
        //        int rd = int((t2==Road||t2==Bridge)&&(t1==Road||t2==Bridge));
        // It'd be better theoretically for roads to work only when both
        // hexes are roads, BUT the path finder works faster when
        // it works just when the destination is a road, because it can
        // just step onto a road and know it's going somewhere, as opposed
        // to having to step on the road AND take another step.
        int rd = int(t2==Road || t2==Bridge);
        int rdv = ( 5 - 10 * rd ) * (pd - 3) / 5;
        // Slow everyone down on gates, canals, or walls
        if( t2 == Gate || t2 == Canal )
            rdv += 50;
        if( t2 == Wall )
            rdv += 150;
        // Slow down everyone on water, unless it's on a bridge
        if( t2 != Bridge && m.water(b) > 0 )
            rdv += 30;
        // If there's no road, I take additional items into account
        if( !rd )
        {
            // One thing we can do is penalize for getting OFF a road
            if( t1==Road || t1==Bridge )
                rdv += 15;
            // I take the difference in altitude and use that as a cost,
            // rounded down, which means that small differences cost 0
            int da = (m.altitude(b)-m.altitude(a))/ALTITUDE_SCALE;
            if( da > 0 )
                rdv += da * (pd-3);
        }
        return 10 + rdv;
    }
};

// Some useful functions are exported to be used without the pathfinder

int hex_distance( HexCoord a, HexCoord b )
{
    return UnitMovement::dist( a, b );
}

int movement_cost( Map& m, HexCoord a, HexCoord b, Unit* unit )
{
    UnitMovement um;
    um.unit = unit;
    return um.kost( m, a, DirNone, b, 8 );
}

// BuildingMovement is for drawing straight lines (!)
struct BuildingMovement
{
    HexCoord source;
    int abort_path;

    inline int dist( Map& m, const HexCoord& a, const HexCoord& b )
    {
        double dx1 = a.x() - b.x();
        double dy1 = a.y() - b.y();
        double dd1 = dx1*dx1+dy1*dy1;
        // The cross product will be high if two vectors are not colinear
        // so we can calculate the cross product of [current->goal] and
        // [source->goal] to see if we're staying along the [source->goal]
        // vector.  This will help keep us in a straight line.
        double dx2 = source.x() - b.x();
        double dy2 = source.y() - b.y();
        double cross = dx1*dy2-dx2*dy1;
        if( cross < 0 ) cross = -cross;
        return int( dd1 + cross );
    }

    inline int kost( Map& m, const HexCoord& a,
                     HexDirection d, const HexCoord& b )
    {
        return 0;
    }
};

// Flat Canal movement tries to find a path for a canal that is not too steep
struct FlatCanalPath: public UnitMovement
{
    int kost( Map& m, const HexCoord& a,
                     HexDirection d, const HexCoord& b )
    {
        // Try to minimize the slope
        int a0 = m.altitude(a);
        int bda = 0;
        for( int dir = 0; dir < 6; ++dir )
        {
            int da = a0-m.altitude( Neighbor(a,HexDirection(dir)) );
            if( da > bda ) bda = da;
        }
        return 1 + 100*bda*bda;
    }
};

//////////////////////////////////////////////////////////////////////
// These functions call AStar with the proper heuristic object

PathStats FindUnitPath( Map& map, HexCoord A, HexCoord B,
                        vector<HexCoord>& path, Unit* unit, int cutoff )
{
    UnitMovement um;
    um.source = A;
    um.unit = unit;
    um.abort_path = cutoff * hex_distance(A,B) / 10;

    AStar<UnitMovement> finder( um, map, A, B );

    // If the goal node is unreachable, don't even try
    if( um.kost( map, A, DirNone, B ) == MAXIMUM_PATH_LENGTH )
    {
        // Check to see if a neighbor is reachable.  This is specific
        // to SimBlob and not something for A* -- I want to find a path
        // to a neighbor if the original goal was unreachable (perhaps it
        // is occupied or unpassable).
        int cost = MAXIMUM_PATH_LENGTH;
        HexCoord Bnew = B;
        for( int d = 0; d < 6; ++d )
        {
            HexCoord hn = Neighbor( B, HexDirection(d) );
            int c = um.kost( map, A, DirNone, hn );
            if( c < cost )
            {
                // This one is closer, hopefully
                Bnew = B;
                cost = c;
            }
        }
        // New goal
        B = Bnew;

        if( cost == MAXIMUM_PATH_LENGTH )
        {
            // No neighbor was good
            return finder.stats;
        }
    }

    finder.find_path( path );
    return finder.stats;
}

PathStats FindBuildPath( Map& map, HexCoord A, HexCoord B,
                         vector<HexCoord>& path )
{
    BuildingMovement bm;
    bm.source = A;

    AStar<BuildingMovement> finder( bm, map, A, B );
    finder.find_path( path );
    return finder.stats;
}

PathStats FindCanalPath( Map& map, HexCoord A, HexCoord B,
                         vector<HexCoord>& path )
{
    FlatCanalPath fcp;
    fcp.source = A;

    AStar<FlatCanalPath> finder( fcp, map, A, B );
    finder.find_path( path );
    return finder.stats;
}

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2010-10-28 12:42:52
Processing time 1.5713 sec